Class: Squeel::Visitors::Visitor

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

Overview

The Base visitor class, containing the default behavior common to subclasses.

Constant Summary collapse

DISPATCH =

A hash that caches the method name to use for a visitor for a given class

Hash.new do |hash, klass|
  hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(context = nil) ⇒ Visitor

Create a new Visitor that uses the supplied context object to contextualize visited nodes.

Parameters:

  • context (Context) (defaults to: nil)

    The context to use for node visitation.


15
16
17
18
# File 'lib/squeel/visitors/visitor.rb', line 15

def initialize(context = nil)
  @context = context
  @hash_context_depth = 0
end

Instance Attribute Details

#contextObject

Returns the value of attribute context


8
9
10
# File 'lib/squeel/visitors/visitor.rb', line 8

def context
  @context
end

Class Method Details

.can_visit?(object) ⇒ Boolean

Returns Whether or not visitors of this class can visit the given object

Parameters:

  • object

    The object to check

Returns:

  • (Boolean)

    Whether or not visitors of this class can visit the given object


42
43
44
45
46
47
48
49
# File 'lib/squeel/visitors/visitor.rb', line 42

def self.can_visit?(object)
  @can_visit ||= Hash.new do |hash, klass|
    hash[klass] = klass.ancestors.detect { |ancestor|
      private_method_defined? DISPATCH[ancestor]
    } ? true : false
  end
  @can_visit[object.class]
end

Instance Method Details

#accept(object, parent = context.base) ⇒ Object

Accept an object.

Parameters:

  • object

    The object to visit

  • parent (defaults to: context.base)

    The parent of this object, to track the object's place in any association hierarchy.

Returns:

  • The results of the node visitation, typically an Arel object of some kind.


26
27
28
# File 'lib/squeel/visitors/visitor.rb', line 26

def accept(object, parent = context.base)
  visit(object, parent)
end

#accept!(object, parent = context.base) ⇒ Object


30
31
32
# File 'lib/squeel/visitors/visitor.rb', line 30

def accept!(object, parent = context.base)
  visit!(object, parent)
end

#can_visit?(object) ⇒ Boolean

Returns Whether or not the visitor can visit the given object

Parameters:

  • object

    The object to check

Returns:

  • (Boolean)

    Whether or not the visitor can visit the given object


36
37
38
# File 'lib/squeel/visitors/visitor.rb', line 36

def can_visit?(object)
  self.class.can_visit? object
end

#hash_context_shifted?Boolean (private)

If we're visiting stuff in a hash, it's good to check whether or not we've shifted context already. If we have, we may want to use caution as it pertains to certain input, in case it's untrusted. See CVE-2012-2661 for info.

Returns:

  • (Boolean)

    Whether we're within a new context.


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

def hash_context_shifted?
  @hash_context_depth > 0
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


79
80
81
# File 'lib/squeel/visitors/visitor.rb', line 79

def implies_hash_context_shift?(v)
  can_visit?(v)
end

#quote(value) ⇒ Arel::Nodes::SqlLiteral (private)

Quote a value based on its type, not on the last column used by the Arel visitor. This is occasionally necessary to avoid having Arel quote a value according to an integer column, converting 'My String' to 0.

Parameters:

  • value

    The value to quote

Returns:

  • (Arel::Nodes::SqlLiteral)

    if the value needs to be pre-quoted

  • the unquoted value, if default quoting won't hurt.


158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/squeel/visitors/visitor.rb', line 158

def quote(value)
  if quoted? value
    case value
    when Array
      value.map {|v| quote(v)}
    when Range
      Range.new(quote(value.begin), quote(value.end), value.exclude_end?)
    else
      if defined?(Arel::Collectors::SQLString)
        Arel.sql(arel_visitor.compile(Arel::Nodes.build_quoted(value)))
      else
        Arel.sql(arel_visitor.accept value)
      end
    end
  else
    value
  end
end

#quoted?(object) ⇒ Boolean (private)

Important to avoid accidentally allowing the default Arel visitor's last_column quoting behavior (where a value is quoted as though it is of the type of the last visited column). This can wreak havoc with Functions and Operations.

Parameters:

  • object

    The object to check

Returns:

  • (Boolean)

    Whether or not the Arel visitor will try to quote the object if not passed as an SqlLiteral.


139
140
141
142
143
144
145
146
147
148
149
# File 'lib/squeel/visitors/visitor.rb', line 139

def quoted?(object)
  case object
  when Arel::Nodes::SqlLiteral, Bignum, Fixnum,
    Arel::SelectManager
    false
  when NilClass
    defined?(Arel::Nodes::Quoted) ? true : false
  else
    true
  end
end

#symbolify(o) ⇒ Object (private)


53
54
55
56
57
58
59
60
# File 'lib/squeel/visitors/visitor.rb', line 53

def symbolify(o)
  case o
  when Symbol, String, Nodes::Stub
    o.to_sym
  else
    nil
  end
end

#visit(object, parent) ⇒ Object (private)

Visit the object.

Parameters:

  • object

    The object to visit

  • parent

    The object's parent within the context


181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/squeel/visitors/visitor.rb', line 181

def visit(object, parent)
  send(DISPATCH[object.class], object, parent)
rescue NoMethodError => e
  raise e if respond_to?(DISPATCH[object.class], true)

  superklass = object.class.ancestors.find { |klass|
    respond_to?(DISPATCH[klass], true)
  }
  raise(TypeError, "Cannot visit #{object.class}") unless superklass
  DISPATCH[object.class] = DISPATCH[superklass]
  retry
end

#visit!(object, parent) ⇒ Object (private)


194
195
196
197
198
# File 'lib/squeel/visitors/visitor.rb', line 194

