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) |
|
|
|
|
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) |
|
|
|
|
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) |
|
|
|
|
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
|
|
|
|