Class: Squeel::Visitors::PredicateVisitor

Inherits:
Base
  • 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 Base

Base::DISPATCH

Instance Attribute Summary

Attributes inherited from Base

#context

Instance Method Summary collapse

Methods inherited from Base

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

Constructor Details

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

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



285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/squeel/visitors/predicate_visitor.rb', line 285

def arel_predicate_for(attribute, value, parent)
  value = can_accept?(value) ? accept(value, parent) : value
  if [Array, Range, Arel::SelectManager].include?(value.class)
    if Array === value && value.empty?
      FALSE_SQL
    else
      attribute.in(value)
    end
  else
    attribute.eq(value)
  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



203
204
205
206
207
208
209
210
211
212
# File 'lib/squeel/visitors/predicate_visitor.rb', line 203

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

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

Function 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



306
307
308
309
310
311
312
313
314
315
# File 'lib/squeel/visitors/predicate_visitor.rb', line 306

def quote_for_node(node, v)
  case node
  when Nodes::Function
    quote(v)
  when Nodes::Predicate
    Nodes::Function === node.expr ? quote(v) : 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



136
137
138
# File 'lib/squeel/visitors/predicate_visitor.rb', line 136

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_accept?(v) ? accept(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.



184
185
186
# File 'lib/squeel/visitors/predicate_visitor.rb', line 184

def visit_Squeel_Nodes_And(o, parent)
  Arel::Nodes::Grouping.new(Arel::Nodes::And.new(accept(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.



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/squeel/visitors/predicate_visitor.rb', line 113

def visit_Squeel_Nodes_Function(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function
      accept(arg, parent)
    when ActiveRecord::Relation
      arg.arel.ast
    when Nodes::KeyPath
      can_accept?(arg.endpoint) ? accept(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
  Arel::Nodes::NamedFunction.new(o.name, args, o.alias)
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)

  accept(o.endpoint, parent)
end

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



197
198
199
# File 'lib/squeel/visitors/predicate_visitor.rb', line 197

def visit_Squeel_Nodes_Not(o, parent)
  accept(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.



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/squeel/visitors/predicate_visitor.rb', line 147

def visit_Squeel_Nodes_Operation(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function
      accept(arg, parent)
    when Nodes::KeyPath
      can_accept?(arg.endpoint) ? accept(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(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



193
194
195
# File 'lib/squeel/visitors/predicate_visitor.rb', line 193

def visit_Squeel_Nodes_Or(o, parent)
  accept(o.left, parent).or(accept(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)



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/squeel/visitors/predicate_visitor.rb', line 83

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

  if Array === value && value.empty? && [:in, :not_in].include?(o.method_name)
    # Special case, in/not_in with empty arrays should be false/true respectively
    return o.method_name == :in ? FALSE_SQL : TRUE_SQL
  end

  if Nodes::KeyPath === value
    value = can_accept?(value.endpoint) ? accept(value, parent) : contextualize(traverse(value, parent))[value.endpoint.to_sym]
  else
    value = accept(value, parent) if can_accept?(value)
  end

  case o.expr
  when Nodes::Stub
    accept(o.expr, parent).send(o.method_name, value)
  when Nodes::Function
    accept(o.expr, parent).send(o.method_name, quote(value))
  else
    contextualize(parent)[o.expr].send(o.method_name, value)
  end
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



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/squeel/visitors/predicate_visitor.rb', line 221

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
    accept(v, parent || k)
  when Array
    v.map {|val| accept(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
end

#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



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

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 accept the endpoint, we wouldn't be here
    v = contextualize(traverse(v, parent))[v.endpoint.to_sym]
  end

  case k
  when Nodes::Predicate
    accept(k % quote_for_node(k.expr, v), parent)
  when Nodes::Function
    arel_predicate_for(accept(k, parent), quote(v), parent)
  when Nodes::KeyPath
    accept(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