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?, #hash_context_shifted?, #initialize, #quote, #quoted?, #visit, #visit_Array, #visit_passthrough

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



341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/squeel/visitors/predicate_visitor.rb', line 341

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)



358
359
360
361
362
363
364
365
366
367
# File 'lib/squeel/visitors/predicate_visitor.rb', line 358

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)



369
370
371
372
373
374
375
376
377
378
# File 'lib/squeel/visitors/predicate_visitor.rb', line 369

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_hash_context_shift?(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



255
256
257
258
259
260
261
262
263
264
# File 'lib/squeel/visitors/predicate_visitor.rb', line 255

def implies_hash_context_shift?(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



388
389
390
391
392
393
394
395
396
397
# File 'lib/squeel/visitors/predicate_visitor.rb', line 388

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



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

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



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

def visit_ActiveRecord_Relation(o, parent)
  o.arel
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_hash_context_shift?(v)
      visit_with_hash_context_shift(k, v, parent)
    else
      visit_without_hash_context_shift(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_As(o, parent) ⇒ Arel::Nodes::As (private)

Visit a Squeel As node, resulting in am ARel As node.

Parameters:

  • The (Nodes::As)

    As node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::As)

    The resulting as node.



249
250
251
# File 'lib/squeel/visitors/predicate_visitor.rb', line 249

def visit_Squeel_Nodes_As(o, parent)
  visit(o.left, parent).as(o.right)
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
# 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, Nodes::Grouping
      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_s]
    when Symbol, Nodes::Stub
      Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_s])
    else
      quote arg
    end
  end

  Arel::Nodes::NamedFunction.new(o.name, args)
end

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

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

Parameters:

  • o (Nodes::Grouping)

    The Grouping node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Grouping)

    An ARel Grouping node, with expression visited



240
241
242
# File 'lib/squeel/visitors/predicate_visitor.rb', line 240

def visit_Squeel_Nodes_Grouping(o, parent)
  Arel::Nodes::Grouping.new(visit(o.expr, parent))
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



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

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) ⇒ Arel::Nodes::Not (private)

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

Parameters:

  • o (Nodes::Not)

    The Not node to visit

  • parent

    The parent object in the context

Returns:

  • (Arel::Nodes::Not)

    An ARel Not node, with expression visited



231
232
233
# File 'lib/squeel/visitors/predicate_visitor.rb', line 231

def visit_Squeel_Nodes_Not(o, parent)
  Arel::Nodes::Not.new(visit(o.expr, parent))
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.



175
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 175

def visit_Squeel_Nodes_Operation(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function, Nodes::As, Nodes::Literal, Nodes::Grouping
      visit(arg, parent)
    when Nodes::KeyPath
      can_visit?(arg.endpoint) ? visit(arg, parent) : contextualize(traverse(arg, parent))[arg.endpoint.to_s]
    when Symbol, Nodes::Stub
      Arel.sql(arel_visitor.accept contextualize(parent)[arg.to_s])
    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

  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_s]
  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, Nodes::Grouping
    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.to_s]
end

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

Visit a symbol. This will return an attribute named after the symbol against the current parent’s contextualized table.

Parameters:

  • o (Symbol)

    The symbol to visit

  • parent

    The symbol’s parent within the context

Returns:

  • (Arel::Attribute)

    An attribute on the contextualized parent table



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

def visit_Symbol(o, parent)
  contextualize(parent)[o]
end

#visit_with_hash_context_shift(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



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/squeel/visitors/predicate_visitor.rb', line 273

def visit_with_hash_context_shift(k, v, parent)
  @hash_context_depth += 1

  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, <<-END
    Hashes, Predicates, and arrays of visitables as values imply that their
    corresponding keys are a parent. This didn't work out so well in the case
    of key = #{k} and value = #{v}"
    END
  end
ensure
  @hash_context_depth -= 1
end

#visit_without_hash_context_shift(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



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
# File 'lib/squeel/visitors/predicate_visitor.rb', line 307

def visit_without_hash_context_shift(k, v, parent)
  case v
  when Nodes::Stub, Symbol
    v = contextualize(parent)[v.to_s]
  when Nodes::KeyPath # If we could visit the endpoint, we wouldn't be here
    v = contextualize(traverse(v, parent))[v.endpoint.to_s]
  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 !hash_context_shifted? && attr_name.include?('.')
        table_name, attr_name = attr_name.split(/\./, 2)
        Arel::Table.new(table_name.to_s, :engine => engine)[attr_name.to_s]
      else
        contextualize(parent)[attr_name]
      end
    arel_predicate_for(attribute, v, parent)
  end
end