Completed
Push — mongo-graph ( 87a116...a5e079 )
by Michael
03:26
created

Statement.parse_tree()   A

Complexity

Conditions 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.3332

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 17
ccs 6
cts 9
cp 0.6667
crap 3.3332
rs 9.4285
1
# frozen_string_literal: true
2
3
module NoSE
4
  # A single condition in a where clause
5
  class Condition
6
    attr_reader :field, :is_range, :operator, :value
7
    alias range? is_range
8
9
    def initialize(field, operator, value)
10
      @field = field
11
      @operator = operator
12
      @is_range = [:>, :>=, :<, :<=].include? operator
13
      @value = value
14
15
      # XXX: Not frozen by now to support modification during query execution
16
      # freeze
17
    end
18
19
    def inspect
20
      "#{@field.inspect} #{@operator} #{value}"
21
    end
22
23
    # Compare conditions equal by their field and operator
24
    # @return [Boolean]
25
    def ==(other)
26
      @field == other.field && @operator == other.operator
27
    end
28
    alias eql? ==
29
30
    def hash
31
      Zlib.crc32 [@field.id, @operator].to_s
32
    end
33
34
    # If the condition is on a foreign key, resolve
35
    # it to the primary key of the related entity
36
    # @return [Condition]
37
    def resolve_foreign_key
38
      return self unless field.is_a?(Fields::ForeignKeyField)
39
40
      Condition.new @field.entity.id_field, @operator, @value
41
    end
42
  end
43
44
  # Used to add a list of conditions to a {Statement}
45
  module StatementConditions
46
    attr_reader :conditions
47
48
    # @return [void]
49
    def populate_conditions(params)
50
      @conditions = params[:conditions]
51
      @eq_fields = conditions.each_value.reject(&:range?).map(&:field).to_set
52
      @range_field = conditions.each_value.find(&:range?)
53
      @range_field = @range_field.field unless @range_field.nil?
54
    end
55
56
    def self.included(base)
57
      base.extend ClassMethods
58
    end
59
60
    # Add methods to the class for populating conditions
61
    module ClassMethods
62
      private
63
64
      # Extract conditions from a parse tree
65
      # @return [Hash]
66
      def conditions_from_tree(tree, params)
67
        conditions = tree[:where].nil? ? [] : tree[:where][:expression]
68
        conditions = conditions.map { |c| build_condition c, tree, params }
69
70
        params[:conditions] = Hash[conditions.map do |condition|
71
          [condition.field.id, condition]
72
        end]
73
      end
74
75
      # Construct a condition object from the parse tree
76
      # @return [void]
77
      def build_condition(condition, tree, params)
78
        field = add_field_with_prefix tree[:path], condition[:field], params
79
        Condition.new field, condition[:op].to_sym,
80
                      condition_value(condition, field)
81
      end
82
83
      # Get the value of a condition from the parse tree
84
      # @return [Object]
85
      def condition_value(condition, field)
86
        value = condition[:value]
87
88
        # Convert the value to the correct type
89
        type = field.class.const_get 'TYPE'
90
        value = field.class.value_from_string(value.to_s) \
91
          unless type.nil? || value.nil?
92
93
        # Don't allow predicates on foreign keys
94
        fail InvalidStatementException, 'Predicates cannot use foreign keys' \
95
          if field.is_a? Fields::ForeignKeyField
96
97
        condition.delete :value
98
99
        value
100
      end
101
    end
102
  end
103
104
  # A path from a primary key to a chain of foreign keys
105
  class KeyPath
106
    include Enumerable
107
108
    extend Forwardable
109
    def_delegators :@keys, :each, :inspect, :to_s, :length, :count, :last,
110
                   :empty?
111
112
    def initialize(keys = [])
113
      fail InvalidKeyPathException, 'first key must be an ID' \
114
        unless keys.empty? || keys.first.instance_of?(Fields::IDField)
115
116
      keys_match = keys.each_cons(2).map do |prev_key, key|
117
        key.parent == prev_key.entity
118
      end.all?
119
      fail InvalidKeyPathException, 'keys must match along the path' \
120
        unless keys_match
121
122
      @keys = keys
