Completed
Push — mongo-graph ( a5e079...6fc2a5 )
by Michael
03:53
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 1
module NoSE
4
  # A single condition in a where clause
5 1
  class Condition
6 1
    attr_reader :field, :is_range, :operator, :value
7 1
    alias range? is_range
8
9 1
    def initialize(field, operator, value)
10 2561
      @field = field
11 2561
      @operator = operator
12 2561
      @is_range = [:>, :>=, :<, :<=].include? operator
13 2561
      @value = value
14
15
      # XXX: Not frozen by now to support modification during query execution
16
      # freeze
17
    end
18
19 1
    def inspect
20
      "#{@field.inspect} #{@operator} #{value}"
21
    end
22
23
    # Compare conditions equal by their field and operator
24
    # @return [Boolean]
25 1
    def ==(other)
26 27
      @field == other.field && @operator == other.operator
27
    end
28 1
    alias eql? ==
29
30 1
    def hash
31 1621
      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 1
    def resolve_foreign_key
38 7
      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 1
  module StatementConditions
46 1
    attr_reader :conditions
47
48
    # @return [void]
49 1
    def populate_conditions(params)
50 1527
      @conditions = params[:conditions]
51 1527
      @eq_fields = conditions.each_value.reject(&:range?).map(&:field).to_set
52 1527
      @range_field = conditions.each_value.find(&:range?)
53 1527
      @range_field = @range_field.field unless @range_field.nil?
54
    end
55
56 1
    def self.included(base)
57 4
      base.extend ClassMethods
58
    end
59
60
    # Add methods to the class for populating conditions
61 1
    module ClassMethods
62 1
      private
63
64
      # Extract conditions from a parse tree
65
      # @return [Hash]
66 1
      def conditions_from_tree(tree, params)
67 315
        conditions = tree[:where].nil? ? [] : tree[:where][:expression]
68 686
        conditions = conditions.map { |c| build_condition c, tree, params }
69
70 313
        params[:conditions] = Hash[conditions.map do |condition|
71 369
          [condition.field.id, condition]
72
        end]
73
      end
74
75
      # Construct a condition object from the parse tree
76
      # @return [void]
77 1
      def build_condition(condition, tree, params)
78 371
        field = add_field_with_prefix tree[:path], condition[:field], params
79 371
        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 1
      def condition_value(condition, field)
86 371
        value = condition[:value]
87
88
        # Convert the value to the correct type
89 371
        type = field.class.const_get 'TYPE'
90
        value = field.class.value_from_string(value.to_s) \
91 371
          unless type.nil? || value.nil?
92
93
        # Don't allow predicates on foreign keys
94
        fail InvalidStatementException, 'Predicates cannot use foreign keys' \
95 370
          if field.is_a? Fields::ForeignKeyField
96
97 369
        condition.delete :value
98
99 369
        value
100
      end
101
    end
102
  end
103
104
  # A path from a primary key to a chain of foreign keys
105 1
  class KeyPath
106 1
    include Enumerable
107
108 1
    extend Forwardable
109 1
    def_delegators :@keys, :each, :inspect, :to_s, :length, :count, :last,
110
                   :empty?
111
112 1
    def initialize(keys = [])
113
      fail InvalidKeyPathException, 'first key must be an ID' \
114 41979
        unless keys.empty? || keys.first.instance_of?(Fields::IDField)
115
116 41978
      keys_match = keys.each_cons(2).map do |prev_key, key|
117 32771
        key.parent == prev_key.entity
118
      end.all?
119
      fail InvalidKeyPathException, 'keys must match along the path' \
120 41978
        unless keys_match
121
122 41977
      @keys = keys
123
    end
124
125
    # Two key paths are equal if their underlying keys are equal or the reverse
126
    # @return [Boolean]
127 1
    def ==(other, check_reverse = true)
128
      @keys == other.instance_variable_get(:@keys) ||
129 4
        (check_reverse && reverse.send(:==, other.reverse, false))
130
    end
131 1
    alias eql? ==
132
133
    # Check if this path starts with another path
134
    # @return [Boolean]
135 1
    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 1
    def include?(key)
144 3477
      @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 1
    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 1
    def [](index)
167 3387
      if index.is_a? Range
168 1
        keys = @keys[index]
169
        keys[0] = keys[0].entity.id_field \
