module Criteria # base class for classes that can contain subcriteria class CriterionContainer def initialize @restrictions = [] end # add a child restriction. For most criteria, these are combined via AND (Conjunction). If you need to construct an OR query, use #Criterion.disjunction() def add(restriction) restriction.model_class = self.model_class restriction.query = self.query restriction.parent = self @restrictions << restriction self end # alias for add(restriction) alias :<< :add # dynamically builds aliases for adding criteria. # If a program calls some_name_, send(:operation, 'some_name', args) will be executed def method_missing name, *args case name.to_s when /(.*)_is_null/ return send(:is_null, $1) when /(.*)_is_not_null/ return send(:is_not_null, $1) when /(.*)_not_(.*)/ send(:not).send($2, $1, *args) return self when /(.*)_(.*)/ return send($2, $1, *args) else super end end # constructs the AR query condition for this criterion's subtree def condition con = @restrictions.collect { |r| r.condition }.compact.join(" AND ") '(' << con << ')' unless con.empty? end # constructs the parameter array for the AR find() method def params par = [] @restrictions.each do |r| par.concat r.params.compact end par end # constructs the array of tables for the :include parameter to :find def joins includes = {} @restrictions.each do |r| includes= includes.merge r.joins end includes end # helper method to construct a "attribute IS NULL" condition def is_null(attribute_name) self << IsNull.new(attribute_name) end # helper method to construct a "attribute IS NOT NULL" condition def is_not_null(attribute_name) self << IsNotNull.new(attribute_name) end # helper method to construct a disjunction (OR) condition. # Returns the criterion representing the disjunction. Criteria added to this will be included in the disjunction. # Person.query.disjunction().first_name_eq('abc').last_name_eq('abc') produces the following condition: # (first_name='abc' OR last_name='abc') def disjunction dis = Disjunction.new self << dis return dis unless block_given? yield dis end alias :or :disjunction # helper method to construct a conjunction (AND) condition. # Returns the criterion representing the conjunction. Criteria added to this will be included in the conjunction. # All criteria are conjunctions by default, so the only case where a conjunction is explicitly needed is when you want to nested AND clauses within a OR colause (disjunction): # pq = Person.disjunction # pq.conjunction.date_lt('some date').date_gt('some other date') # pq.active_eq(1) def conjunction dis = Disjunction.new self << dis return dis unless block_given? yield dis end alias :and :conjunction # helper method for "in" condition # _values_ should be an array. # Person.query.id_in([1,2,3,4]) produces: # id IN (1,2,3,4) def in(attribute_name, values) self << In.new(attribute_name, values) end # helper method to add a joined table. Pass in the name of the _attribute_ that contains the relationship. # Returns the criterion representing the joined table. Criteria added to this will operate on the joined table. # Person.query.join('address').city_eq('Tokyo') produces the following condition: # (addresses.city='Tokyo') def join( attribute ) jn = Join.new(attribute) self << jn return jn unless block_given? yield jn end # helper method for negation # Like join and disjunction, this returns the new subcriterion. Criteria added to the subcriterion will all be included in the NOT clause. # Person.query.not.name_eq('abc').id_gt(9) produces the following condition: # NOT( name = 'abc' and id > 9 ) def not nt = Not.new self << nt return nt unless block_given? yield nt end end # Superclass for all criteria # implements base functionality to build a query tree and extract AR-compliant ocnditions and include parameters class Criterion < CriterionContainer # the parent criterion attr_accessor :parent attr_accessor :query attr_accessor :model_class protected # recursively build the table prefixes for joined attributes def prefix parent.prefix if parent end end end