Class: Squeel::Visitors::PredicateVisitor

Inherits:
Visitor
  • Object
show all
Defined in:
lib/squeel/visitors/predicate_visitor.rb

Constant Summary collapse

TRUE_SQL =
Arel.sql('1=1').freeze
FALSE_SQL =
Arel.sql('1=0').freeze

Constants inherited from Visitor

Visitor::DISPATCH

Instance Attribute Summary

Attributes inherited from Visitor

#context

Instance Method Summary collapse

Methods inherited from Visitor

#accept, #can_visit?, can_visit?, #initialize, #quote, #quoted?, #visit

Constructor Details

This class inherits a constructor from Squeel::Visitors::Visitor

Instance Method Details

#arel_predicate_for(attribute, value, parent) ⇒ Arel::Nodes::Node (private)

Determine whether to use IN or equality testing for a predicate, based on its value class, then return the appropriate predicate.

Parameters:

  • attribute

    The ARel attribute (or function/operation) the predicate will be created for

  • value

    The value to be compared against

Returns:

  • (Arel::Nodes::Node)

    An ARel predicate node



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/squeel/visitors/predicate_visitor.rb', line 314

def arel_predicate_for(attribute, value, parent)
  if ActiveRecord::Relation === value && value.select_values.empty?
    value = visit(value.select(value.klass.arel_table[value.klass.primary_key]), parent)
  else
    value = can_visit?(value) ? visit(value, parent) : value
  end

  case value
  when Array
    attribute_in_array(attribute, value)
  when Range, Arel::SelectManager
    attribute.in(value)
  else
    attribute.eq(value)
  end
end

#attribute_in_array(attribute, array) ⇒ Object (private)



331
332
333
334
335
336
337
338
339
340
# File 'lib/squeel/visitors/predicate_visitor.rb', line 331

def attribute_in_array(attribute, array)
  if array.empty?
    FALSE_SQL
  elsif array.include? nil
    array = array.compact
    array.empty? ? attribute.eq(nil) : attribute.in(array).or(attribute.eq nil)
  else
    attribute.in array
  end
end

#attribute_not_in_array(attribute, array) ⇒ Object (private)



342
343
344
345
346
347
348
349
350
351
# File 'lib/squeel/visitors/predicate_visitor.rb', line 342

def attribute_not_in_array(attribute, array)
  if array.empty?
    TRUE_SQL
  elsif array.include? nil
    array = array.compact
    array.empty? ? attribute.not_eq(nil) : attribute.not_in(array).and(attribute.not_eq nil)
  else
    attribute.not_in array
  end
end

#implies_context_change?(v) ⇒ Boolean (private)

Returns Whether the given value implies a context change.

Parameters:

  • v

    The value to consider

Returns:

  • (Boolean)

    Whether the given value implies a context change



232
233
234
235
236
237
238
239
240
241
# File 'lib/squeel/visitors/predicate_visitor.rb', line 232

def implies_context_change?(v)
  case v
  when Hash, Nodes::Predicate, Nodes::Unary, Nodes::Binary, Nodes::Nary, Nodes::Sifter
    true
  when Nodes::KeyPath
    can_visit?(v.endpoint) && !(Nodes::Stub === v.endpoint)
  else
    false
  end
end

#quote_for_node(node, v) ⇒ Object (private)

Certain nodes require us to do the quoting before the ARel visitor gets a chance to try, because we want to avoid having our values quoted as a type of the last visited column. Otherwise, we can end up with annoyances like having “joe” quoted to 0, if the last visited column was of an integer type.

Parameters:

  • node

    The node we (might) be quoting for

  • v

    The value to (possibly) quote



361
362
363
364
365
366
367
368
369
370
# File 'lib/squeel/visitors/predicate_visitor.rb', line 361

def quote_for_node(node, v)
  case node
  when Nodes::Function, Nodes::Literal
    quote(v)
  when Nodes::Predicate
    quote_for_node(node.expr, v)
  else
    v
  end
end

#visit_ActiveRecord_Base(o, parent) ⇒ Fixnum (private)