123
    end
124
125
    # Two key paths are equal if their underlying keys are equal or the reverse
126
    # @return [Boolean]
127
    def ==(other, check_reverse = true)
128
      @keys == other.instance_variable_get(:@keys) ||
129
        (check_reverse && reverse.send(:==, other.reverse, false))
130
    end
131
    alias eql? ==
132
133
    # Check if this path starts with another path
134
    # @return [Boolean]
135
    def start_with?(other, check_reverse = true)
136
      other_keys = other.instance_variable_get(:@keys)
137
      @keys[0..other_keys.length - 1] == other_keys ||
138
        (check_reverse && reverse.start_with?(other.reverse, false))
139
    end
140
141
    # Check if a key is included in the path
142
    # @return [Boolean]
143
    def include?(key)
144
      @keys.include?(key) || entities.any? { |e| e.id_field == key }
145
    end
146
147
    # Combine two key paths by gluing together the keys
148
    # @return [KeyPath]
149
    def +(other)
150
      fail TypeError unless other.is_a? KeyPath
151
      other_keys = other.instance_variable_get(:@keys)
152
153
      # Just copy if there's no combining necessary
154
      return dup if other_keys.empty?
155
      return other.dup if @keys.empty?
156
157
      # Only allow combining if the entities match
158
      fail ArgumentError unless other_keys.first.parent == entities.last
159
160
      # Combine the two paths
161
      KeyPath.new(@keys + other_keys[1..-1])
162
    end
163
164
    # Return a slice of the path
165
    # @return [KeyPath]
166
    def [](index)
167
      if index.is_a? Range
168
        keys = @keys[index]
169
        keys[0] = keys[0].entity.id_field \
170
          unless keys.empty? || keys[0].instance_of?(Fields::IDField)
171
        KeyPath.new(keys)
172
      else
173
        key = @keys[index]
174
        key = key.entity.id_field \
175
          unless key.nil? || key.instance_of?(Fields::IDField)
176
        key
177
      end
178
    end
179
180
    # Return the reverse of this path
181
    # @return [KeyPath]
182
    def reverse
183
      KeyPath.new reverse_path
184
    end
185
186
    # Reverse this path in place
187
    # @return [void]
188
    def reverse!
189
      @keys = reverse_path
190
    end
191
192
    # Simple wrapper so that we continue to be a KeyPath
193
    # @return [KeyPath]
194
    def to_a
195
      self
196
    end
197
198
    # Return all the entities along the path
199
    # @return [Array<Entity>]
200
    def entities
201
      @entities ||= @keys.map(&:entity)
202
    end
203
204
    # Split the path where it intersects the given entity
205
    # @return [KeyPath]
206
    def split(entity)
207
      if first.parent == entity
208
        query_keys = KeyPath.new([entity.id_field])
209
      else
210
        query_keys = []
211
        each do |key|
212
          query_keys << key
213
          break if key.is_a?(Fields::ForeignKeyField) && key.entity == entity
214
        end
215
        query_keys = KeyPath.new(query_keys)
216
      end
217
    end
218
219
    # Find where the path intersects the given
220
    # entity and splice in the target path
221
    # @return [KeyPath]
222
    def splice(target, entity)
223
      split(entity) + target
224
    end
225
226
    # Get the named path to reach this field through the list of keys
227
    # @return [Array<String>]
228
    def path_for_field(field)
229
      return [field.name] if @keys.first.parent == field.parent
230
231
      @keys.each_cons(2).take_while do |prev_key, _|
232
        prev_key.entity != field.parent
233
      end.map(&:last).map(&:name) << field.name
234
    end
235
236
    # Find the parent of a given field
237
    # @Return [Entity]
238
    def find_field_parent(field)
239
      parent = find do |key|
240
        field.parent == key.parent ||
241
          (key.is_a?(Fields::ForeignKeyField) && field.parent == key.entity)
242
      end
243
244
      # This field is not on this portion of the path, so skip
245
      return nil if parent.nil?
246
247
      parent = parent.parent unless parent.is_a?(Fields::ForeignKeyField)
248
      parent
249
    end
250
251
    # Produce all subpaths of this path
