0
- module ActiveRecordHook # :nodoc:
0
- def self.included base
0
- def find_with_squirrel *args, &blk
0
- if blk || (args.last.is_a?(Hash) && args.last.has_key?(:paginate))
0
- query = Query.new(self, &blk)
0
- find_without_squirrel(*args)
0
- alias_method :find_without_squirrel, :find
0
- alias_method :find, :find_with_squirrel
0
- attr_reader :conditions, :model
0
- def initialize model, &blk
0
- @conditions = ConditionBlock.new(@model, "AND", blk ? blk.binding : nil, :base, &blk)
0
- hand_out_join_associations
0
- @joins ||= hashized_join_associations
0
- @join_dependency ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(model, self.include, nil)
0
- if args.first == :query
0
- opts = args.last.is_a?(Hash) ? args.last : {}
0
- pagination = opts.delete(:paginate) || {}
0
- model.send(:with_scope, :find => opts) do
0
- @conditions.paginate(pagination) unless pagination.empty?
0
- results = model.find args[0], @conditions.to_params
0
- if @conditions.paginate?
0
- count_conditions = @conditions.to_params
0
- limit = count_conditions.delete(:limit)
0
- offset = count_conditions.delete(:offset)
0
- attr_reader :total_results
0
- total_results = model.count(count_conditions)
0
- results.instance_variable_set("@pages", Paginator.new( :count => total_results, :limit => limit, :offset => offset) )
0
- results.instance_variable_set("@total_results", total_results)
0
+# Squirrel is a library for making querying the database using ActiveRecord cleaner, easier
0
+# to read, and less prone to user error. It does this by allowing AR::Base#find to take a block,
0
+# which is run to build the conditions and includes required to execute the query.
0
+ # When included in AR::Base, it chains the #find method to allow for block execution.
0
+ def self.included base
0
+ def find_with_squirrel *args, &blk
0
+ if blk || (args.last.is_a?(Hash) && args.last.has_key?(:paginate))
0
+ query = Query.new(self, &blk)
0
+ find_without_squirrel(*args)
0
+ alias_method :find_without_squirrel, :find
0
+ alias_method :find, :find_with_squirrel
0
+ # The Query is what contains the query and is what handles execution and pagination of the
0
+ attr_reader :conditions, :model
0
+ # Creates a Query specific to the given model (which is a class that descends from AR::Base)
0
+ # and a block that will be run to find the conditions for the #find call.
0
+ def initialize model, &blk
0
+ @binding = blk && blk.binding
0
+ @conditions = ConditionGroup.new(@model, "AND", @binding, &blk)
0
+ @conditions.assign_joins( join_dependency )
0
+ # Builds the dependencies needed to find what AR plans to call the tables in the query
0
+ # by finding and sending what would be passed in as the +include+ parameter to #find.
0
+ # This is a necessary step because what AR calls tables deeply nested might not be
0
+ # completely obvious.)
0
+ jd = ::ActiveRecord::Associations::ClassMethods::JoinDependency
0
+ @join_dependency ||= jd.new model,
0
+ @conditions.to_find_include,
0
+ # Runs the block which builds the conditions. If requested, paginates the result_set.
0
+ # If the first parameter to #find is :query (instead of :all or :first), then the query
0
+ # object itself is returned. Useful for debugging, but not much else.
0
+ if args.first == :query
0
+ opts = args.last.is_a?(Hash) ? args.last : {}
0
+ pagination = opts.delete(:paginate) || {}
0
+ model.send(:with_scope, :find => opts) do
0
+ @conditions.paginate(pagination) unless pagination.empty?
0
+ find_parameters = { :conditions => to_find_conditions,
0
+ :include => to_find_include,
0
+ :order => to_find_order,
0
+ :limit => to_find_limit,
0
+ :offset => to_find_offset }
0
+ results = model.find args[0], find_parameters
0
+ if @conditions.paginate?
0
+ paginate_result_set results, find_parameters
0
+ # Delegates the to_find_conditions call to the root ConditionGroup
0
+ def to_find_conditions
0
+ @conditions.to_find_conditions
0
+ # Delegates the to_find_include call to the root ConditionGroup
0
+ @conditions.to_find_include
0
+ # Delegates the to_find_order call to the root ConditionGroup
0
+ @conditions.to_find_order
0
+ # Delegates the to_find_limit call to the root ConditionGroup
0
+ @conditions.to_find_limit
0
+ # Delegates the to_find_offset call to the root ConditionGroup
0
+ @conditions.to_find_offset
0
+ # Used by #execute to paginates the result set if
0
+ # pagination was requested. In this case, it adds +pages+ and +total_results+ accessors
0
+ # to the result set. See Paginator for more details.
0
+ def paginate_result_set set, conditions
0
+ limit = conditions.delete(:limit)
0
+ offset = conditions.delete(:offset)
0
+ attr_reader :total_results
0
+ total_results = model.count(conditions)
0
+ set.instance_variable_set("@pages",
0
+ Paginator.new( :count => total_results,
0
+ set.instance_variable_set("@total_results", total_results)
0
+ # ConditionGroups are groups of Conditions, oddly enough. They most closely map to models
0
+ # in your schema, but they also handle the grouping jobs for the #any and #all blocks.
0
+ attr_accessor :model, :logical_join, :binding, :reflection, :path
0
+ # Creates a ConditionGroup by passing in the following arguments:
0
+ # * model: The AR subclass that defines what columns and associations will be accessible
0
+ # * logical_join: A string containing the join that will be used when concatenating the
0
+ # conditions together. The root level ConditionGroup created by Query defaults the
0
+ # join to be "AND", but the #any and #all methods will create specific ConditionGroups
0
+ # using "OR" and "AND" as their join, respectively.
0
+ # * binding: The +binding+ of the block passed to the original #find. Will be used to
0
+ # +eval+ what +self+ would be. This is necessary for using methods like +params+ and
0
+ # +session+ in your controllers.
0
+ # * path: The "path" taken through the models to arrive at this model. For example, if
0
+ # your User class has_many Posts which has_many Comments each of which belongs_to User,
0
+ # the path to the second User would be [:posts, :comments, :user]
0
+ # * reflection: The association used to get to this block. If nil, then no new association
0
+ # was traversed, which means we're in an #any or #all grouping block.
0
+ # * blk: The block to be executed.
0
+ # This method defines a number of methods to be available inside the block, one for each
0
+ # of the columns and associations in the specified model. Note that you CANNOT use
0
+ # user-defined methods on your model inside Squirrel queries. They don't have any meaning
0
+ # in the context of a database query.
0
+ def initialize model, logical_join, binding, path = nil, reflection = nil, &blk
0
+ @logical_join = logical_join
0
+ @condition_blocks = []
0
+ @reflection = reflection
0
+ @path = [ path, reflection ].compact.flatten
0
+ existing_methods = self.class.instance_methods(false)
0
+ (model.column_names - existing_methods).each do |col|
0
+ (class << self; self; end).class_eval do
0
+ define_method(col.to_s.intern) do
0
- def hand_out_join_associations
0
- @conditions.assign_join_associations(joins)
0
- def join_parent_names assn
0
- if assn.respond_to? :parent
0
- [join_parent_names(assn.parent), assn.reflection.name].flatten
0
- def hashized_join_associations
0
- join_dependency.joins.reverse.each do |join|
0
- join_parent_names(join).inject(joins){|hash, name| hash[name] ||= {} }[:_join] = join
0
- attr_accessor :model, :join, :binding, :reflections, :reflection
0
- def initialize model, join, binding, reflection = nil, &blk
0
- @condition_blocks = []
0
- @reflection = reflection
0
- existing_methods = self.class.instance_methods(false)
0
- (model.column_names - existing_methods).each do |col|
0
- (class << self; self; end).class_eval do
0
- define_method(col.to_s.intern) do
0
- (model.reflections.keys - existing_methods).each do |assn|
0
- (class << self; self; end).class_eval do
0
- define_method(assn.to_s.intern) do
0
- @conditions << Condition.new(name)
0
- def association name, &blk
0
- name = name.to_s.intern
0
- ref = @model.reflect_on_association(name)
0
- @condition_blocks << ConditionBlock.new(ref.klass, join, binding, ref.name, &blk)
0
- @condition_blocks.last
0
- @condition_blocks << ConditionBlock.new(model, "OR", binding, &blk)
0
- @condition_blocks.last
0
- @condition_blocks << ConditionBlock.new(model, "AND", binding, &blk)
0
- @condition_blocks.last
0
- @order += [columns].flatten
0
- @order.blank? ? nil : @order.collect{|col| col.respond_to?(:full_name) ? (col.full_name + (col.negative? ? " DESC" : "")) : col }.join(", ")
0
- def paginate opts = {}
0
- page = (opts[:page] || 1).to_i
0
- per_page = (opts[:per_page] || 20).to_i
0
- limit( per_page, ( page - 1 ) * per_page )
0
- def limit lim, off = nil
0
- find_options.merge! :limit => lim, :offset => (off || find_options[:offset])
0
- @paginator || @condition_blocks.any?(&:paginate?)
0
- @negative = !@negative
0
- ps.merge! :conditions => to_sql, :include => self.include
0
- ps.merge! find_options.merge(:order => order_clause)
0
- segments = conditions.collect{|c| c.to_sql }.compact
0
- return nil if segments.length == 0
0
- cond = "(" + segments.collect{|s| s.first }.join(" #{join} ") + ")"
0
- cond = "NOT #{cond}" if negative?
0
- values = segments.inject([]){|all, now| all + now[1..-1] }
0
- @conditions + @condition_blocks
0
- def assign_join_associations(joins)
0
- @join_associations = reflection.nil? ? joins : joins[reflection]
0
- conditions.each do |cond|
0
- cond.assign_join_associations(@join_associations)
0
- @condition_blocks.inject({}) do |inc, cb|
0
- inc[cb.reflection] ||= {}
0
- inc[cb.reflection] = inc[cb.reflection].merge(cb.include)
0
- @condition_blocks.any?{|each| each.reflection != self.reflection }
0
- def method_missing meth, *args
0
- m = eval("method(:#{meth})", binding)
0
- raise NameError, "Column or Relationship #{meth} not defined for #{@model.class.name}"
0
- attr_reader :name, :operator, :operand
0
- [ :==, :===, :=~, :<=>, :<=, :<, :>, :>= ].each do |op|
0
- define_method(op) do |val|
0
+ (model.reflections.keys - existing_methods).each do |assn|
0
+ (class << self; self; end).class_eval do
0
+ define_method(assn.to_s.intern) do
0
+ # Creates a Condition and queues it for inclusion. When calling a method defined
0
+ # during the creation of the ConditionGroup object is the same as calling column(:column_name).
0
+ # This is useful if you need to access a column that happens to coincide with the name of
0
+ # an already-defined method (e.g. anything returned by instance_methods(false) for the
0
+ @conditions << Condition.new(name)
0
+ # Similar to #column, this will create an association even if you can't use the normal
0
+ def association name, &blk
0
+ name = name.to_s.intern
0
+ ref = @model.reflect_on_association(name)
0
+ @condition_blocks << ConditionGroup.new(ref.klass, logical_join, binding, path, ref.name, &blk)
0
+ @condition_blocks.last
0
+ # Creates a ConditionGroup that has the logical_join set to "OR".
0
+ @condition_blocks << ConditionGroup.new(model, "OR", binding, path, &blk)
0
+ @condition_blocks.last
0
+ # Creates a ConditionGroup that has the logical_join set to "AND".
0
+ @condition_blocks << ConditionGroup.new(model, "AND", binding, path, &blk)
0
+ @condition_blocks.last
0
+ # Sets the arguments for the :order parameter. Arguments can be columns (i.e. Conditions)
0
+ # or they can be strings (for "RANDOM()", etc.). If a Condition is used, and the column is
0
+ # negated using #not or #desc, then the resulting specification in the ORDER clause will
0
+ # be ordered descending. That is, "order_by name.desc" will become "ORDER name DESC"
0
+ @order += [columns].flatten
0
+ # Flags the result set to be paginated according to the :page and :per_page parameters
0
+ def paginate opts = {}
0
+ page = (opts[:page] || 1).to_i
0
+ per_page = (opts[:per_page] || 20).to_i
0
+ limit( per_page, ( page - 1 ) * per_page )
0
+ # Similar to #paginate, but does not flag the result set for pagination. Takes a limit
0
+ # and an offset (by default the offset is 0).