Visit ActiveRecord::Base objects. These should be converted to their id before being used in a comparison.

Parameters:

  • o (ActiveRecord::Base)

    The AR::Base object to visit

  • parent

    The current parent object in the context

Returns:

  • (Fixnum)

    The id of the object



52
53
54
# File 'lib/squeel/visitors/predicate_visitor.rb', line 52

def visit_ActiveRecord_Base(o, parent)
  o.id
end

#visit_ActiveRecord_Relation(o, parent) ⇒ Arel::SelectManager (private)

Visit an ActiveRecord Relation, returning an Arel::SelectManager

Parameters:

  • o (ActiveRecord::Relation)

    The Relation to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::SelectManager)

    The ARel select manager that represents the relation’s query



165
166
167
# File 'lib/squeel/visitors/predicate_visitor.rb', line 165

def visit_ActiveRecord_Relation(o, parent)
  o.arel
end

#visit_Array(o, parent) ⇒ Array (private)

Visit an array, which involves accepting any values we know how to accept, and skipping the rest.

Parameters:

  • o (Array)

    The Array to visit

  • parent

    The current parent object in the context

Returns:

  • (Array)

    The visited array



42
43
44
# File 'lib/squeel/visitors/predicate_visitor.rb', line 42

def visit_Array(o, parent)
  o.map { |v| can_visit?(v) ? visit(v, parent) : v }.flatten
end

#visit_Hash(o, parent) ⇒ Array (private)

Visit a Hash. This entails iterating through each key and value and visiting each value in turn.

Parameters:

  • o (Hash)

    The Hash to visit

  • parent

    The current parent object in the context

Returns:

  • (Array)

    An array of values for use in a where or having clause



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/squeel/visitors/predicate_visitor.rb', line 18

def visit_Hash(o, parent)
  predicates = o.map do |k, v|
    if implies_context_change?(v)
      visit_with_context_change(k, v, parent)
    else
      visit_without_context_change(k, v, parent)
    end
  end

  predicates.flatten!

  if predicates.size > 1
    Arel::Nodes::Grouping.new(Arel::Nodes::And.new predicates)
  else
    predicates.first
  end
end

#visit_Squeel_Nodes_And(o, parent) ⇒ Arel::Nodes::Grouping (private)

Visit a Squeel And node, returning an ARel Grouping containing an ARel And node.

Parameters:

  • o (Nodes::And)

    The And node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Grouping)

    A grouping node, containnig an ARel And node as its expression. All children will be visited before being passed to the And.



213
214
215
# File 'lib/squeel/visitors/predicate_visitor.rb', line 213

def visit_Squeel_Nodes_And(o, parent)
  Arel::Nodes::Grouping.new(Arel::Nodes::And.new(visit(o.children, parent)))
end

#visit_Squeel_Nodes_Function(o, parent) ⇒ Arel::Nodes::NamedFunction (private)

Visit a Squeel function, returning an ARel NamedFunction node.

Parameters:

  • o (Nodes::Function)

    The function node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::NamedFunction)

    A named function node. Function arguments are visited, if necessary, before being passed to the NamedFunction.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/squeel/visitors/predicate_visitor.rb', line 140

