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 | 499 | @field = field |
|
11 | 499 | @operator = operator |
|
12 | 499 | @is_range = [:>, :>=, :<, :<=].include? operator |
|
0 ignored issues
–
show
coding-style
introduced
by
![]() |
|||
13 | 499 | @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 | 7 | @field == other.field && @operator == other.operator |
|
27 | end |
||
28 | 1 | alias eql? == |
|
29 | |||
30 | 1 | def hash |
|
31 | 408 | 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 | 314 | @conditions = params[:conditions] |
|
51 | 314 | @eq_fields = conditions.each_value.reject(&:range?).map(&:field).to_set |
|
52 | 314 | @range_field = conditions.each_value.find(&:range?) |
|
53 | 314 | @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 | 215 | conditions = tree[:where].nil? ? [] : tree[:where][:expression] |
|
68 | 480 | conditions = conditions.map { |c| build_condition c, tree, params } |
|
69 | |||
70 | 213 | params[:conditions] = Hash[conditions.map do |condition| |
|
71 | 263 | [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 | 265 | field = add_field_with_prefix tree[:path], condition[:field], params |
|
79 | 265 | 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 | 265 | value = condition[:value] |
|
87 | |||
88 | # Convert the value to the correct type |
||
89 | 265 | type = field.class.const_get 'TYPE' |
|
90 | value = field.class.value_from_string(value.to_s) \ |
||
91 | 265 | unless type.nil? || value.nil? |
|
92 | |||
93 | # Don't allow predicates on foreign keys |
||
94 | fail InvalidStatementException, 'Predicates cannot use foreign keys' \ |
||
95 | 264 | if field.is_a? Fields::ForeignKeyField |
|
96 | |||
97 | 263 | condition.delete :value |
|
98 | |||
99 | 263 | 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 | 9160 | unless keys.empty? || keys.first.instance_of?(Fields::IDField) |
|
115 | |||
116 | 9159 | keys_match = keys.each_cons(2).all? do |prev_key, key| |
|
117 | 11775 | key.parent == prev_key.entity |
|
118 | end |
||
119 | fail InvalidKeyPathException, 'keys must match along the path' \ |
||
120 | 9159 | unless keys_match |
|
121 | |||
122 | 9158 | @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) |
|
0 ignored issues
–
show
|
|||
128 | 4 | @keys == other.instance_variable_get(:@keys) || |
|
129 | (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) |
|
0 ignored issues
–
show
|
|||
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 | 292 | @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 |
||
0 ignored issues
–
show
|
|||
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]) |
||
0 ignored issues
–
show
|
|||
162 | end |
||
163 | |||
164 | # Return a slice of the path |
||
165 | # @return [KeyPath] |
||
166 | 1 | def [](index) |
|
167 | 202 | 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 | 201 | key = @keys[index] |
|
174 | key = key.entity.id_field \ |
||
175 | 201 | unless key.nil? || key.instance_of?(Fields::IDField) |
|
176 | 201 | key |
|
177 | end |
||
178 | end |
||
179 | |||
180 | # Return the reverse of this path |
||
181 | # @return [KeyPath] |
||
182 | 1 | def reverse |
|
183 | 69 | 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 | 1124 | @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 | 26 | @keys.each_cons(2).take_while do |prev_key, _| |
|
232 | 36 | 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 | 1 | def find_field_parent(field) |
|
239 | 2 | parent = find do |key| |
|
240 | 2 | 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 | 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) |
|
0 ignored issues
–
show
|
|||
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 | 69 | return [] if @keys.empty? |
|
0 ignored issues
–
show
|
|||
270 | 69 | [@keys.last.entity.id_field] + @keys[1..-1].reverse.map(&:reverse) |
|
0 ignored issues
–
show
|
|||
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 | 246 | klass = statement_class text, support |
|
282 | 246 | tree = parse_tree text, klass |
|
283 | |||
284 | # Ensure we have a valid path in the parse tree |
||
285 | 246 | tree[:path] ||= [tree[:entity]] |
|
286 | fail InvalidStatementException, |
||
0 ignored issues
–
show
|
|||
287 | "FROM clause must start with #{tree[:entity]}" \ |
||
288 | 246 | if tree[:entity] && tree[:path].first != tree[:entity] |
|
289 | |||
290 | 245 | params = statement_parameters tree, model |
|
291 | 245 | statement = klass.parse tree, params, text, group: group, label: label |
|
292 | 241 | statement.instance_variable_set :@comment, tree[:comment].to_s |
|
293 | |||
294 | # Support queries need to populate extra values before finalizing |
||
295 | 241 | unless support |
|
296 | 241 | statement.hash |
|
297 | 241 | statement.freeze |
|
298 | end |
||
299 | |||
300 | 241 | 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 | 246 | return SupportQuery if support |
|
307 | |||
308 | 246 | case text.split.first |
|
309 | when 'INSERT' |
||
310 | 17 | Insert |
|
311 | when 'DELETE' |
||
312 | 9 | Delete |
|
313 | when 'UPDATE' |
||
314 | 24 | Update |
|
315 | when 'CONNECT' |
||
316 | 9 | Connect |
|
317 | when 'DISCONNECT' |
||
318 | 4 | Disconnect |
|
319 | else # SELECT |
||
320 | 183 | 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 | 246 | type = klass.name.split('::').last.downcase.to_sym |
|
332 | 246 | type = :connect if type == :disconnect |
|
333 | |||
334 | # If parsing fails, re-raise as our custom exception |
||
335 | begin |
||
336 | 246 | tree = CQLT.new.apply(CQLP.new.method(type).call.parse(text)) |
|
337 | rescue Parslet::ParseFailed => exc |
||
0 ignored issues
–
show
|
|||
338 | new_exc = ParseFailed.new exc.cause.ascii_tree |
||
339 | new_exc.set_backtrace exc.backtrace |
||
340 | raise new_exc |
||
341 | end |
||
342 | |||
343 | 246 | 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 | 245 | entity = model[tree[:path].first.to_s] |
|
351 | 245 | key_path = find_longest_path(tree[:path], entity) |
|
352 | |||
353 | { |
||
354 | 245 | model: model, |
|
355 | entity: entity, |
||
356 | key_path: key_path, |
||
357 | graph: QueryGraph::Graph.from_path(key_path) |
||
358 | } |
||
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 | 245 | path = path_entities.map(&:to_s)[1..-1] |
|
0 ignored issues
–
show
|
|||
366 | 245 | longest_entity_path = [from] |
|
367 | 245 | keys = [from.id_field] |
|
368 | |||
369 | 245 | path.each do |key| |
|
370 | # Search through foreign keys |
||
371 | 75 | last_entity = longest_entity_path.last |
|
372 | 75 | longest_entity_path << last_entity[key].entity |
|
373 | 75 | keys << last_entity[key] |
|
374 | end |
||
375 | |||
376 | 245 | 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
|
|||
383 | 510 | field_path = field.map(&:to_s) |
|
384 | 510 | prefix_index = path.index(field_path.first) |
|
385 | field_path = path[0..prefix_index - 1] + field_path \ |
||
386 | 510 | unless prefix_index.zero? |
|
387 | 510 | field_path.map!(&:to_s) |
|
388 | |||
389 | # Expand the graph to include any keys which were found |
||
390 | 510 | field_path[0..-2].prefixes.drop(1).each do |key_path| |
|
391 | 95 | key = params[:model].find_field key_path |
|
392 | 95 | params[:graph].add_edge key.parent, key.entity, key |
|
393 | end |
||
394 | |||
395 | 510 | 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 | 327 | @entity = params[:entity] |
|
401 | 327 | @key_path = params[:key_path] |
|
402 | 327 | @longest_entity_path = @key_path.entities |
|
403 | 327 | @graph = params[:graph] |
|
404 | 327 | @model = params[:model] |
|
405 | 327 | @text = text |
|
406 | 327 | @group = group |
|
407 | 327 | @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 | 1 | 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
|
|||
455 | 293 | if prefix_path.nil? |
|
456 | 93 | from = path.first.parent.name.dup |
|
457 | else |
||
458 | # Find where the two paths intersect to get the first path component |
||
459 | 200 | first_key = prefix_path.entries.find do |key| |
|
460 | 232 | path.entities.include?(key.parent) || \ |
|
461 | key.is_a?(Fields::ForeignKeyField) && \ |
||
462 | path.entities.include?(key.entity) |
||
463 | end |
||
464 | 200 | from = if first_key.primary_key? |
|
465 | 168 | 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('.') \ |
||
0 ignored issues
–
show
|
|||
472 | 293 | if path.length > 1 |
|
473 | |||
474 | 293 | unless field.nil? |
|
475 | 200 | from << '.' unless from.empty? |
|
476 | 200 | from << field.name |
|
477 | end |
||
478 | |||
479 | 293 | 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 | 2 | 'SET ' + @settings.map do |setting| |
|
0 ignored issues
–
show
|
|||
487 | 3 | value = maybe_quote setting.value, setting.field |
|
488 | 3 | "#{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 | 1 | def where_clause(field_namer = :to_s.to_proc) |
|
496 | 93 | ' WHERE ' + @conditions.values.map do |condition| |
|
0 ignored issues
–
show
|
|||
497 | 98 | value = condition.value.nil? ? '?' : condition.value |
|
498 | 98 | "#{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 | 1 | class FieldSetting |
|
505 | 1 | attr_reader :field, :value |
|
506 | |||
507 | 1 | def initialize(field, value) |
|
508 | 123 | @field = field |
|
509 | 123 | @value = value |
|
510 | |||
511 | 123 | 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 | 5 | other.field == @field |
|
521 | end |
||
522 | 1 | alias eql? == |
|
523 | |||
524 | # Hash by field and value |
||
525 | 1 | def hash |
|
526 | 109 | 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 | 40 | params[:settings] = tree[:settings].map do |setting| |
|
546 | 109 | field = params[:entity][setting[:field].to_s] |
|
547 | 109 | value = setting[:value] |
|
548 | |||
549 | 109 | type = field.class.const_get 'TYPE' |
|
550 | value = field.class.value_from_string(value.to_s) \ |
||
551 | 109 | unless type.nil? || value.nil? |
|
552 | |||
553 | 109 | setting.delete :value |
|
554 | 109 | 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 | 334 | !(@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 | 87 | return nil if select.empty? |
|
580 | |||
581 | params = { |
||
582 | 84 | select: select, |
|
583 | graph: graph, |
||
584 | key_path: graph.longest_path, |
||
585 | entity: key_path.first.parent, |
||
586 | conditions: conditions |
||
587 | } |
||
588 | |||
589 | 84 | support_query = SupportQuery.new entity, params, nil, group: @group |
|
590 | 84 | support_query.instance_variable_set :@statement, self |
|
591 | 84 | support_query.instance_variable_set :@index, index |
|
592 | 84 | support_query.instance_variable_set :@comment, (hash ^ index.hash).to_s |
|
593 | 84 | support_query.instance_variable_set :@text, support_query.unparse |
|
594 | 84 | support_query.hash |
|
595 | 84 | 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
|
|||
602 | 59 | graphs = index.graph.size > 1 ? index.graph.split(entity, true) : [] |
|
603 | |||
604 | 59 | 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 | conditions = { |
||
610 | 24 | 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 | 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 | 28 | graph.keys_from_entity(entity).find do |key| |
|
623 | 28 | subgraph.entities.include? key.entity |
|
624 | 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 |