170 1
          unless keys.empty? || keys[0].instance_of?(Fields::IDField)
171 1
        KeyPath.new(keys)
172
      else
173 3386
        key = @keys[index]
174
        key = key.entity.id_field \
175 3386
          unless key.nil? || key.instance_of?(Fields::IDField)
176 3386
        key
177
      end
178
    end
179
180
    # Return the reverse of this path
181
    # @return [KeyPath]
182 1
    def reverse
183 109
      KeyPath.new reverse_path
184
    end
185
186
    # Reverse this path in place
187
    # @return [void]
188 1
    def reverse!
189
      @keys = reverse_path
190
    end
191
192
    # Simple wrapper so that we continue to be a KeyPath
193
    # @return [KeyPath]
194 1
    def to_a
195
      self
196
    end
197
198
    # Return all the entities along the path
199
    # @return [Array<Entity>]
200 1
    def entities
201 9939
      @entities ||= @keys.map(&:entity)
202
    end
203
204
    # Split the path where it intersects the given entity
205
    # @return [KeyPath]
206 1
    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 1
    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 1
    def path_for_field(field)
229 56
      return [field.name] if @keys.first.parent == field.parent
230
231
      @keys.each_cons(2).take_while do |prev_key, _|
232 36
        prev_key.entity != field.parent
233 26
      end.map(&:last).map(&:name) << field.name
234
    end
235
236
    # Find the parent of a given field
237
    # @Return [Entity]
238 1
    def find_field_parent(field)
239 2
      parent = find do |key|
240
        field.parent == key.parent ||