252
    # @return [Enumerable<KeyPath>]
253
    def subpaths(include_self = true)
254
      Enumerator.new do |enum|
255
        enum.yield self if include_self
256
        1.upto(@keys.length) do |i|
257
          i.upto(@keys.length) do |j|
258
            enum.yield self[i - 1..j - 1]
259
          end
260
        end
261
      end
262
    end
263
264
    private
265
266
    # Get the reverse path
267
    # @return [Array<Fields::Field>]
268
    def reverse_path
269
      return [] if @keys.empty?
270
      [@keys.last.entity.id_field] + @keys[1..-1].reverse.map(&:reverse)
271
    end
272
  end
273
274
  # A CQL statement and its associated data
275
  class Statement
276
    attr_reader :entity, :key_path, :label, :graph,
277
                :group, :text, :eq_fields, :range_field, :comment
278
279
    # Parse either a query or an update
280
    def self.parse(text, model, group: nil, label: nil, support: false)
281
      klass = statement_class text, support
282
      tree = parse_tree text, klass
283
284
      # Ensure we have a valid path in the parse tree
285
      tree[:path] ||= [tree[:entity]]
286
      fail InvalidStatementException,
287
           "FROM clause must start with #{tree[:entity]}" \
288
           if tree[:entity] && tree[:path].first != tree[:entity]
289
290
      params = statement_parameters tree, model
291
      statement = klass.parse tree, params, text, group: group, label: label
292
      statement.instance_variable_set :@comment, tree[:comment].to_s
293
294
      # Support queries need to populate extra values before finalizing
295
      unless support
296
        statement.hash
297
        statement.freeze
298
      end
299
300
      statement
301
    end
302
303
    # Produce the class of the statement for the given text
304
    # @return [Class, Symbol]
305
    def self.statement_class(text, support)
306
      return SupportQuery if support
307
308
      case text.split.first
309
      when 'INSERT'
310
        Insert
311
      when 'DELETE'
312
        Delete
313
      when 'UPDATE'
314
        Update
315
      when 'CONNECT'
316
        Connect
317
      when 'DISCONNECT'
318
        Disconnect
319
      else # SELECT
320
        Query
321
      end
322
    end
323
    private_class_method :statement_class
324
325
    # Run the parser and produce the parse tree
326
    # @raise [ParseFailed]
327
    # @return [Hash]
328
    def self.parse_tree(text, klass)
329
      # Set the type of the statement
330
      # (but CONNECT and DISCONNECT use the same parse rule)
331
      type = klass.name.split('::').last.downcase.to_sym
332
      type = :connect if type == :disconnect
333
334
      # If parsing fails, re-raise as our custom exception
335
      begin
336
        tree = CQLT.new.apply(CQLP.new.method(type).call.parse(text))
337
      rescue Parslet::ParseFailed => exc
338
        new_exc = ParseFailed.new exc.cause.ascii_tree
339
        new_exc.set_backtrace exc.backtrace
340
        raise new_exc
341
      end
342
343
      tree
344
    end
345
    private_class_method :parse_tree
346
347
    # Produce the parameter hash needed to build a new statement
348
    # @return [Hash]
349
    def self.statement_parameters(tree, model)
350
      entity = model[tree[:path].first.to_s]
351
      key_path = find_longest_path(tree[:path], entity)
352
353
      {
354
        model: model,
355
        entity: entity,
356
        key_path: key_path,
357
        graph: QueryGraph::Graph.from_path(key_path)
358
      }
359
    end
360
    private_class_method :statement_parameters
361
362
    # Calculate the longest path of entities traversed by the statement
363
    # @return [KeyPath]
364
    def self.find_longest_path(path_entities, from)
365
      path = path_entities.map(&:to_s)[1..-1]
366
      longest_entity_path = [from]
367
      keys = [from.id_field]
368
369
      path.each do |key|
370
        # Search through foreign keys
371
        last_entity = longest_entity_path.last
372
        longest_entity_path << last_entity[key].entity
373
        keys << last_entity[key]
374
      end
375
376
      KeyPath.new(keys)
377
    end
378
    private_class_method :find_longest_path
379
380
    # A helper to look up a field based on the path specified in the statement