def visit_Squeel_Nodes_Function(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function, Nodes::As, Nodes::Literal
      visit(arg, parent)
    when ActiveRecord::Relation
      arg.arel.ast
    when Nodes::KeyPath
      can_visit?(arg.endpoint) ? visit(arg, parent) : contextualize(traverse(arg, parent))[arg.endpoint.to_sym]
    when Symbol, Nodes::Stub
      Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
    else
      quote arg
    end
  end
  func = Arel::Nodes::NamedFunction.new(o.name, args)

  o.alias ? func.as(o.alias) : func
end

#visit_Squeel_Nodes_KeyPath(o, parent) ⇒ Object (private)

Visit a KeyPath by traversing the path and then visiting the endpoint.

Parameters:

  • o (Nodes::KeyPath)

    The KeyPath to visit

  • parent

    The parent object in the context

Returns:

  • The visited endpoint, in the context of the KeyPath’s path



61
62
63
64
65
# File 'lib/squeel/visitors/predicate_visitor.rb', line 61

def visit_Squeel_Nodes_KeyPath(o, parent)
  parent = traverse(o, parent)

  visit(o.endpoint, parent)
end

#visit_Squeel_Nodes_Literal(o, parent) ⇒ Arel::Nodes::SqlLiteral (private)

Visit a Literal by converting it to an ARel SqlLiteral

Parameters:

  • o (Nodes::Literal)

    The Literal to visit

  • parent

    The parent object in the context (unused)

Returns:

  • (Arel::Nodes::SqlLiteral)

    An SqlLiteral



82
83
84
# File 'lib/squeel/visitors/predicate_visitor.rb', line 82

def visit_Squeel_Nodes_Literal(o, parent)
  Arel.sql(o.expr)
end

#visit_Squeel_Nodes_Not(o, parent) ⇒ Object (private)



226
227
228
# File 'lib/squeel/visitors/predicate_visitor.rb', line 226

def visit_Squeel_Nodes_Not(o, parent)
  visit(o.expr, parent).not
end

#visit_Squeel_Nodes_Operation(o, parent) ⇒ Arel::Nodes::InfixOperation (private)

Visit a Squeel operation node, convering it to an ARel InfixOperation (or subclass, as appropriate)

Parameters:

  • o (Nodes::Operation)

    The Operation node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::InfixOperation)

    The InfixOperation (or Addition, Multiplication, etc) node, with both operands visited, if needed.



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'lib/squeel/visitors/predicate_visitor.rb', line 176

def visit_Squeel_Nodes_Operation(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function, Nodes::As, Nodes::Literal
      visit(arg, parent)
    when Nodes::KeyPath
      can_visit?(arg.endpoint) ? visit(arg, parent) : contextualize(traverse(arg, parent))[arg.endpoint.to_sym]
    when Symbol, Nodes::Stub
      Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_sym])
    else
      quote arg
    end
  end

  op = case o.operator
  when :+
    Arel::Nodes::Addition.new(args[0], args[1])
  when :-
    Arel::Nodes::Subtraction.new(args[0], args[1])
  when :*
    Arel::Nodes::Multiplication.new(args[0], args[1])
  when :/
    Arel::Nodes::Division.new(args[0], args[1])
  else
    Arel::Nodes::InfixOperation.new(o.operator, args[0], args[1])
  end
  o.alias ? op.as(o.alias) : op
end

#visit_Squeel_Nodes_Or(o, parent) ⇒ Arel::Nodes::Or (private)

Visit a Squeel Or node, returning an ARel Or node.

Parameters:

  • o (Nodes::Or)

    The Or node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Or)

    An ARel Or node, with left and right sides visited



222
223
224
# File 'lib/squeel/visitors/predicate_visitor.rb', line 222

def visit_Squeel_Nodes_Or(o, parent)
  Arel::Nodes::Grouping.new(Arel::Nodes::Or.new(visit(o.left, parent), (visit(o.right, parent))))
end

#visit_Squeel_Nodes_Predicate(o, parent) ⇒ Object (private)

Visit a Squeel predicate, converting it into an ARel predicate

Parameters:

  • o (Nodes::Predicate)

    The predicate to visit

  • parent

    The parent object in the context

Returns:

  • An ARel predicate node (Arel::Nodes::Equality, Arel::Nodes::Matches, etc)



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/squeel/visitors/predicate_visitor.rb', line 103

def visit_Squeel_Nodes_Predicate(o, parent)
  value = o.value

  case value
  when Nodes::KeyPath
    value = can_visit?(value.endpoint) ? visit(value, parent) : contextualize(traverse(value, parent))[value.endpoint.to_sym]
  when ActiveRecord::Relation
    value = visit(
      value.select_values.empty? ? value.select(value.klass.arel_table[value.klass.primary_key]) : value,
      parent
    )
  else
    value = visit(value, parent) if can_visit?(value)
  end

  value = quote_for_node(o.expr, value)

  attribute = case o.expr
  when Nodes::Stub, Nodes::Function, Nodes::Literal
    visit(o.expr, parent)
  else
    contextualize(parent)[o.expr]
  end

  if Array === value && [:in, :not_in].include?(o.method_name)
    o.method_name == :in ? attribute_in_array(attribute, value) : attribute_not_in_array(attribute, value)
  else
    attribute.send(o.method_name, value)
  end
