lib/dm-core/query.rb
# TODO: break this up into classes for each primary option, eg:
#
# - DataMapper::Query::Fields
# - DataMapper::Query::Links
# - DataMapper::Query::Conditions
# - DataMapper::Query::Offset
# - DataMapper::Query::Limit
# - DataMapper::Query::Order
#
# TODO: move assertions, validations, transformations, and equality
# checking into each class and clean up Query
#
# TODO: add a way to "register" these classes with the Query object
# so that new reserved options can be added in the future. Each
# class will need to implement a "slug" method or something similar
# so that their option namespace can be reserved.
# TODO: move condition transformations into a Query::Conditions
# helper class that knows how to transform the primitives, and
# calls #comparison_for(repository, model) on objects (or some
# other convention that we establish)
module DataMapper
# Query class represents a query which will be run against the data-store.
# Generally Query objects can be found inside Collection objects.
#
class Query
include DataMapper::Assertions
extend Equalizer
OPTIONS = [ :fields, :links, :conditions, :offset, :limit, :order, :unique, :add_reversed, :reload ].to_set.freeze
equalize :repository, :model, :sorted_fields, :links, :conditions, :order, :offset, :limit, :reload?, :unique?, :add_reversed?
# Extract conditions to match a Resource or Collection
#
# @param [Array, Collection, Resource] source
# the source to extract the values from
# @param [ProperySet] source_key
# the key to extract the value from the resource
# @param [ProperySet] target_key
# the key to match the resource with
#
# @return [AbstractComparison, AbstractOperation]
# the conditions to match the resources with
#
# @api private
def self.target_conditions(source, source_key, target_key)
target_key_size = target_key.size
source_values = []
if source.nil?
source_values << [ nil ] * target_key_size
else
Array(source).each do |resource|
next unless source_key.loaded?(resource)
source_value = source_key.get!(resource)
next unless target_key.valid?(source_value)
source_values << source_value
end
end
source_values.uniq!
if target_key_size == 1
target_key = target_key.first
source_values.flatten!
if source_values.size == 1
Conditions::EqualToComparison.new(target_key, source_values.first)
else
Conditions::InclusionComparison.new(target_key, source_values)
end
else
or_operation = Conditions::OrOperation.new
source_values.each do |source_value|
and_operation = Conditions::AndOperation.new
target_key.zip(source_value) do |property, value|
and_operation << Conditions::EqualToComparison.new(property, value)
end
or_operation << and_operation
end
or_operation
end
end
# @param [Repository] repository
# the default repository to scope the query within
# @param [Model] model
# the default model for the query
# @param [#query, Enumerable] source
# the source to generate the query with
#
# @return [Query]
# the query to match the resources with
#
# @api private
def self.target_query(repository, model, source)
if source.respond_to?(:query)
source.query
elsif source.kind_of?(Enumerable)
key = model.key(repository.name)
conditions = Query.target_conditions(source, key, key)
repository.new_query(model, :conditions => conditions)
else
raise ArgumentError, "+source+ must respond to #query or be an Enumerable, but was #{source.class}"
end
end
# Returns the repository query should be
# executed in
#
# Set in cases like the following:
#
# @example
#
# Document.all(:repository => :medline)
#
#
# @return [Repository]
# the Repository to retrieve results from
#
# @api semipublic
attr_reader :repository
# Returns model (class) that is used
# to instantiate objects from query result
# returned by adapter
#
# @return [Model]
# the Model to retrieve results from
#
# @api semipublic
attr_reader :model
# Returns the fields
#
# Set in cases like the following:
#
# @example
#
# Document.all(:fields => [:title, :vernacular_title, :abstract])
#
# @return [PropertySet]
# the properties in the Model that will be retrieved
#
# @api semipublic
attr_reader :fields
# Returns the links (associations) query fetches
#
# @return [Array<DataMapper::Associations::Relationship>]
# the relationships that will be used to scope the results
#
# @api private
attr_reader :links
# Returns the conditions of the query
#
# In the following example:
#
# @example
#
# Team.all(:wins.gt => 30, :conference => 'East')
#
# Conditions are "greater than" operator for "wins"
# field and exact match operator for "conference".
#
# @return [Array]
# the conditions that will be used to scope the results
#
# @api semipublic
attr_reader :conditions
# Returns the offset query uses
#
# Set in cases like the following:
#
# @example
#
# Document.all(:offset => page.offset)
#
# @return [Integer]
# the offset of the results
#
# @api semipublic
attr_reader :offset
# Returns the limit query uses
#
# Set in cases like the following:
#
# @example
#
# Document.all(:limit => 10)
#
# @return [Integer, nil]
# the maximum number of results
#
# @api semipublic
attr_reader :limit
# Returns the order
#
# Set in cases like the following:
#
# @example
#
# Document.all(:order => [:created_at.desc, :length.desc])
#
# query order is a set of two ordering rules, descending on
# "created_at" field and descending again on "length" field
#
# @return [Array]
# the order of results
#
# @api semipublic
attr_reader :order
# Returns the original options
#
# @return [Hash]
# the original options
#
# @api private
attr_reader :options
# Indicates if each result should be returned in reverse order
#
# Set in cases like the following:
#
# @example
#
# Document.all(:limit => 5).reverse
#
# Note that :add_reversed option may be used in conditions directly,
# but this is rarely the case
#
# @return [Boolean]
# true if the results should be reversed, false if not
#
# @api private
def add_reversed?
@add_reversed
end
# Indicates if the Query results should replace the results in the Identity Map
#
# TODO: needs example
#
# @return [Boolean]
# true if the results should be reloaded, false if not
#
# @api semipublic
def reload?
@reload
end
# Indicates if the Query results should be unique
#
# TODO: needs example
#
# @return [Boolean]
# true if the results should be unique, false if not
#
# @api semipublic
def unique?
@unique
end
# Indicates if the Query has raw conditions
#
# @return [Boolean]
# true if the query has raw conditions, false if not
#
# @api semipublic
def raw?
@raw
end
# Indicates if the Query is valid
#
# @return [Boolean]
# true if the query is valid
#
# @api semipublic
def valid?
conditions.valid?
end
# Returns a new Query with a reversed order
#
# @example
#
# Document.all(:limit => 5).reverse
#
# Will execute a single query with correct order
#
# @return [Query]
# new Query with reversed order
#
# @api semipublic
def reverse
dup.reverse!
end
# Reverses the sort order of the Query
#
# @example
#
# Document.all(:limit => 5).reverse
#
# Will execute a single query with original order
# and then reverse collection in the Ruby space
#
# @return [Query]
# self
#
# @api semipublic
def reverse!
# reverse the sort order
@order.map! { |direction| direction.dup.reverse! }
# copy the order to the options
@options = @options.merge(:order => @order).freeze
self
end
# Updates the Query with another Query or conditions
#
# Pretty unrealistic example:
#
# @example
#
# Journal.all(:limit => 2).query.limit # => 2
# Journal.all(:limit => 2).query.update(:limit => 3).limit # => 3
#
# @param [Query, Hash] other
# other Query or conditions
#
# @return [Query]
# self
#
# @api semipublic
def update(other)
other_options = if kind_of?(other.class)
return self if self.eql?(other)
assert_valid_other(other)
other.options
else
other = other.to_hash
return self if other.empty?
other
end
@options = @options.merge(other_options).freeze
assert_valid_options(@options)
normalize = DataMapper::Ext::Hash.only(other_options, *OPTIONS - [ :conditions ]).map do |attribute, value|
instance_variable_set("@#{attribute}", DataMapper::Ext.try_dup(value))
attribute
end
merge_conditions([ DataMapper::Ext::Hash.except(other_options, *OPTIONS), other_options[:conditions] ])
normalize_options(normalize | [ :links, :unique ])
self
end
# Similar to Query#update, but acts on a duplicate.
#
# @param [Query, Hash] other
# other query to merge with
#
# @return [Query]
# updated duplicate of original query
#
# @api semipublic
def merge(other)
dup.update(other)
end
# Builds and returns new query that merges
# original with one given, and slices the result
# with respect to :limit and :offset options
#
# This method is used by Collection to
# concatenate options from multiple chained
# calls in cases like the following:
#
# @example
#
# author.books.all(:year => 2009).all(:published => false)
#
# @api semipublic
def relative(options)
options = options.to_hash
offset = nil
limit = self.limit
if options.key?(:offset) && (options.key?(:limit) || limit)
options = options.dup
offset = options.delete(:offset)
limit = options.delete(:limit) || limit - offset
end
query = merge(options)
query = query.slice!(offset, limit) if offset
query
end
# Return the union with another query
#
# @param [Query] other
# the other query
#
# @return [Query]
# the union of the query and other
#
# @api semipublic
def union(other)
return dup if self == other
set_operation(:union, other)
end
alias_method :|, :union
alias_method :+, :union
# Return the intersection with another query
#
# @param [Query] other
# the other query
#
# @return [Query]
# the intersection of the query and other
#
# @api semipublic
def intersection(other)
return dup if self == other
set_operation(:intersection, other)
end
alias_method :&, :intersection
# Return the difference with another query
#
# @param [Query] other
# the other query
#
# @return [Query]
# the difference of the query and other
#
# @api semipublic
def difference(other)
set_operation(:difference, other)
end
alias_method :-, :difference
# Clear conditions
#
# @return [self]
#
# @api semipublic
def clear
@conditions = Conditions::Operation.new(:null)
self
end
# Takes an Enumerable of records, and destructively filters it.
# First finds all matching conditions, then sorts it,
# then does offset & limit
#
# @param [Enumerable] records
# The set of records to be filtered
#
# @return [Enumerable]
# Whats left of the given array after the filtering
#
# @api semipublic
def filter_records(records)
records = records.uniq if unique?
records = match_records(records) if conditions
records = sort_records(records) if order
records = limit_records(records) if limit || offset > 0
records
end
# Filter a set of records by the conditions
#
# @param [Enumerable] records
# The set of records to be filtered
#
# @return [Enumerable]
# Whats left of the given array after the matching
#
# @api semipublic
def match_records(records)
conditions = self.conditions
records.select { |record| conditions.matches?(record) }
end
# Sorts a list of Records by the order
#
# @param [Enumerable] records
# A list of Resources to sort
#
# @return [Enumerable]
# The sorted records
#
# @api semipublic
def sort_records(records)
sort_order = order.map { |direction| [ direction.target, direction.operator == :asc ] }
records.sort_by do |record|
sort_order.map do |(property, ascending)|
Sort.new(record_value(record, property), ascending)
end
end
end
# Limits a set of records by the offset and/or limit
#
# @param [Enumerable] records
# A list of records to sort
#
# @return [Enumerable]
# The offset & limited records
#
# @api semipublic
def limit_records(records)
offset = self.offset
limit = self.limit
size = records.size
if offset > size - 1
[]
elsif (limit && limit != size) || offset > 0
records[offset, limit || size] || []
else
records.dup
end
end
# Slices collection by adding limit and offset to the
# query, so a single query is executed
#
# @example
#
# Journal.all(:limit => 10).slice(3, 5)
#
# will execute query with the following limit and offset
# (when repository uses DataObjects adapter, and thus
# queries use SQL):
#
# LIMIT 5 OFFSET 3
#
# @api semipublic
def slice(*args)
dup.slice!(*args)
end
alias_method :[], :slice
# Slices collection by adding limit and offset to the
# query, so a single query is executed
#
# @example
#
# Journal.all(:limit => 10).slice!(3, 5)
#
# will execute query with the following limit
# (when repository uses DataObjects adapter, and thus
# queries use SQL):
#
# LIMIT 10
#
# and then takes a slice of collection in the Ruby space
#
# @api semipublic
def slice!(*args)
offset, limit = extract_slice_arguments(*args)
if self.limit || self.offset > 0
offset, limit = get_relative_position(offset, limit)
end
update(:offset => offset, :limit => limit)
end
# Returns detailed human readable
# string representation of the query
#
# @return [String] detailed string representation of the query
#
# @api semipublic
def inspect
attrs = [
[ :repository, repository.name ],
[ :model, model ],
[ :fields, fields ],
[ :links, links ],
[ :conditions, conditions ],
[ :order, order ],
[ :limit, limit ],
[ :offset, offset ],
[ :reload, reload? ],
[ :unique, unique? ],
]
"#<#{self.class.name} #{attrs.map { |key, value| "@#{key}=#{value.inspect}" }.join(' ')}>"
end
# Get the properties used in the conditions
#
# @return [Set<Property>]
# Set of properties used in the conditions
#
# @api private
def condition_properties
properties = Set.new
each_comparison do |comparison|
next unless comparison.respond_to?(:subject)
subject = comparison.subject
properties << subject if subject.kind_of?(Property)
end
properties
end
# Return a list of fields in predictable order
#
# @return [Array<Property>]
# list of fields sorted in deterministic order
#
# @api private
def sorted_fields
fields.sort_by { |property| property.hash }
end
# Transform Query into subquery conditions
#
# @return [AndOperation]
# a subquery for the Query
#
# @api private
def to_subquery
collection = model.all(merge(:fields => model_key))
Conditions::Operation.new(:and, Conditions::Comparison.new(:in, self_relationship, collection))
end
# Hash representation of a Query
#
# @return [Hash]
# Hash representation of a Query
#
# @api private
def to_hash
{
:repository => repository.name,
:model => model.name,
:fields => fields,
:links => links,
:conditions => conditions,
:offset => offset,
:limit => limit,
:order => order,
:unique => unique?,
:add_reversed => add_reversed?,
:reload => reload?,
}
end
# Extract options from a Query
#
# @param [Query] query
# the query to extract options from
#
# @return [Hash]
# the options to use to initialize the new query
#
# @api private
def to_relative_hash
DataMapper::Ext::Hash.only(to_hash, :fields, :order, :unique, :add_reversed, :reload)
end
private
# Initializes a Query instance
#
# @example
#
# JournalIssue.all(:repository => :medline, :created_on.gte => Date.today - 7)
#
# initialized a query with repository defined with name :medline,
# model JournalIssue and options { :created_on.gte => Date.today - 7 }
#
# @param [Repository] repository
# the Repository to retrieve results from
# @param [Model] model
# the Model to retrieve results from
# @param [Hash] options
# the conditions and scope
#
# @api semipublic
def initialize(repository, model, options = {})
assert_kind_of 'repository', repository, Repository
assert_kind_of 'model', model, Model
@repository = repository
@model = model
@options = options.dup.freeze
repository_name = repository.name
@properties = @model.properties(repository_name)
@relationships = @model.relationships(repository_name)
assert_valid_options(@options)
@fields = @options.fetch :fields, @properties.defaults
@links = @options.key?(:links) ? @options[:links].dup : []
@conditions = Conditions::Operation.new(:null)
@offset = @options.fetch :offset, 0
@limit = @options.fetch :limit, nil
@order = @options.fetch :order, @model.default_order(repository_name)
@unique = @options.fetch :unique, true
@add_reversed = @options.fetch :add_reversed, false
@reload = @options.fetch :reload, false
@raw = false
merge_conditions([ DataMapper::Ext::Hash.except(@options, *OPTIONS), @options[:conditions] ])
normalize_options
end
# Copying contructor, called for Query#dup
#
# @api semipublic
def initialize_copy(*)
@fields = @fields.dup
@links = @links.dup
@conditions = @conditions.dup
@order = DataMapper::Ext.try_dup(@order)
end
# Validate the options
#
# @param [#each] options
# the options to validate
#
# @raise [ArgumentError]
# if any pairs in +options+ are invalid options
#
# @api private
def assert_valid_options(options)
options = options.to_hash
options.each do |attribute, value|
case attribute
when :fields then assert_valid_fields(value, options[:unique])
when :links then assert_valid_links(value)
when :conditions then assert_valid_conditions(value)
when :offset then assert_valid_offset(value, options[:limit])
when :limit then assert_valid_limit(value)
when :order then assert_valid_order(value, options[:fields])
when :unique, :add_reversed, :reload then assert_valid_boolean("options[:#{attribute}]", value)
else
assert_valid_conditions(attribute => value)
end
end
end
# Verifies that value of :fields option
# refers to existing properties
#
# @api private
def assert_valid_fields(fields, unique)
valid_properties = model.properties
model.descendants.each do |descendant|
valid_properties += descendant.properties
end
fields.each do |field|
case field
when Symbol, String
unless valid_properties.named?(field)
raise ArgumentError, "+options[:fields]+ entry #{field.inspect} does not map to a property in #{model}"
end
end
end
end
# Verifies that value of :links option
# refers to existing associations
#
# @api private
def assert_valid_links(links)
if links.empty?
raise ArgumentError, '+options[:links]+ should not be empty'
end
links.each do |link|
case link
when Symbol, String
unless @relationships.named?(link.to_sym)
raise ArgumentError, "+options[:links]+ entry #{link.inspect} does not map to a relationship in #{model}"
end
end
end
end
# Verifies that value of :conditions option
# refers to existing properties
#
# @api private
def assert_valid_conditions(conditions)
case conditions
when Hash
conditions.each do |subject, bind_value|
case subject
when Symbol, String
original = subject
subject = subject.to_s
name = subject[0, subject.index('.') || subject.length]
unless @properties.named?(name) || @relationships.named?(name)
raise ArgumentError, "condition #{original.inspect} does not map to a property or relationship in #{model}"
end
end
end
when Array
if conditions.empty?
raise ArgumentError, '+options[:conditions]+ should not be empty'
end
first_condition = conditions.first
unless first_condition.kind_of?(String) && !DataMapper::Ext.blank?(first_condition)
raise ArgumentError, '+options[:conditions]+ should have a statement for the first entry'
end
end
end
# Verifies that query offset is non-negative and only used together with limit
# @api private
def assert_valid_offset(offset, limit)
unless offset >= 0
raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{offset.inspect}"
end
if offset > 0 && limit.nil?
raise ArgumentError, '+options[:offset]+ cannot be greater than 0 if limit is not specified'
end
end
# Verifies the limit is equal to or greater than 0
#
# @raise [ArgumentError]
# raised if the limit is not an Integer or less than 0
#
# @api private
def assert_valid_limit(limit)
unless limit >= 0
raise ArgumentError, "+options[:limit]+ must be greater than or equal to 0, but was #{limit.inspect}"
end
end
# Verifies that :order option uses proper operator and refers
# to existing property
#
# @api private
def assert_valid_order(order, fields)
Array(order).each do |order_entry|
case order_entry
when Symbol, String
unless @properties.named?(order_entry)
raise ArgumentError, "+options[:order]+ entry #{order_entry.inspect} does not map to a property in #{model}"
end
end
end
end
# Used to verify value of boolean properties in conditions
# @api private
def assert_valid_boolean(name, value)
if value != true && value != false
raise ArgumentError, "+#{name}+ should be true or false, but was #{value.inspect}"
end
end
# Verifies that associations given in conditions belong
# to the same repository as query's model
#
# @api private
def assert_valid_other(other)
other_repository = other.repository
repository = self.repository
other_class = other.class
unless other_repository == repository
raise ArgumentError, "+other+ #{other_class} must be for the #{repository.name} repository, not #{other_repository.name}"
end
other_model = other.model
model = self.model
unless other_model >= model
raise ArgumentError, "+other+ #{other_class} must be for the #{model.name} model, not #{other_model.name}"
end
end
# Handle all the conditions options provided
#
# @param [Array<Conditions::AbstractOperation, Conditions::AbstractComparison, Hash, Array>]
# a list of conditions
#
# @return [undefined]
#
# @api private
def merge_conditions(conditions)
@conditions = Conditions::Operation.new(:and) << @conditions unless @conditions.nil?
conditions.compact!
conditions.each do |condition|
case condition
when Conditions::AbstractOperation, Conditions::AbstractComparison
add_condition(condition)
when Hash
condition.each { |key, value| append_condition(key, value) }
when Array
statement, *bind_values = *condition
raw_condition = [ statement ]
raw_condition << bind_values if bind_values.size > 0
add_condition(raw_condition)
@raw = true
end
end
end
# Normalize options
#
# @param [Array<Symbol>] options
# the options to normalize
#
# @return [undefined]
#
# @api private
def normalize_options(options = OPTIONS)
normalize_order if options.include? :order
normalize_fields if options.include? :fields
normalize_links if options.include? :links
normalize_unique if options.include? :unique
end
# Normalize order elements to Query::Direction instances
#
# @api private
def normalize_order
return if @order.nil?
@order = Array(@order).map do |order|
case order
when Direction
order.dup
when Operator
target = order.target
property = target.kind_of?(Property) ? target : @properties[target]
Direction.new(property, order.operator)
when Symbol, String
Direction.new(@properties[order])
when Property
Direction.new(order)
when Path
Direction.new(order.property)
else
order
end
end
end
# Normalize fields to Property instances
#
# @api private
def normalize_fields
@fields = @fields.map do |field|
case field
when Symbol, String
@properties[field]
else
field
end
end
end
# Normalize links to Query::Path
#
# Normalization means links given as symbols are replaced with
# relationships they refer to, intermediate links are "followed"
# and duplicates are removed
#
# @api private
def normalize_links
stack = @links.dup
@links.clear
while link = stack.pop
relationship = case link
when Symbol, String
@relationships[link]
else
link
end
if relationship.respond_to?(:links)
stack.concat(relationship.links)
elsif !@links.include?(relationship)
@links << relationship
end
end
@links.reverse!
end
# Normalize the unique attribute
#
# If any links are present, and the unique attribute was not
# explicitly specified, then make sure the query is marked as unique
#
# @api private
def normalize_unique
@unique = links.any? unless @options.key?(:unique)
end
# Append conditions to this Query
#
# TODO: needs example
#
# @param [Property, Symbol, String, Operator, Associations::Relationship, Path] subject
# the subject to match
# @param [Object] bind_value
# the value to match on
# @param [Symbol] operator
# the operator to match with
#
# @return [Query::Conditions::AbstractOperation]
# the Query conditions
#
# @api private
def append_condition(subject, bind_value, model = self.model, operator = :eql)
case subject
when Property, Associations::Relationship then append_property_condition(subject, bind_value, operator)
when Symbol then append_symbol_condition(subject, bind_value, model, operator)
when String then append_string_condition(subject, bind_value, model, operator)
when Operator then append_operator_conditions(subject, bind_value, model)
when Path then append_path(subject, bind_value, model, operator)
else
raise ArgumentError, "#{subject} is an invalid instance: #{subject.class}"
end
end
# @api private
def equality_operator_for_type(bind_value)
case bind_value
when Model, String then :eql
when Enumerable then :in
when Regexp then :regexp
else :eql
end
end
# @api private
def append_property_condition(subject, bind_value, operator)
negated = operator == :not
if operator == :eql || negated
# transform :relationship => nil into :relationship.not => association
if subject.respond_to?(:collection_for) && bind_value.nil?
negated = !negated
bind_value = collection_for_nil(subject)
end
operator = equality_operator_for_type(bind_value)
end
condition = Conditions::Comparison.new(operator, subject, bind_value)
if negated
condition = Conditions::Operation.new(:not, condition)
end
add_condition(condition)
end
# @api private
def append_symbol_condition(symbol, bind_value, model, operator)
append_condition(symbol.to_s, bind_value, model, operator)
end
# @api private
def append_string_condition(string, bind_value, model, operator)
if string.include?('.')
query_path = model
target_components = string.split('.')
last_component = target_components.last
operator = target_components.pop.to_sym if DataMapper::Query::Conditions::Comparison.slugs.any? { |slug| slug.to_s == last_component }
target_components.each { |method| query_path = query_path.send(method) }
append_condition(query_path, bind_value, model, operator)
else
repository_name = repository.name
subject = model.properties(repository_name)[string] ||
model.relationships(repository_name)[string]
append_condition(subject, bind_value, model, operator)
end
end
# @api private
def append_operator_conditions(operator, bind_value, model)
append_condition(operator.target, bind_value, model, operator.operator)
end
# @api private
def append_path(path, bind_value, model, operator)
path.relationships.each do |relationship|
inverse = relationship.inverse
@links.unshift(inverse) unless @links.include?(inverse)
end
append_condition(path.property, bind_value, path.model, operator)
end
# Add a condition to the Query
#
# @param [AbstractOperation, AbstractComparison]
# the condition to add to the Query
#
# @return [undefined]
#
# @api private
def add_condition(condition)
@conditions = Conditions::Operation.new(:and) if @conditions.nil?
@conditions << condition
end
# Extract arguments for #slice and #slice! then return offset and limit
#
# @param [Integer, Array(Integer), Range] *args the offset,
# offset and limit, or range indicating first and last position
#
# @return [Integer] the offset
# @return [Integer, nil] the limit, if any
#
# @api private
def extract_slice_arguments(*args)
offset, limit = case args.size
when 2 then extract_offset_limit_from_two_arguments(*args)
when 1 then extract_offset_limit_from_one_argument(*args)
end
return offset, limit if offset && limit
raise ArgumentError, "arguments may be 1 or 2 Integers, or 1 Range object, was: #{args.inspect}"
end
# @api private
def extract_offset_limit_from_two_arguments(*args)
args if args.all? { |arg| arg.kind_of?(Integer) }
end
# @api private
def extract_offset_limit_from_one_argument(arg)
case arg
when Integer then extract_offset_limit_from_integer(arg)
when Range then extract_offset_limit_from_range(arg)
end
end
# @api private
def extract_offset_limit_from_integer(integer)
[ integer, 1 ]
end
# @api private
def extract_offset_limit_from_range(range)
offset = range.first
limit = range.last - offset
limit = limit.succ unless range.exclude_end?
return offset, limit
end
# @api private
def get_relative_position(offset, limit)
self_offset = self.offset
self_limit = self.limit
new_offset = self_offset + offset
if limit <= 0 || (self_limit && new_offset + limit > self_offset + self_limit)
raise RangeError, "offset #{offset} and limit #{limit} are outside allowed range"
end
return new_offset, limit
end
# TODO: DRY this up with conditions
# @api private
def record_value(record, property)
case record
when Hash
record.fetch(property, record[property.field])
when Resource
property.get!(record)
end
end
# @api private
def collection_for_nil(relationship)
query = relationship.query.dup
relationship.target_key.each do |target_key|
query[target_key.name.not] = nil if target_key.allow_nil?
end
relationship.target_model.all(query)
end
# @api private
def each_comparison
operands = conditions.operands.to_a
while operand = operands.shift
if operand.respond_to?(:operands)
operands.unshift(*operand.operands)
else
yield operand
end
end
end
# Apply a set operation on self and another query
#
# @param [Symbol] operation
# the set operation to apply
# @param [Query] other
# the other query to apply the set operation on
#
# @return [Query]
# the query that was created for the set operation
#
# @api private
def set_operation(operation, other)
assert_valid_other(other)
query = self.class.new(@repository, @model, other.to_relative_hash)
query.instance_variable_set(:@conditions, other_conditions(other, operation))
query
end
# Return the union with another query's conditions
#
# @param [Query] other
# the query conditions to union with
#
# @return [OrOperation]
# the union of the query conditions and other conditions
#
# @api private
def other_conditions(other, operation)
self_conditions = query_conditions(self)
unless self_conditions.kind_of?(Conditions::Operation)
operation_slug = case operation
when :intersection, :difference then :and
when :union then :or
end
self_conditions = Conditions::Operation.new(operation_slug, self_conditions)
end
self_conditions.send(operation, query_conditions(other))
end
# Extract conditions from a Query
#
# @param [Query] query
# the query with conditions
#
# @return [AbstractOperation]
# the operation
#
# @api private
def query_conditions(query)
if query.limit || query.links.any?
query.to_subquery
else
query.conditions
end
end
# Return a self referrential relationship
#
# @return [Associations::OneToMany::Relationship]
# the 1:m association to the same model
#
# @api private
def self_relationship
@self_relationship ||=
begin
model = self.model
Associations::OneToMany::Relationship.new(
:self,
model,
model,
self_relationship_options
)
end
end
# Return options for the self referrential relationship
#
# @return [Hash]
# the options to use with the self referrential relationship
#
# @api private
def self_relationship_options
keys = model_key.map { |property| property.name }
repository = self.repository
{
:child_key => keys,
:parent_key => keys,
:child_repository_name => repository.name,
:parent_repository_name => repository.name,
}
end
# Return the model key
#
# @return [PropertySet]
# the model key
#
# @api private
def model_key
@properties.key
end
end # class Query
end # module DataMapper