381
    # @return [Fields::Field]
382
    def self.add_field_with_prefix(path, field, params)
383
      field_path = field.map(&:to_s)
384
      prefix_index = path.index(field_path.first)
385
      field_path = path[0..prefix_index - 1] + field_path \
386
        unless prefix_index.zero?
387
      field_path.map!(&:to_s)
388
389
      # Expand the graph to include any keys which were found
390
      field_path[0..-2].prefixes.drop(1).each do |key_path|
391
        key = params[:model].find_field key_path
392
        params[:graph].add_edge key.parent, key.entity, key
393
      end
394
395
      params[:model].find_field field_path
396
    end
397
    private_class_method :add_field_with_prefix
398
399
    def initialize(params, text, group: nil, label: nil)
400
      @entity = params[:entity]
401
      @key_path = params[:key_path]
402
      @longest_entity_path = @key_path.entities
403
      @graph = params[:graph]
404
      @model = params[:model]
405
      @text = text
406
      @group = group
407
      @label = label
408
    end
409
410
    # Specifies if the statement modifies any data
411
    # @return [Boolean]
412
    def read_only?
413
      false
414
    end
415
416
    # Specifies if the statement will require data to be inserted
417
    # @return [Boolean]
418
    def requires_insert?(_index)
419
      false
420
    end
421
422
    # Specifies if the statement will require data to be deleted
423
    # @return [Boolean]
424
    def requires_delete?(_index)
425
      false
426
    end
427
428
    # :nocov:
429
    def to_color
430
      "#{@text} [magenta]#{@longest_entity_path.map(&:name).join ', '}[/]"
431
    end
432
    # :nocov:
433
434
    protected
435
436
    # Quote the value of an identifier used as
437
    # a value for a field, quoted if needed
438
    # @return [String]
439
    def maybe_quote(value, field)
440
      if value.nil?
441
        '?'
442
      elsif [Fields::IDField,
443
             Fields::ForeignKeyField,
444
             Fields::StringField].include? field.class
445
        "\"#{value}\""
446
      else
447
        value.to_s
448
      end
449
    end
450
451
    # Generate a string which can be used in the "FROM" clause
452
    # of a statement or optionally to specify a field
453
    # @return [String]
454
    def from_path(path, prefix_path = nil, field = nil)
455
      if prefix_path.nil?
456
        from = path.first.parent.name.dup
457
      else
458
        # Find where the two paths intersect to get the first path component
459
        first_key = prefix_path.entries.find do |key|
460
          path.entities.include?(key.parent) || \
461
            key.is_a?(Fields::ForeignKeyField) && \
462
              path.entities.include?(key.entity)
463
        end
464
        from = if first_key.primary_key?
465
                 first_key.parent.name.dup
466
               else
467
                 first_key.name.dup
468
               end
469
      end
470
471
      from << '.' << path.entries[1..-1].map(&:name).join('.') \
472
        if path.length > 1
473
474
      unless field.nil?
475
        from << '.' unless from.empty?
476
        from << field.name
477
      end
478
479
      from
480
    end
481
482
    # Produce a string which can be used
483
    # as the settings clause in a statement
484
    # @return [String]
485
    def settings_clause
486
      'SET ' + @settings.map do |setting|
487
        value = maybe_quote setting.value, setting.field
488
        "#{setting.field.name} = #{value}"
489
      end.join(', ')
490
    end
491
492
    # Produce a string which can be used
493
    # as the WHERE clause in a statement
494
    # @return [String]
495
    def where_clause(field_namer = :to_s.to_proc)
496
      ' WHERE ' += @conditions.values.map do |condition|
0 ignored issues
show
Bug introduced by
The Ruby parser could not interpret the code. It reported: unexpected token tOP_ASGN (Using Ruby 2.3...meter, under `AllCops`).
Loading history...
497
        value = condition.value.nil? ? '?' : condition.value
498
        "#{field_namer.call condition.field} #{condition.operator} #{value}"
499
      end.join(' AND ')
500
    end
501
  end
502
503
  # The setting of a field from an {Update} statement
504
  class FieldSetting
505
    attr_reader :field, :value
