sdks/other/ruby-on-rails/lib/usergrid_ironhorse/query.rb (355 lines of code) (raw):
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# http://guides.rubyonrails.org/active_record_querying.html
module Usergrid
module Ironhorse
class Query
RecordNotFound = ActiveRecord::RecordNotFound
def initialize(model_class)
@model_class = model_class
@options = {}
end
## Initializes new record from relation while maintaining the current
## scope.
##
## Expects arguments in the same format as +Base.new+.
##
## users = User.where(name: 'DHH')
## user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
##
## You can also pass a block to new with the new record as argument:
##
## user = users.new { |user| user.name = 'Oscar' }
## user.name # => Oscar
#def new(*args, &block)
# scoping { @model_class.new(*args, &block) }
#end
# Find by uuid or name - This can either be a specific uuid or name (1), a list of uuids
# or names (1, 5, 6), or an array of uuids or names ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised.
#
# Person.find(1) # returns the object for ID = 1
# Person.find("1") # returns the object for ID = 1
# Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
# Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
def find(*ids)
raise RecordNotFound unless ids
ids = ids.first if ids.first.is_a? Array
@records = ids.collect { |id| find_one! id } # todo: can this be optimized in one call?
#entities = @model_class.resource[ids.join '&'].get.entities
#raise RecordNotFound unless (entities.size == ids.size)
#@records = entities.collect {|entity| @model_class.model_name.constantize.new(entity.data) }
@records.size == 1 ? @records.first : @records
end
# Finds the first record matching the specified conditions. There
# is no implied ordering so if order matters, you should specify it
# yourself.
#
# If no record is found, returns <tt>nil</tt>.
#
# Post.find_by name: 'Spartacus', rating: 4
# Post.find_by "published_at < ?", 2.weeks.ago
def find_by(*conditions)
where(*conditions).take
end
# Like <tt>find_by</tt>, except that if no record is found, raises
# an <tt>ActiveRecord::RecordNotFound</tt> error.
def find_by!(*conditions)
where(*conditions).take!
end
# Gives a record (or N records if a parameter is supplied) without any implied
# order.
#
# Person.take # returns an object fetched by SELECT * FROM people
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
# Person.where(["name LIKE '%?'", name]).take
def take(limit=1)
limit(limit).to_a
end
# Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>take!</tt> accepts no arguments.
def take!
take or raise RecordNotFound
end
# Find the first record (or first N records if a parameter is supplied).
# If no order is defined it will order by primary key.
#
# Person.first # returns the first object fetched by SELECT * FROM people
# Person.where(["user_name = ?", user_name]).first
# Person.where(["user_name = :u", { :u => user_name }]).first
# Person.order("created_on DESC").offset(5).first
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
def first(limit=1)
limit(limit).load.first
end
# Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>first!</tt> accepts no arguments.
def first!
first or raise RecordNotFound
end
# Find the last record (or last N records if a parameter is supplied).
# If no order is defined it will order by primary key.
#
# Person.last # returns the last object fetched by SELECT * FROM people
# Person.where(["user_name = ?", user_name]).last
# Person.order("created_on DESC").offset(5).last
# Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
#
# Take note that in that last case, the results are sorted in ascending order:
#
# [#<Person id:2>, #<Person id:3>, #<Person id:4>]
#
# and not:
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit=1)
limit(limit).reverse_order.load.first
end
# Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>last!</tt> accepts no arguments.
def last!
last or raise RecordNotFound
end
def each
to_a.each { |*block_args| yield(*block_args) }
while @response.data['cursor'] && !limit_value
next_page
to_a.each { |*block_args| yield(*block_args) }
end
end
def next_page
@options[:cursor] = @response.data['cursor']
@records = nil
load
self
end
# Returns +true+ if a record exists in the table that matches the +id+ or
# conditions given, or +false+ otherwise. The argument can take six forms:
#
# * String - Finds the record with a primary key corresponding to this
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
# (such as <tt>['color = ?', 'red']</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{color: 'red'}</tt>).
# * +false+ - Returns always +false+.
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
# For more information about specifying conditions as a Hash or Array,
# see the Conditions section in the introduction to ActiveRecord::Base.
def exists?(conditions=nil)
# todo: does not yet handle all conditions described above
case conditions
when Array, Hash
pluck :uuid
!where(conditions).take.empty?
else
!!find_one(conditions)
end
end
alias_method :any?, :exists?
alias_method :many?, :exists?
def limit(limit=1)
@options[:limit] = limit
self
end
def offset(num)
@options[:offset] = num
self
end
# Removes from the query the condition(s) specified in +skips+.
#
# Example:
#
# Post.order('id asc').except(:order) # discards the order condition
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
#
def except(*skips)
skips.each {|option| @options.delete option}
end
# Removes any condition from the query other than the one(s) specified in +onlies+.
#
# Example:
#
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
#
def only(*onlies)
@options.keys do |k|
unless onlines.include? k
@options.delete k
end
end
end
# Allows to specify an order attribute:
#
# User.order('name')
# => SELECT "users".* FROM "users" ORDER BY name
#
# User.order('name DESC')
# => SELECT "users".* FROM "users" ORDER BY name DESC
#
# User.order('name DESC, email')
# => SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
@options[:order] << args
end
# Replaces any existing order defined on the relation with the specified order.
#
# User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
#
# Subsequent calls to order on the same relation will be appended. For example:
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
# generates a query with 'ORDER BY name ASC, id ASC'.
def reorder(*args)
@options[:order] = args
end
def all
@options[:conditions] = nil
self
end
# Works in two unique ways.
#
# First: takes a block so it can be used just like Array#select.
#
# Model.all.select { |m| m.field == value }
#
# This will build an array of objects from the database for the scope,
# converting them into an array and iterating through them using Array#select.
#
# Second: Modifies the SELECT statement for the query so that only certain
# fields are retrieved:
#
# Model.select(:field)
# # => [#<Model field:value>]
#
# Although in the above example it looks as though this method returns an
# array, it actually returns a relation object and can have other query
# methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
#
# The argument to the method can also be an array of fields.
#
# Model.select(:field, :other_field, :and_one_more)
# # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
#
def select(*fields)
if block_given?
to_a.select { |*block_args| yield(*block_args) }
else
raise ArgumentError, 'Call this with at least one field' if fields.empty?
clone.select!(*fields)
end
end
# Like #select, but modifies relation in place.
def select!(*fields)
@options[:select] ||= fields.join ','
self
end
alias_method :pluck, :select!
def reverse_order
@options[:reversed] = true
self
end
def readonly
@options[:readonly] = true
self
end
def to_a
load
@records
end
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
# Returns size of the results (not size of the stored collection)
def size
loaded? ? @records.length : count
end
# true if there are no records
def empty?
return @records.empty? if loaded?
c = count
c.respond_to?(:zero?) ? c.zero? : c.empty?
end
# true if there are any records
def any?
if block_given?
to_a.any? { |*block_args| yield(*block_args) }
else
!empty?
end
end
# true if there is more than one record
def many?
if block_given?
to_a.many? { |*block_args| yield(*block_args) }
else
limit_value ? to_a.many? : size > 1
end
end
# find_all_by_
# find_by_
# find_first_by_
# find_last_by_
def method_missing(method_name, *args)
method = method_name.to_s
if method.end_with? '!'
method.chop!
error_on_empty = true
end
if method.start_with? 'find_all_by_'
attribs = method.gsub /^find_all_by_/, ''
elsif method.start_with? 'find_by_'
attribs = method.gsub /^find_by_/, ''
limit(1)
elsif method.start_with? 'find_first_by_'
limit(1)
find_first = true
attribs = method.gsub /^find_first_by_/, ''
elsif method.start_with? 'find_last_by_'
limit(1)
find_last = true
attribs = method.gsub /^find_last_by_/, ''
else
super
end
attribs = attribs.split '_and_'
conditions = {}
attribs.each { |attr| conditions[attr] = args.shift }
where(conditions, *args)
load
raise RecordNotFound if error_on_empty && @records.empty?
return @records.first if limit_value == 1
@records
end
# Tries to create a new record with the same scoped attributes
# defined in the relation. Returns the initialized object if validation fails.
#
# Expects arguments in the same format as +Base.create+.
#
# ==== Examples
# users = User.where(name: 'Oscar')
# users.create # #<User id: 3, name: "oscar", ...>
#
# users.create(name: 'fxn')
# users.create # #<User id: 4, name: "fxn", ...>
#
# users.create { |user| user.name = 'tenderlove' }
# # #<User id: 5, name: "tenderlove", ...>
#
# users.create(name: nil) # validation on name
# # #<User id: nil, name: nil, ...>
def create(*args, &block)
@model_class.create(*args, &block)
end
# Similar to #create, but calls +create!+ on the base class. Raises
# an exception if a validation error occurs.
#
# Expects arguments in the same format as <tt>Base.create!</tt>.
def create!(*args, &block)
@model_class.create!(*args, &block)
end
# Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
#
# Expects arguments in the same format as +Base.create+.
#
# ==== Examples
# # Find the first user named Penélope or create a new one.
# User.where(:first_name => 'Penélope').first_or_create
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
#
# # Find the first user named Penélope or create a new one.
# # We already have one so the existing record will be returned.
# User.where(:first_name => 'Penélope').first_or_create
# # => <User id: 1, first_name: 'Penélope', last_name: nil>
#
# # Find the first user named Scarlett or create a new one with a particular last name.
# User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
#
# # Find the first user named Scarlett or create a new one with a different last name.
# # We already have one so the existing record will be returned.
# User.where(:first_name => 'Scarlett').first_or_create do |user|
# user.last_name = "O'Hara"
# end
# # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
def first_or_create(attributes={}, &block)
result = first
unless result
attributes = @options[:hash].merge(attributes) if @options[:hash]
result = create(attributes, &block)
end
result
end
# Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
#
# Expects arguments in the same format as <tt>Base.create!</tt>.
def first_or_create!(attributes={}, &block)
result = first
unless result
attributes = @options[:hash].merge(attributes) if @options[:hash]
result = create!(attributes, &block)
end
result
end
# Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
#
# Expects arguments in the same format as <tt>Base.new</tt>.
def first_or_initialize(attributes={}, &block)
result = first
unless result
attributes = @options[:hash].merge(attributes) if @options[:hash]
result = @model_class.new(attributes, &block)
end
result
end
# Destroys the records matching +conditions+ by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
# executed (including <tt>:dependent</tt> association options and
# +before_destroy+/+after_destroy+ Observer methods). Returns the
# collection of objects that were destroyed; each will be frozen, to
# reflect that no changes should be made (since they can't be
# persisted).
#
# Note: Instantiation, callback execution, and deletion of each
# record can be time consuming when you're removing many records at
# once. It generates at least one SQL +DELETE+ query per record (or
# possibly more, to enforce your callbacks). If you want to delete many
# rows quickly, without concern for their associations or callbacks, use
# +delete_all+ instead.
#
# ==== Parameters
#
# * +conditions+ - A string, array, or hash that specifies which records
# to destroy. If omitted, all records are destroyed. See the
# Conditions section in the introduction to ActiveRecord::Base for
# more information.
#
# ==== Examples
#
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(status: "inactive")
# Person.where(:age => 0..18).destroy_all
def destroy_all(conditions=nil)
if conditions
where(conditions).destroy_all
else
to_a.each {|object| object.destroy}
@records
end
end
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
# therefore all callbacks and filters are fired off before the object is deleted. This method is
# less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
#
# This essentially finds the object (or multiple objects) with the given id, creates a new object
# from the attributes, and then calls destroy on it.
#
# ==== Parameters
#
# * +id+ - Can be either an Integer or an Array of Integers.
#
# ==== Examples
#
# # Destroy a single object
# Foo.destroy(1)
#
# # Destroy multiple objects
# foos = [1,2,3]
# Foo.destroy(foos)
def destroy(id)
if id.is_a?(Array)
id.map {|one_id| destroy(one_id)}
else
find(id).destroy
end
end
# Deletes the records matching +conditions+ without instantiating the records
# first, and hence not calling the +destroy+ method nor invoking callbacks. This
# is a single SQL DELETE statement that goes straight to the database, much more
# efficient than +destroy_all+. Be careful with relations though, in particular
# <tt>:dependent</tt> rules defined on associations are not honored. Returns the
# number of rows affected.
#
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
# Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
#
# If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
#
# Post.limit(100).delete_all
# # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
def delete_all(conditions=nil)
raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
if conditions
where(conditions).delete_all
else
pluck :uuid
response = load
response.each {|entity| entity.delete} # todo: can this be optimized into one call?
response.size
end
end
# Deletes the row with a primary key matching the +id+ argument, using a
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
# Record objects are not instantiated, so the object's callbacks are not
# executed, including any <tt>:dependent</tt> association options or
# Observer methods.
#
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
#
# Note: Although it is often much faster than the alternative,
# <tt>#destroy</tt>, skipping callbacks might bypass business logic in
# your application that ensures referential integrity or performs other
# essential jobs.
#
# ==== Examples
#
# # Delete a single row
# Foo.delete(1)
#
# # Delete multiple rows
# Foo.delete([2,3,4])
def delete(id_or_array)
if id_or_array.is_a? Array
id_or_array.each {|id| @model_class.resource[id].delete} # todo: can this be optimized into one call?
else
@model_class.resource[id_or_array].delete
end
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
# also be supplied. This method sends a single update straight to the database. It does not instantiate
# the involved models and it does not trigger Active Record callbacks or validations.
#
# ==== Parameters
#
# * +updates+ - hash of attribute updates
#
# ==== Examples
#
# # Update all customers with the given attributes
# Customer.update_all wants_email: true
#
# # Update all books with 'Rails' in their title
# Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
#
# # Update all books that match conditions, but limit it to 5 ordered by date
# Book.where('title LIKE ?', '%Rails%').order(:created).limit(5).update_all(:author => 'David')
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
raise ArgumentError, "updates must be a Hash" unless updates.is_a? Hash
run_update(updates)
end
# Looping through a collection of records from the database
# (using the +all+ method, for example) is very inefficient
# since it will try to instantiate all the objects at once.
#
# In that case, batch processing methods allow you to work
# with the records in batches, thereby greatly reducing memory consumption.
#
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
# specified by the +:batch_size+ option).
#
# Person.all.find_each do |person|
# person.do_awesome_stuff
# end
#
# Person.where("age > 21").find_each do |person|
# person.party_all_night!
# end
#
# You can also pass the +:start+ option to specify
# an offset to control the starting point.
def find_each(options = {})
find_in_batches(options) do |records|
records.each { |record| yield record }
end
end
# Yields each batch of records that was found by the find +options+ as
# an array. The size of each batch is set by the +:batch_size+
# option; the default is 1000.
#
# You can control the starting point for the batch processing by
# supplying the +:start+ option. This is especially useful if you
# want multiple workers dealing with the same processing queue. You can
# make worker 1 handle all the records between id 0 and 10,000 and
# worker 2 handle from 10,000 and beyond (by setting the +:start+
# option on that worker).
#
# It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
# work. This also mean that this method only works with integer-based
# primary keys. You can't set the limit either, that's used to control
# the batch sizes.
#
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
#
# # Let's process the next 2000 records
# Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
# group.each { |person| person.party_all_night! }
# end
def find_in_batches(options={})
options.assert_valid_keys(:start, :batch_size)
raise "Not yet implemented" # todo
start = options.delete(:start) || 0
batch_size = options.delete(:batch_size) || 1000
while records.any?
records_size = records.size
primary_key_offset = records.last.id
yield records
break if records_size < batch_size
if primary_key_offset
records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
else
raise "Primary key not included in the custom select clause"
end
end
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
# The resulting object is returned whether the object was saved successfully to the database or not.
#
# ==== Parameters
#
# * +id+ - This should be the id or an array of ids to be updated.
# * +attributes+ - This should be a hash of attributes or an array of hashes.
#
# ==== Examples
#
# # Updates one record
# Person.update(15, user_name: 'Samuel', group: 'expert')
#
# # Updates multiple records
# people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
# Person.update(people.keys, people.values)
def update(id, attributes)
if id.is_a?(Array)
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
object.update_attributes(attributes)
object
end
end
## todo: scoping
## Scope all queries to the current scope.
##
## Comment.where(:post_id => 1).scoping do
## Comment.first # SELECT * FROM comments WHERE post_id = 1
## end
##
## Please check unscoped if you want to remove all previous scopes (including
## the default_scope) during the execution of a block.
#def scoping
# previous, @model_class.current_scope = @model_class.current_scope, self
# yield
#ensure
# klass.current_scope = previous
#end
# #where accepts conditions in one of several formats.
#
# === string
#
# A single string, without additional arguments, is used in the where clause of the query.
#
# Client.where("orders_count = '2'")
# # SELECT * where orders_count = '2';
#
# Note that building your own string from user input may expose your application
# to injection attacks if not done properly. As an alternative, it is recommended
# to use one of the following methods.
#
# === array
#
# If an array is passed, then the first element of the array is treated as a template, and
# the remaining elements are inserted into the template to generate the condition.
# Active Record takes care of building the query to avoid injection attacks, and will
# convert from the ruby type to the database type where needed. Elements are inserted
# into the string in the order in which they appear.
#
# User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
#
# Alternatively, you can use named placeholders in the template, and pass a hash as the
# second element of the array. The names in the template are replaced with the corresponding
# values from the hash.
#
# User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
#
# This can make for more readable code in complex queries.
#
# Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
# than the previous methods; you are responsible for ensuring that the values in the template
# are properly quoted. The values are passed to the connector for quoting, but the caller
# is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
# the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
#
# User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
#
# If #where is called with multiple arguments, these are treated as if they were passed as
# the elements of a single array.
#
# User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
#
# When using strings to specify conditions, you can use any operator available from
# the database. While this provides the most flexibility, you can also unintentionally introduce
# dependencies on the underlying database. If your code is intended for general consumption,
# test with multiple database backends.
#
# === hash
#
# #where will also accept a hash condition, in which the keys are fields and the values
# are values to be searched for.
#
# Fields can be symbols or strings. Values can be single values, arrays, or ranges.
#
# User.where({ name: "Joe", email: "joe@example.com" })
# # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com'
#
# User.where({ name: ["Alice", "Bob"]})
# # SELECT * WHERE name IN ('Alice', 'Bob')
#
# User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
# # SELECT * WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
#
# In the case of a belongs_to relationship, an association key can be used
# to specify the model if an ActiveRecord object is used as the value.
#
# author = Author.find(1)
#
# # The following queries will be equivalent:
# Post.where(:author => author)
# Post.where(:author_id => author)
#
# === empty condition
#
# If the condition returns true for blank?, then where is a no-op and returns the current relation.
#
def where(opts, *rest)
return self if opts.blank?
case opts
when Hash
@options[:hash] = opts # keep around for first_or_create stuff...
opts.each do |k,v|
# todo: can we support IN and BETWEEN syntax as documented above?
v = "'#{v}'" if v.is_a? String
query_conditions << "#{k} = #{v}"
end
when String
query_conditions << opts
when Array
query = opts.shift.gsub '?', "'%s'"
query = query % opts
query_conditions << query
end
self
end
protected
def limit_value
@options[:limit]
end
def query_conditions
@options[:conditions] ||= []
end
def loaded?
!!@records
end
def reversed?
!!@options[:reversed]
end
def find_one(id_or_name=nil)
begin
entity = @model_class.resource[id_or_name].query(nil, limit: 1).entity
@model_class.model_name.constantize.new(entity.data) if entity
rescue RestClient::ResourceNotFound
nil
end
end
def find_one!(id_or_name=nil)
find_one(id_or_name) or raise RecordNotFound
end
# Server-side options:
# Xql string Query in the query language
# type string Entity type to return
# Xreversed string Return results in reverse order
# connection string Connection type (e.g., "likes")
# start string First entity's UUID to return
# cursor string Encoded representation of the query position for paging
# Xlimit integer Number of results to return
# permission string Permission type
# Xfilter string Condition on which to filter
def query_options
# todo: support more options?
options = {}
options.merge!({:limit => limit_value.to_json}) if limit_value
options.merge!({:skip => @options[:skip].to_json}) if @options[:skip]
options.merge!({:reversed => reversed?.to_json}) if reversed?
options.merge!({:order => @options[:order]}) if @options[:order]
options.merge!({:cursor => @options[:cursor]}) if @options[:cursor]
options
end
def create_query
select = @options[:select] || '*'
where = ('where ' + query_conditions.join(' and ')) unless query_conditions.blank?
"select #{select} #{where}"
end
def run_query
@model_class.resource.query(create_query, query_options)
end
def run_update(attributes)
@model_class.resource.update_query(attributes, create_query, query_options)
end
def load
return if loaded?
begin
@response = run_query
if (!@options[:select] or @options[:select] == '*')
@records = @response.entities.collect {|r| @model_class.model_name.constantize.new(r.data)}
else # handle list
selects = @options[:select].split ','
@records = @response.entities.collect do |r|
data = {}
(0..selects.size).each do |i|
data[selects[i]] = r[i]
end
@model_class.model_name.constantize.new(data)
end
end
rescue RestClient::ResourceNotFound
@records = []
end
end
end
end
end