end

#visit_Squeel_Nodes_Sifter(o, parent) ⇒ Object (private)

Visit a Squeel sifter by executing its corresponding constraint block in the parent’s class, with its given arguments, then visiting the result.

Parameters:

  • o (Nodes::Sifter)

    The Sifter to visit

  • parent

    The parent object in the context

Returns:

  • The result of visiting the executed block’s return value



92
93
94
95
# File 'lib/squeel/visitors/predicate_visitor.rb', line 92

def visit_Squeel_Nodes_Sifter(o, parent)
  klass = classify(parent)
  visit(klass.send(o.name, *o.args), parent)
end

#visit_Squeel_Nodes_Stub(o, parent) ⇒ Arel::Attribute (private)

Visit a Stub by converting it to an ARel attribute

Parameters:

  • o (Nodes::Stub)

    The Stub to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Attribute)

    An attribute of the parent table with the Stub’s column



73
74
75
# File 'lib/squeel/visitors/predicate_visitor.rb', line 73

def visit_Squeel_Nodes_Stub(o, parent)
  contextualize(parent)[o.symbol]
end

#visit_with_context_change(k, v, parent) ⇒ Object (private)

Change context (by setting the new parent to the result of a #find or #traverse on the key), then accept the given value.

Parameters:

  • k

    The hash key

  • v

    The hash value

  • parent

    The current parent object in the context

Returns:

  • The visited value



250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/squeel/visitors/predicate_visitor.rb', line 250

def visit_with_context_change(k, v, parent)
  parent = case k
    when Nodes::KeyPath
      traverse(k, parent, true)
    else
      find(k, parent)
    end

  case v
  when Hash, Nodes::KeyPath, Nodes::Predicate, Nodes::Unary, Nodes::Binary, Nodes::Nary, Nodes::Sifter
    visit(v, parent || k)
  when Array
    v.map {|val| visit(val, parent || k)}
  else
    raise ArgumentError, "    Hashes, Predicates, and arrays of visitables as values imply that their\n    corresponding keys are a parent. This didn't work out so well in the case\n    of key = \#{k} and value = \#{v}\"\n    END\n  end\nend\n"

#visit_without_context_change(k, v, parent) ⇒ Object (private)

Create a predicate for a given key/value pair. If the value is a Symbol, Stub, or KeyPath, it’s converted to a table.column for the predicate value.

Parameters:

  • k

    The hash key

  • v

    The hash value

  • parent

    The current parent object in the context

Returns:

  • An ARel predicate



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/squeel/visitors/predicate_visitor.rb', line 280

def visit_without_context_change(k, v, parent)
  case v
  when Nodes::Stub, Symbol
    v = contextualize(parent)[v.to_sym]
  when Nodes::KeyPath # If we could visit the endpoint, we wouldn't be here
    v = contextualize(traverse(v, parent))[v.endpoint.to_sym]
  end

  case k
  when Nodes::Predicate
    visit(k % quote_for_node(k.expr, v), parent)
  when Nodes::Function, Nodes::Literal
    arel_predicate_for(visit(k, parent), quote(v), parent)
  when Nodes::KeyPath
    visit(k % quote_for_node(k.endpoint, v), parent)
  else
    attr_name = k.to_s
    attribute = if attr_name.include?('.')
        table_name, attr_name = attr_name.split(/\./, 2)
        Arel::Table.new(table_name.to_sym, :engine => engine)[attr_name.to_sym]
      else
        contextualize(parent)[k.to_sym]
      end
    arel_predicate_for(attribute, v, parent)
  end
end