module DataMapper class Query # The Conditions module contains classes used as part of a Query when # filtering collections of resources. # # The Conditions module contains two types of class used for filtering # queries: Comparison and Operation. Although these are used on all # repository types -- not just SQL-based repos -- these classes are best # thought of as being the DataMapper counterpart to an SQL WHERE clause. # # Comparisons compare properties and relationships with values, while # operations tie Comparisons together to form more complex expressions. # # For example, the following SQL query fragment: # # ... WHERE my_field = my_value AND another_field = another_value ... # # ... would be represented as two EqualToComparison instances tied # together with an AndOperation. # # Conditions -- together with the Query class -- allow DataMapper to # represent SQL-like expressions in an ORM-agnostic manner, and are used # for both in-memory filtering of loaded Collection instances, and by # adapters to retrieve records directly from your repositories. # # The classes contained in the Conditions module are for internal use by # DataMapper and DataMapper plugins, and are not intended to be used # directly in your applications. module Conditions # An abstract class which provides easy access to comparison operators # # @example Creating a new comparison # Comparison.new(:eql, MyClass.my_property, "value") # class Comparison # Creates a new Comparison instance # # The returned instance will be suitable for matching the given # subject (property or relationship) against the value. # # @param [Symbol] slug # The type of comparison operator required. One of: :eql, :in, :gt, # :gte, :lt, :lte, :regexp, :like. # @param [Property, Associations::Relationship] # The subject of the comparison - the value of the subject will be # matched against the given value parameter. # @param [Object] value # The value for the comparison. # # @return [DataMapper::Query::Conditions::AbstractComparison] # # @example # Comparison.new(:eql, MyClass.properties[:id], 1) # # @api semipublic def self.new(slug, subject, value) if klass = comparison_class(slug) klass.new(subject, value) else raise ArgumentError, "No Comparison class for #{slug.inspect} has been defined" end end # Returns an array of all slugs registered with Comparison # # @return [Array] # # @api private def self.slugs AbstractComparison.descendants.map { |comparison_class| comparison_class.slug } end class << self private # Holds comparison subclasses keyed on their slug # # @return [Hash] # # @api private def comparison_classes @comparison_classes ||= {} end # Returns the comparison class identified by the given slug # # @param [Symbol] slug # See slug parameter for Comparison.new # # @return [AbstractComparison, nil] # # @api private def comparison_class(slug) comparison_classes[slug] ||= AbstractComparison.descendants.detect { |comparison_class| comparison_class.slug == slug } end end end # class Comparison # A base class for the various comparison classes. class AbstractComparison extend Equalizer equalize :subject, :value # @api semipublic attr_accessor :parent # The property or relationship which is being matched against # # @return [Property, Associations::Relationship] # # @api semipublic attr_reader :subject # Value to be compared with the subject # # This value is compared against that contained in the subject when # filtering collections, or the value in the repository when # performing queries. # # In the case of primitive property, this is the value as it # is stored in the repository. # # @return [Object] # # @api semipublic def value dumped_value end # The loaded/typecast value # # In the case of primitive types, this will be the same as +value+, # however when using primitive property this stores the loaded value. # # If writing an adapter, you should use +value+, while plugin authors # should refer to +loaded_value+. # #-- # As an example, you might use symbols with the Enum type in dm-types # # property :myprop, Enum[:open, :closed] # # These are stored in repositories as 1 and 2, respectively. +value+ # returns the 1 or 2, while +loaded_value+ returns the symbol. #++ # # @return [Object] # # @api semipublic attr_reader :loaded_value # Keeps track of AbstractComparison subclasses (used in Comparison) # # @return [Set] # @api private def self.descendants @descendants ||= DescendantSet.new end # Registers AbstractComparison subclasses (used in Comparison) # # @api private def self.inherited(descendant) descendants << descendant end # Setter/getter: allows subclasses to easily set their slug # # @param [Symbol] slug # The slug to be set for this class. Passing nil returns the current # value instead. # # @return [Symbol] # The current slug set for the Comparison. # # @example Creating a MyComparison compairson with slug :exact. # class MyComparison < AbstractComparison # slug :exact # end # # @api semipublic def self.slug(slug = nil) slug ? @slug = slug : @slug end # Return the comparison class slug # # @return [Symbol] # the comparison class slug # # @api private def slug self.class.slug end # Test that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) match_property?(record) end # Tests that the Comparison is valid # # Subclasses can overload this to customise the means by which they # determine the validity of the comparison. #valid? is called prior to # performing a query on the repository: each Comparison within a Query # must be valid otherwise the query will not be performed. # # @see DataMapper::Property#valid? # @see DataMapper::Associations::Relationship#valid? # # @return [Boolean] # # @api semipublic def valid? valid_for_subject?(loaded_value) end # Returns whether the subject is a Relationship # # @return [Boolean] # # @api semipublic def relationship? false end # Returns whether the subject is a Property # # @return [Boolean] # # @api semipublic def property? subject.kind_of?(Property) end # Returns a human-readable representation of this object # # @return [String] # # @api semipublic def inspect "#<#{self.class} @subject=#{@subject.inspect} " \ "@dumped_value=#{@dumped_value.inspect} @loaded_value=#{@loaded_value.inspect}>" end # Returns a string version of this Comparison object # # @example # Comparison.new(:==, MyClass.my_property, "value") # # => "my_property == value" # # @return [String] # # @api semipublic def to_s "#{subject.name} #{comparator_string} #{dumped_value.inspect}" end # @api private def negated? parent = self.parent parent ? parent.negated? : false end private # @api private attr_reader :dumped_value # Creates a new AbstractComparison instance with +subject+ and +value+ # # @param [Property, Associations::Relationship] subject # The subject of the comparison - the value of the subject will be # matched against the given value parameter. # @param [Object] value # The value for the comparison. # # @api semipublic def initialize(subject, value) @subject = subject @loaded_value = typecast(value) @dumped_value = dump end # @api private def match_property?(record, operator = :===) expected.send(operator, record_value(record)) end # Typecasts the given +val+ using subject#typecast # # If the subject has no typecast method the value is returned without # any changes. # # @param [Object] val # The object to attempt to typecast. # # @return [Object] # The typecasted object. # # @see Property#typecast # # @api private def typecast(value) typecast_property(value) end # @api private def typecast_property(value) subject.typecast(value) end # Dumps the given loaded_value using subject#value # # This converts property values to the primitive as stored in the # repository. # # @return [Object] # The raw (dumped) object. # # @see Property#value # # @api private def dump dump_property(loaded_value) end # @api private def dump_property(value) subject.dump(value) end # Returns a value for the comparison +subject+ # # Extracts value for the +subject+ property or relationship from the # given +record+, where +record+ is a Resource instance or a Hash. # # @param [DataMapper::Resource, Hash] record # The resource or hash from which to retrieve the value. # @param [Property, Associations::Relationship] # The subject of the comparison. For example, if this is a property, # the value for the resources +subject+ property is retrieved. # @param [Symbol] key_type # In the event that +subject+ is a relationship, key_type indicated # which key should be used to retrieve the value from the resource. # # @return [Object] # # @api semipublic def record_value(record, key_type = :source_key) subject = self.subject case record when Hash record_value_from_hash(record, subject, key_type) when Resource record_value_from_resource(record, subject, key_type) else record end end # Returns a value from a record hash # # Retrieves value for the +subject+ property or relationship from the # given +hash+. # # @return [Object] # # @see AbstractComparison#record_value # # @api private def record_value_from_hash(hash, subject, key_type) hash.fetch subject, case subject when Property subject.load(hash[subject.field]) when Associations::Relationship subject.send(key_type).map { |property| record_value_from_hash(hash, property, key_type) } end end # Returns a value from a resource # # Extracts value for the +subject+ property or relationship from the # given +resource+. # # @return [Object] # # @see AbstractComparison#record_value # # @api private def record_value_from_resource(resource, subject, key_type) case subject when Property subject.get!(resource) when Associations::Relationship subject.send(key_type).get!(resource) end end # Retrieves the value of the +subject+ # # @return [Object] # # @api semipublic def expected(value = @loaded_value) expected = record_value(value, :target_key) if @subject.respond_to?(:source_key) @subject.source_key.typecast(expected) else expected end end # Test the value to see if it is valid # # @return [Boolean] true if the value is valid # # @api semipublic def valid_for_subject?(loaded_value) subject.valid?(loaded_value, negated?) end end # class AbstractComparison # Included into comparisons which are capable of supporting # Relationships. module RelationshipHandler # Returns whether this comparison subject is a Relationship # # @return [Boolean] # # @api semipublic def relationship? subject.kind_of?(Associations::Relationship) end # Tests that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) if relationship? && expected.respond_to?(:query) match_relationship?(record) else super end end # Returns the conditions required to match the subject relationship # # @return [Hash] # # @api semipublic def foreign_key_mapping relationship = subject.inverse relationship = relationship.links.first if relationship.respond_to?(:links) Query.target_conditions(value, relationship.source_key, relationship.target_key) end private # @api private def match_relationship?(record) expected.query.conditions.matches?(record_value(record)) end # Typecasts each value in the inclusion set # # @return [Array] # # @see AbtractComparison#typecast # # @api private def typecast(value) if relationship? typecast_relationship(value) else super end end # @api private def dump if relationship? dump_relationship(loaded_value) else super end end # @api private def dump_relationship(value) value end end # module RelationshipHandler # Tests whether the value in the record is equal to the expected # set for the Comparison. class EqualToComparison < AbstractComparison include RelationshipHandler slug :eql # Tests that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) if expected.nil? record_value(record).nil? else super end end private # @api private def typecast_relationship(value) case value when Hash then typecast_hash(value) when Resource then typecast_resource(value) end end # @api private def typecast_hash(hash) subject = self.subject subject.target_model.new(subject.query.merge(hash)) end # @api private def typecast_resource(resource) resource end # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string '=' end end # class EqualToComparison # Tests whether the value in the record is contained in the # expected set for the Comparison, where expected is an # Array, Range, or Set. class InclusionComparison < AbstractComparison include RelationshipHandler slug :in # Checks that the Comparison is valid # # @see DataMapper::Query::Conditions::AbstractComparison#valid? # # @return [Boolean] # # @api semipublic def valid? loaded_value = self.loaded_value case loaded_value when Collection then valid_collection?(loaded_value) when Range then valid_range?(loaded_value) when Enumerable then valid_enumerable?(loaded_value) else false end end private # @api private def match_property?(record) super(record, :include?) end # Overloads AbtractComparison#expected # # @return [Array] # @see AbtractComparison#expected # # @api private def expected loaded_value = self.loaded_value if loaded_value.kind_of?(Range) typecast_range(loaded_value) elsif loaded_value.respond_to?(:map) # FIXME: causes a lazy load when a Collection loaded_value.map { |val| super(val) } else super end end # @api private def valid_collection?(collection) valid_for_subject?(collection) end # @api private def valid_range?(range) (range.any? || negated?) && valid_for_subject?(range.first) && valid_for_subject?(range.last) end # @api private def valid_enumerable?(enumerable) (!enumerable.empty? || negated?) && enumerable.all? { |entry| valid_for_subject?(entry) } end # @api private def typecast_property(value) if value.kind_of?(Range) typecast_range(value) elsif value.respond_to?(:map) && !value.kind_of?(String) value.map { |entry| super(entry) } else super end end # @api private def typecast_range(range) range.class.new(typecast_property(range.first), typecast_property(range.last), range.exclude_end?) end # @api private def typecast_relationship(value) case value when Hash then typecast_hash(value) when Resource then typecast_resource(value) when Collection then typecast_collection(value) when Enumerable then typecast_enumerable(value) end end # @api private def typecast_hash(hash) subject = self.subject subject.target_model.all(subject.query.merge(hash)) end # @api private def typecast_resource(resource) resource.collection_for_self end # @api private def typecast_collection(collection) collection end # @api private def typecast_enumerable(enumerable) collection = nil enumerable.each do |entry| typecasted = typecast_relationship(entry) if collection collection |= typecasted else collection = typecasted end end collection end # Dumps the given +val+ using subject#value # # @return [Array] # # @see AbtractComparison#dump # # @api private def dump loaded_value = self.loaded_value if subject.respond_to?(:dump) && loaded_value.respond_to?(:map) && !loaded_value.kind_of?(Range) dumped_value = loaded_value.map { |value| dump_property(value) } dumped_value.uniq! dumped_value else super end end # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string 'IN' end end # class InclusionComparison # Tests whether the value in the record matches the expected # regexp set for the Comparison. class RegexpComparison < AbstractComparison slug :regexp # Checks that the Comparison is valid # # @see AbstractComparison#valid? # # @api semipublic def valid? loaded_value.kind_of?(Regexp) end private # Returns the value untouched # # @return [Object] # # @api private def typecast(value) value end # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string '=~' end end # class RegexpComparison # Tests whether the value in the record is like the expected set # for the Comparison. Equivalent to a LIKE clause in an SQL database. # # TODO: move this to dm-more with DataObjectsAdapter plugins class LikeComparison < AbstractComparison slug :like private # Overloads the +expected+ method in AbstractComparison # # Return a regular expression suitable for matching against the # records value. # # @return [Regexp] # # @see AbtractComparison#expected # # @api semipublic def expected Regexp.new('\A' << super.gsub('%', '.*').tr('_', '.') << '\z') end # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string 'LIKE' end end # class LikeComparison # Tests whether the value in the record is greater than the # expected set for the Comparison. class GreaterThanComparison < AbstractComparison slug :gt # Tests that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) return false if expected.nil? record_value = record_value(record) !record_value.nil? && record_value > expected end private # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string '>' end end # class GreaterThanComparison # Tests whether the value in the record is less than the expected # set for the Comparison. class LessThanComparison < AbstractComparison slug :lt # Tests that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) return false if expected.nil? record_value = record_value(record) !record_value.nil? && record_value < expected end private # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string '<' end end # class LessThanComparison # Tests whether the value in the record is greater than, or equal to, # the expected set for the Comparison. class GreaterThanOrEqualToComparison < AbstractComparison slug :gte # Tests that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) return false if expected.nil? record_value = record_value(record) !record_value.nil? && record_value >= expected end private # @see AbstractComparison#to_s # # @api private def comparator_string '>=' end end # class GreaterThanOrEqualToComparison # Tests whether the value in the record is less than, or equal to, the # expected set for the Comparison. class LessThanOrEqualToComparison < AbstractComparison slug :lte # Tests that the record value matches the comparison # # @param [Resource, Hash] record # The record containing the value to be matched # # @return [Boolean] # # @api semipublic def matches?(record) return false if expected.nil? record_value = record_value(record) !record_value.nil? && record_value <= expected end private # @return [String] # # @see AbstractComparison#to_s # # @api private def comparator_string '<=' end end # class LessThanOrEqualToComparison end # module Conditions end # class Query end # module DataMapper