506
507
    def initialize(field, value)
508
      @field = field
509
      @value = value
510
511
      freeze
512
    end
513
514
    def inspect
515
      "#{@field.inspect} = #{value}"
516
    end
517
518
    # Compare settings equal by their field
519
    def ==(other)
520
      other.field == @field
521
    end
522
    alias eql? ==
523
524
    # Hash by field and value
525
    def hash
526
      Zlib.crc32 [@field.id, @value].to_s
527
    end
528
  end
529
530
  # Module to add variable settings to a {Statement}
531
  module StatementSettings
532
    attr_reader :settings
533
534
    def self.included(base)
535
      base.extend ClassMethods
536
    end
537
538
    # Add methods to the class for populating settings
539
    module ClassMethods
540
      private
541
542
      # Extract settings from a parse tree
543
      # @return [Array<FieldSetting>]
544
      def settings_from_tree(tree, params)
545
        params[:settings] = tree[:settings].map do |setting|
546
          field = params[:entity][setting[:field].to_s]
547
          value = setting[:value]
548
549
          type = field.class.const_get 'TYPE'
550
          value = field.class.value_from_string(value.to_s) \
551
            unless type.nil? || value.nil?
552
553
          setting.delete :value
554
          FieldSetting.new field, value
555
        end
556
      end
557
    end
558
  end
559
560
  # Extend {Statement} objects to allow them to generate support queries
561
  module StatementSupportQuery
562
    # Determine if this statement modifies a particular index
563
    def modifies_index?(index)
564
      !(@settings.map(&:field).to_set & index.all_fields).empty?
565
    end
566
567
    # Support queries required to updating the given index with this statement
568
    # @return [Array<SupportQuery>]
569
    def support_queries(_index)
570
      []
571
    end
572
573
    private
574
575
    # Build a support query to update a given index
576
    # and select fields with certain conditions
577
    # @return [SupportQuery]
578
    def build_support_query(entity, index, graph, select, conditions)
579
      return nil if select.empty?
580
581
      params = {
582
        select: select,
583
        graph: graph,
584
        key_path: graph.longest_path,
585
        entity: key_path.first.parent,
586
        conditions: conditions
587
      }
588
589
      support_query = SupportQuery.new entity, params, nil, group: @group
590
      support_query.instance_variable_set :@statement, self
591
      support_query.instance_variable_set :@index, index
592
      support_query.instance_variable_set :@comment, (hash ^ index.hash).to_s
593
      support_query.instance_variable_set :@text, support_query.unparse
594
      support_query.hash
595
      support_query.freeze
596
    end
597
598
    # Produce support queries for the entity of the
599
    # statement which select the given set of fields
600
    # @return [Array<SupportQuery>]
601
    def support_queries_for_entity(index, select)
602
      graphs = index.graph.size > 1 ? index.graph.split(entity, true) : []
603
604
      graphs.map do |graph|
605
        support_fields = select.select do |field|
606
          field.parent != entity && graph.entities.include?(field.parent)
607
        end.to_set
608
609
        conditions = {
610
          entity.id_field.id => Condition.new(entity.id_field, :'=', nil)
611
        }
612
613
        split_entity = split_entity graph, index.graph, entity
614
        build_support_query split_entity, index, graph, support_fields,
615
                            conditions
616
      end.compact
617
    end
618
619
    # Determine which entity a subgraph was split at
620
    # @return [Entity]
621
    def split_entity(subgraph, graph, entity)
622
      graph.keys_from_entity(entity).find do |key|
623
        subgraph.entities.include? key.entity
624
      end.entity
625
    end
626
  end
627
628
  # Thrown when something tries to parse an invalid statement
629
  class InvalidStatementException < StandardError
630
  end
631
632
  # Thrown when trying to construct a KeyPath which is not valid
633
  class InvalidKeyPathException < StandardError
634
  end
635
636
  # Thrown when parsing a statement fails
637
  class ParseFailed < StandardError
638
  end
639
end
640
641
require_relative 'statements/connection'
642
require_relative 'statements/delete'
643
require_relative 'statements/insert'
644
require_relative 'statements/query'
645
require_relative 'statements/update'
646