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