241 2
          (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 2
      return nil if parent.nil?
246
247 2
      parent = parent.parent unless parent.is_a?(Fields::ForeignKeyField)
248 2
      parent
249
    end
250
251
    # Produce all subpaths of this path
252
    # @return [Enumerable<KeyPath>]
253 1
    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 1
    private
265
266
    # Get the reverse path
267
    # @return [Array<Fields::Field>]
268 1
    def reverse_path
269 109
      return [] if @keys.empty?
270 109
      [@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 1
  class Statement
276 1
    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 1
    def self.parse(text, model, group: nil, label: nil, support: false)
281 403
      klass = statement_class text, support
282 403
      tree = parse_tree text, klass
283
284
      # Ensure we have a valid path in the parse tree
285 403
      tree[:path] ||= [tree[:entity]]
286
      fail InvalidStatementException,
287
           "FROM clause must start with #{tree[:entity]}" \
288 403
           if tree[:entity] && tree[:path].first != tree[:entity]
289
290 402
      params = statement_parameters tree, model
291 402
      statement = klass.parse tree, params, text, group: group, label: label
292 399
      statement.instance_variable_set :@comment, tree[:comment].to_s
293
294
      # Support queries need to populate extra values before finalizing
295 399
      unless support
296 399
        statement.hash
297 399
        statement.freeze
298
      end
299
300 399
      statement
301
    end
302
303
    # Produce the class of the statement for the given text
304
    # @return [Class, Symbol]
305 1
    def self.statement_class(text, support)
306 403
      return SupportQuery if support
307
308 403
      case text.split.first
309
      when 'INSERT'
310 74
        Insert
311
      when 'DELETE'
312 9
        Delete
313
      when 'UPDATE'
314 26
        Update
315
      when 'CONNECT'
316 9
        Connect
317
      when 'DISCONNECT'
318 4
        Disconnect
319
      else # SELECT
320 281
        Query
321
      end
322
    end
323 1
    private_class_method :statement_class
324
325
    # Run the parser and produce the parse tree
326
    # @raise [ParseFailed]
327
    # @return [Hash]
328 1
    def self.parse_tree(text, klass)
329
      # Set the type of the statement
330
      # (but CONNECT and DISCONNECT use the same parse rule)
331 403
      type = klass.name.split('::').last.downcase.to_sym
332 403
      type = :connect if type == :disconnect
333
334
      # If parsing fails, re-raise as our custom exception
335 403
      begin
336 403
        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 403
      tree
344
    end
345 1
    private_class_method :parse_tree
346
347
    # Produce the parameter hash needed to build a new statement
348
    # @return [Hash]
349 1
    def self.statement_parameters(tree, model)
350 402
      entity = model[tree[:path].first.to_s]
351 402
      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 402
      }
359
    end
360 1
    private_class_method :statement_parameters
361
362
    # Calculate the longest path of entities traversed by the statement
363
    # @return [KeyPath]
364 1
    def self.find_longest_path(path_entities, from)
365 402
      path = path_entities.map(&:to_s)[1..-1]
366 402
      longest_entity_path = [from]
367 402
      keys = [from.id_field]
368
369 402
      path.each do |key|
370
        # Search through foreign keys
371 152
        last_entity = longest_entity_path.last
372 152
        longest_entity_path << last_entity[key].entity
373 152
        keys << last_entity[key]
374
      end
375
376 402
      KeyPath.new(keys)
377
    end
378 1
    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 1
    def self.add_field_with_prefix(path, field, params)
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for add_field_with_prefix is considered too high. [20.42/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
383 706
      field_path = field.map(&:to_s)
384 706
      prefix_index = path.index(field_path.first)
385
      field_path = path[0..prefix_index - 1] + field_path \
386 706
        unless prefix_index.zero?
387 706
      field_path.map!(&:to_s)
388
389
      # Expand the graph to include any keys which were found
390 706
      field_path[0..-2].prefixes.drop(1).each do |key_path|
391 212
        key = params[:model].find_field key_path
392 212
        params[:graph].add_edge key.parent, key.entity, key
393
      end
394
395 706
      params[:model].find_field field_path
396
    end
397 1
    private_class_method :add_field_with_prefix
398
399 1
    def initialize(params, text, group: nil, label: nil)
400 1540
      @entity = params[:entity]
401 1540
      @key_path = params[:key_path]
402 1540
      @longest_entity_path = @key_path.entities
403 1540
      @graph = params[:graph]
404 1540
      @model = params[:model]
405 1540
      @text = text
406 1540
      @group = group
407 1540
      @label = label
408
    end
409
410
    # Specifies if the statement modifies any data
411
    # @return [Boolean]
412 1
    def read_only?
413
      false
414
    end
415
416
    # Specifies if the statement will require data to be inserted
417
    # @return [Boolean]
418 1
    def requires_insert?(_index)
419 11
      false
420
    end
421
422
    # Specifies if the statement will require data to be deleted
423
    # @return [Boolean]
424 1
    def requires_delete?(_index)
425 827
      false
426
    end
427
428
    # :nocov:
429 1
    def to_color
430 3
      "#{@text} [magenta]#{@longest_entity_path.map(&:name).join ', '}[/]"
431
    end
432
    # :nocov:
433
434 1
    protected
435
436
    # Quote the value of an identifier used as
437
    # a value for a field, quoted if needed
438
    # @return [String]
439 1
    def maybe_quote(value, field)
440 5
      if value.nil?
441
        '?'
442 5
      elsif [Fields::IDField,
443
             Fields::ForeignKeyField,
444
             Fields::StringField].include? field.class
445 5
        "\"#{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 1
    def from_path(path, prefix_path = nil, field = nil)
0 ignored issues
show
Coding Style introduced by
This method is 21 lines long. Your coding style permits a maximum length of 20.
Loading history...
Coding Style introduced by
The Assignment, Branch, Condition size for from_path is considered too high. [33.87/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
455 4534
      if prefix_path.nil?
456 1149
        from = path.first.parent.name.dup
457
      else
458
        # Find where the two paths intersect to get the first path component
459 3385
        first_key = prefix_path.entries.find do |key|
460
          path.entities.include?(key.parent) || \
461
            key.is_a?(Fields::ForeignKeyField) && \
462 3417
              path.entities.include?(key.entity)
463
        end
464 3385
        from = if first_key.primary_key?
465 3353
                 first_key.parent.name.dup
466
               else
467 32
                 first_key.name.dup
468
               end
469
      end
470
471
      from << '.' << path.entries[1..-1].map(&:name).join('.') \
472 4534
        if path.length > 1
473
474 4534
      unless field.nil?
475 3385
        from << '.' unless from.empty?
476 3385
        from << field.name
477
      end
478
479 4534
      from
480
    end
481
482
    # Produce a string which can be used
483
    # as the settings clause in a statement
484
    # @return [String]
485 1
    def settings_clause
486
      'SET ' + @settings.map do |setting|
487 3
        value = maybe_quote setting.value, setting.field
488 3
        "#{setting.field.name} = #{value}"
489 2
      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 1
    def where_clause(field_namer = :to_s.to_proc)
496
      ' WHERE ' + @conditions.values.map do |condition|
497 1154
        value = condition.value.nil? ? '?' : condition.value
498 1154
        "#{field_namer.call condition.field} #{condition.operator} #{value}"
499 1149
      end.join(' AND ')
500
    end
501
  end
502
503
  # The setting of a field from an {Update} statement
504 1
  class FieldSetting
505 1
    attr_reader :field, :value
506
507 1
    def initialize(field, value)
508 295
      @field = field
509 295
      @value = value
510
511 295
      freeze
512
    end
513
514 1
    def inspect
515
      "#{@field.inspect} = #{value}"
516
    end
517
518
    # Compare settings equal by their field
519 1
    def ==(other)
520 25
      other.field == @field
521
    end
522 1
    alias eql? ==
523
524
    # Hash by field and value
525 1
    def hash
526 281
      Zlib.crc32 [@field.id, @value].to_s
527
    end
528
  end
529
530
  # Module to add variable settings to a {Statement}
531 1
  module StatementSettings
532 1
    attr_reader :settings
533
534 1
    def self.included(base)
535 2
      base.extend ClassMethods
536
    end
537
538
    # Add methods to the class for populating settings
539 1
    module ClassMethods
540 1
      private
541
542
      # Extract settings from a parse tree
543
      # @return [Array<FieldSetting>]
544 1
      def settings_from_tree(tree, params)
545 99
        params[:settings] = tree[:settings].map do |setting|
546 281
          field = params[:entity][setting[:field].to_s]
547 281
          value = setting[:value]
548
549 281
          type = field.class.const_get 'TYPE'
550
          value = field.class.value_from_string(value.to_s) \
551 281
            unless type.nil? || value.nil?
552
553 281
          setting.delete :value
554 281
          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 1
  module StatementSupportQuery
562
    # Determine if this statement modifies a particular index
563 1
    def modifies_index?(index)
564 270
      !(@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 1
    def support_queries(_index)
570
      []
571
    end
572
573 1
    private
574
575
    # Build a support query to update a given index
576
    # and select fields with certain conditions
577
    # @return [SupportQuery]
578 1
    def build_support_query(entity, index, graph, select, conditions)
579 1976
      return nil if select.empty?
580
581 1140
      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 1140
      support_query = SupportQuery.new entity, params, nil, group: @group
590 1140
      support_query.instance_variable_set :@statement, self
591 1140
      support_query.instance_variable_set :@index, index
592 1140
      support_query.instance_variable_set :@comment, (hash ^ index.hash).to_s
593 1140
      support_query.instance_variable_set :@text, support_query.unparse
594 1140
      support_query.hash
595 1140
      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 1
    def support_queries_for_entity(index, select)
0 ignored issues
show
Coding Style introduced by
The Assignment, Branch, Condition size for support_queries_for_entity is considered too high. [26.38/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
602 42
      graphs = index.graph.size > 1 ? index.graph.split(entity, true) : []
603
604
      graphs.map do |graph|
605 24
        support_fields = select.select do |field|
606 58
          field.parent != entity && graph.entities.include?(field.parent)
607
        end.to_set
608
609 24
        conditions = {
610
          entity.id_field.id => Condition.new(entity.id_field, :'=', nil)
611
        }
612
613 24
        split_entity = split_entity graph, index.graph, entity
614 24
        build_support_query split_entity, index, graph, support_fields,
615
                            conditions
616 42
      end.compact
617
    end
618
619
    # Determine which entity a subgraph was split at
620
    # @return [Entity]
621 1
    def split_entity(subgraph, graph, entity)
622
      graph.keys_from_entity(entity).find do |key|
623 2767
        subgraph.entities.include? key.entity
624 1934
      end.entity
625
    end
626
  end
627
628
  # Thrown when something tries to parse an invalid statement
629 1
  class InvalidStatementException < StandardError
630
  end
631
632
  # Thrown when trying to construct a KeyPath which is not valid
633 1
  class InvalidKeyPathException < StandardError
634
  end
635
636
  # Thrown when parsing a statement fails
637 1
  class ParseFailed < StandardError
638
  end
639
end
640
641 1
require_relative 'statements/connection'
642 1
require_relative 'statements/delete'
643 1
require_relative 'statements/insert'
644 1
require_relative 'statements/query'
645
require_relative 'statements/update'
646