class Philtre::Grinder
Using the expressions in the filter, transform a dataset with placeholders into a real dataset with expressions, for example:
ds = Personage.filter( :brief.lieu, :title.lieu ).order( :age.lieu ) g = Grinder.new( Philtre.new(title: 'Grand High Poobah', :order => :age.desc ) ) nds = g.transform( ds ) nds.sql => SELECT * FROM "personages" WHERE (("title" = 'Grand High Poobah'))
In a sense, this is a means to defining SQL functions with optional keyword arguments.
Attributes
Public Class Methods
filter must respond to expr_for( key, sql_field = nil ), expr_hash and order_hash
# File lib/philtre/grinder.rb, line 25 def initialize( filter = Philtre::Filter.new ) @filter = filter end
Public Instance Methods
Grouped hash of place holders in the original dataset from the last transform. Only has values after transform has been called.
# File lib/philtre/grinder.rb, line 67 def places @places || raise("Call transform to find place holders.") end
pass in a dataset containing PlaceHolder expressions. you'll get back a modified dataset with the filter values filled in.
# File lib/philtre/grinder.rb, line 34 def transform( dataset, apply_unknown: true ) @unknown = [] @places = {} @subsets = [] # handy for debugging @original_dataset = dataset # the transformed dataset with placeholders that # exist in filter replaced. unknown might have values # after this. t_dataset = super(dataset) unknown_placeholders if unknown.any? if apply_unknown # now filter by whatever predicates are left over # ie those not in the incoming dataset. Leftover # order parameters will overwrite existing ones # that are not protected by an outer select. filter.subset( *unknown ).apply t_dataset else raise "unknown values #{unknown.inspect} for\n#{dataset.sql}" end else t_dataset end end
collection of values in the filter that were not found as placeholders in the original dataset.
# File lib/philtre/grinder.rb, line 73 def unknown @unknown || raise("Call transform to find placeholders not provided by the filter.") end
Protected Instance Methods
# File lib/philtre/grinder.rb, line 93 def context_places @places ||= {} @places[subset] ||= {} end
Set of all keys that are not in the placeholders
# File lib/philtre/grinder.rb, line 80 def extra_keys( keys ) incoming = keys existing = places.flat_map{|subset, placeholders| placeholders.keys} # find the elements in incoming that are not in existing incoming - existing & incoming end
# File lib/philtre/grinder.rb, line 107 def push_subset( latest_subset, &block ) subset_stack.push latest_subset rv = yield subset_stack.pop rv end
TODO rename subset to clause
# File lib/philtre/grinder.rb, line 99 def subset @subsets.last || :none end
# File lib/philtre/grinder.rb, line 103 def subset_stack @subsets ||= [] end
# File lib/philtre/grinder.rb, line 88 def unknown_placeholders unknown.concat extra_keys( filter.expr_hash.keys ) unknown.concat extra_keys( filter.order_hash.keys ) end
Override the ASTTransformer method, which is where the work is done to transform the dataset containing placeholders into a dataset containing a proper SQL statement. Yes, this is in fact every OO purist's worst nightware - a Giant Switch Statement.
# File lib/philtre/grinder.rb, line 118 def v( obj ) case obj when Sequel::Dataset # transform empty expressions to false (or nil, but false is more debuggable) # can't use nil for all kinds of expressions because nil mean NULL for # most of the Sequel::SQL expressions. obj.clone Hash[ v(obj.opts).map{|k,val| [k, val.is_a?(Philtre::EmptyExpression) ? false : val]} ] # for Sequel::Models when -> obj { obj.is_a?(Class) && obj.ancestors.include?(Sequel::Model) } # From sequel-5.x.x, I suspect, # SomeModel.dataset.opts includes :row_proc => SomeModel and :model => SomeModel # which sends v into an endless loop. opts = obj.dataset.opts.reject{|_,v| v == obj} transformed = v(opts).map do |k,val| [k, val.is_a?(Philtre::EmptyExpression) ? false : val] end obj.dataset.clone Hash[transformed] # for other things that are convertible to dataset when ->(obj){obj.respond_to? :to_dataset} v obj.to_dataset # Keep the context for place holders, # so we know what kind of expression to insert later. # Each of :where, :order, :having, :select etc will come as a hash. # There are some other top-level options too. when Hash rv = {} obj.each do |key, val| push_subset key do rv[v(key)] = v(val) end end rv when Philtre::PlaceHolder # get the expression for the placeholder. # use the placeholder's field name if given expr = case subset # substitute a comparison or some other predicate when :where, :having filter.expr_for obj.name, obj.sql_field # substitute an order by expression when :order filter.order_for obj.name # Substitute the field name only if it has a value. # nil when the name doesn't have a value. This way, # select :some_field.lieu # is left out when some_field does not have a value. when :select if filter[obj.name] obj.sql_field || obj.name end else raise "don't understand subset #{subset}" end # keep it, just in case context_places[obj.name] = expr # transform expr || Philtre::EmptyExpression.new when Array # sometimes things are already an empty array, in which # case just leave them alone. return super if obj.empty? # collect expressions, some may be empty. exprs = super.reject{|e| e.is_a? Philtre::EmptyExpression} # an empty array of expressions must be translated # to an empty expression at this point. exprs.empty? ? Philtre::EmptyExpression.new : exprs when Sequel::SQL::ComplexExpression # use the Array case above, otherwise copy the expression itself v( obj.args ).empty? ? Philtre::EmptyExpression.new : super else super end end