def visit!(object, parent)
  send("#{DISPATCH[object.class]}!", object, parent)
rescue NoMethodError => e
  visit(object, parent)
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


452
453
454
# File 'lib/squeel/visitors/visitor.rb', line 452

def visit_ActiveRecord_Base(o, parent)
  o.id
end

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

Visit an Active Record 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


442
443
444
# File 'lib/squeel/visitors/visitor.rb', line 442

def visit_ActiveRecord_Relation(o, parent)
  o.arel
end

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


456
457
458
# File 'lib/squeel/visitors/visitor.rb', line 456

def visit_Arel_Nodes_Node(o, parent)
  o
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


220
221
222
# File 'lib/squeel/visitors/visitor.rb', line 220

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

#visit_Array!(o, parent) ⇒ Object (private)


224
225
226
# File 'lib/squeel/visitors/visitor.rb', line 224

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 an ordering, grouping, etc.


234
235
236
237
238
239
240
241
242
# File 'lib/squeel/visitors/visitor.rb', line 234

def visit_Hash(o, parent)
  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.flatten
end

#visit_Hash!(o, parent) ⇒ Object (private)


244
245
246
247
248
249
250
251
252
# File 'lib/squeel/visitors/visitor.rb', line 244

def visit_Hash!(o, parent)
  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.flatten
end

#visit_passthrough(object, parent) ⇒ Object (private) Also known as: visit_Fixnum, visit_Bignum

Pass an object through the visitor unmodified. This is in order to allow objects that don't require modification to be handled by Arel directly.

Parameters:

  • object

    The object to visit

  • parent

    The object's parent within the context

Returns:

  • The object, unmodified


207
208
209
# File 'lib/squeel/visitors/visitor.rb', line 207

def visit_passthrough(object, parent)
  object
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.


340
341
342
# File 'lib/squeel/visitors/visitor.rb', line 340

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.


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

def visit_Squeel_Nodes_As(o, parent)
  # patch for 4+, binds params using native to_sql before transforms to sql string
  if ::ActiveRecord::VERSION::MAJOR >= 4 && o.left.is_a?(::ActiveRecord::Relation)
    Arel::Nodes::TableAlias.new(
      Arel::Nodes::Grouping.new(
        Arel::Nodes::SqlLiteral.new(
          o.left.respond_to?(:to_sql_with_binding_params) ? o.left.to_sql_with_binding_params : o.left.to_sql
        )
      ),
      o.right
    )
  else
    left = visit(o.left, parent)
    # Some nodes, like Arel::SelectManager, have their own #as methods,
    # with behavior that we don't want to clobber.
    if left.respond_to?(:as)
      left.as(o.right)
    else
      Arel::Nodes::As.new(left, o.right)
    end
  end
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.


377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/squeel/visitors/visitor.rb', line 377

def visit_Squeel_Nodes_Function(o, parent)
  args = o.args.map do |arg|
    case arg
    when Nodes::Function, Nodes::As, Nodes::Literal, Nodes::Grouping, Nodes::KeyPath
      visit(arg, parent)
    when ActiveRecord::Relation
      arg.arel.ast
    when Symbol, Nodes::Stub
      if defined?(Arel::Collectors::SQLString)
        Arel.sql(arel_visitor.compile(contextualize(parent)[arg.to_s]))
      else
        Arel.sql(arel_visitor.accept(contextualize(parent)[arg.to_s]))
      end
    else
      quote arg
    end
  end

  Arel::Nodes::NamedFunction.new(o.function_name.to_s, 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


367
368
369
# File 'lib/squeel/visitors/visitor.rb', line 367

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. This will traverse the keypath's “path”, setting a new parent as though the keypath's endpoint was in a deeply-nested hash, then visit the endpoint with the new parent.

Parameters:

  • o (Nodes::KeyPath)

    The keypath to visit

  • parent

    The keypath's parent within the context

Returns:

  • The visited endpoint, with the parent from the KeyPath's path.


283
284
285
286
287
# File 'lib/squeel/visitors/visitor.rb', line 283

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

  visit(o.endpoint, parent)
end

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


289
290
291
292
293
# File 'lib/squeel/visitors/visitor.rb', line 289

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


300
301
302
# File 'lib/squeel/visitors/visitor.rb', line 300

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


358
359
360
# File 'lib/squeel/visitors/visitor.rb', line 358

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.


405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/squeel/visitors/visitor.rb', line 405

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


349
350
351
# File 'lib/squeel/visitors/visitor.rb', line 349

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_Stub(o, parent) ⇒ Arel::Attribute (private)

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

Parameters:

  • o (Nodes::Stub)

    The stub to visit

  • parent

    The stub's parent within the context

Returns:

  • (Arel::Attribute)

    An attribute on the contextualized parent table


272
273
274
# File 'lib/squeel/visitors/visitor.rb', line 272

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


261
262
263
# File 'lib/squeel/visitors/visitor.rb', line 261

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


90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/squeel/visitors/visitor.rb', line 90

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

  can_visit?(v) ? visit(v, parent || k) : v
ensure
  @hash_context_depth -= 1
end

#visit_with_hash_context_shift!(k, v, parent) ⇒ Object (private)


105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/squeel/visitors/visitor.rb', line 105

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

  can_visit?(v) ? visit!(v, parent || k) : v
ensure
  @hash_context_depth -= 1
end

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

If there is no context change, the default behavior is to return the value unchanged. Subclasses will alter this behavior as needed.

Parameters:

  • k

    The hash key

  • v

    The hash value

  • parent

    The current parent object in the context

Returns:

  • The same value we just received.


127
128
129
# File 'lib/squeel/visitors/visitor.rb', line 127

def visit_without_hash_context_shift(k, v, parent)
  v
end