Completed
Push — master ( 89761c...e12ccb )
by Michael
02:47
created

Statement.statement_parameters()   A

Complexity

Conditions 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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