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

filter[R]

Public Class Methods

new( filter = Philtre::Filter.new ) click to toggle source

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

[]( dataset, apply_unknown: true )
Alias for: transform
places() click to toggle source

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
transform( dataset, apply_unknown: true ) click to toggle source

pass in a dataset containing PlaceHolder expressions. you'll get back a modified dataset with the filter values filled in.

Calls superclass method
# 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
Also aliased as: []
unknown() click to toggle source

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

context_places() click to toggle source
# File lib/philtre/grinder.rb, line 93
def context_places
  @places ||= {}
  @places[subset] ||= {}
end
extra_keys( keys ) click to toggle source

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
push_subset( latest_subset ) { || ... } click to toggle source
# 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
subset() click to toggle source

TODO rename subset to clause

# File lib/philtre/grinder.rb, line 99
def subset
  @subsets.last || :none
end
subset_stack() click to toggle source
# File lib/philtre/grinder.rb, line 103
def subset_stack
  @subsets ||= []
end
unknown_placeholders() click to toggle source
# 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
v( obj ) click to toggle source

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.

Calls superclass method
# 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