nose 0.1.0pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/nose/backend/cassandra.rb +390 -0
- data/lib/nose/backend/file.rb +185 -0
- data/lib/nose/backend/mongo.rb +242 -0
- data/lib/nose/backend.rb +557 -0
- data/lib/nose/cost/cassandra.rb +33 -0
- data/lib/nose/cost/entity_count.rb +27 -0
- data/lib/nose/cost/field_size.rb +31 -0
- data/lib/nose/cost/request_count.rb +32 -0
- data/lib/nose/cost.rb +68 -0
- data/lib/nose/debug.rb +45 -0
- data/lib/nose/enumerator.rb +199 -0
- data/lib/nose/indexes.rb +239 -0
- data/lib/nose/loader/csv.rb +99 -0
- data/lib/nose/loader/mysql.rb +199 -0
- data/lib/nose/loader/random.rb +48 -0
- data/lib/nose/loader/sql.rb +105 -0
- data/lib/nose/loader.rb +38 -0
- data/lib/nose/model/entity.rb +136 -0
- data/lib/nose/model/fields.rb +293 -0
- data/lib/nose/model.rb +113 -0
- data/lib/nose/parser.rb +202 -0
- data/lib/nose/plans/execution_plan.rb +282 -0
- data/lib/nose/plans/filter.rb +99 -0
- data/lib/nose/plans/index_lookup.rb +302 -0
- data/lib/nose/plans/limit.rb +42 -0
- data/lib/nose/plans/query_planner.rb +361 -0
- data/lib/nose/plans/sort.rb +49 -0
- data/lib/nose/plans/update.rb +60 -0
- data/lib/nose/plans/update_planner.rb +270 -0
- data/lib/nose/plans.rb +135 -0
- data/lib/nose/proxy/mysql.rb +275 -0
- data/lib/nose/proxy.rb +102 -0
- data/lib/nose/query_graph.rb +481 -0
- data/lib/nose/random/barbasi_albert.rb +48 -0
- data/lib/nose/random/watts_strogatz.rb +50 -0
- data/lib/nose/random.rb +391 -0
- data/lib/nose/schema.rb +89 -0
- data/lib/nose/search/constraints.rb +143 -0
- data/lib/nose/search/problem.rb +328 -0
- data/lib/nose/search/results.rb +200 -0
- data/lib/nose/search.rb +266 -0
- data/lib/nose/serialize.rb +747 -0
- data/lib/nose/statements/connection.rb +160 -0
- data/lib/nose/statements/delete.rb +83 -0
- data/lib/nose/statements/insert.rb +146 -0
- data/lib/nose/statements/query.rb +161 -0
- data/lib/nose/statements/update.rb +101 -0
- data/lib/nose/statements.rb +645 -0
- data/lib/nose/timing.rb +79 -0
- data/lib/nose/util.rb +305 -0
- data/lib/nose/workload.rb +244 -0
- data/lib/nose.rb +37 -0
- data/templates/workload.erb +42 -0
- metadata +700 -0
@@ -0,0 +1,645 @@
|
|
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).all? do |prev_key, key|
|
117
|
+
key.parent == prev_key.entity
|
118
|
+
end
|
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'
|
data/lib/nose/timing.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NoSE
|
4
|
+
# Tracks the runtime of various functions and outputs a measurement
|
5
|
+
class Timer
|
6
|
+
# Start tracking function runtime
|
7
|
+
# @return [void]
|
8
|
+
def self.enable
|
9
|
+
traced = {
|
10
|
+
IndexEnumerator => [
|
11
|
+
:indexes_for_workload,
|
12
|
+
:support_indexes,
|
13
|
+
:combine_indexes
|
14
|
+
],
|
15
|
+
Search::Search => [
|
16
|
+
:query_costs,
|
17
|
+
:update_costs,
|
18
|
+
:search_overlap,
|
19
|
+
:solve_mipper
|
20
|
+
],
|
21
|
+
Search::Problem => [
|
22
|
+
:setup_model,
|
23
|
+
:add_variables,
|
24
|
+
:add_constraints,
|
25
|
+
:define_objective,
|
26
|
+
:total_cost,
|
27
|
+
:add_update_costs,
|
28
|
+
:total_size,
|
29
|
+
:total_indexes,
|
30
|
+
:solve
|
31
|
+
],
|
32
|
+
MIPPeR::CbcModel => [
|
33
|
+
:add_constraints,
|
34
|
+
:add_variables,
|
35
|
+
:update,
|
36
|
+
:optimize
|
37
|
+
]
|
38
|
+
}
|
39
|
+
@old_methods = Hash.new { |h, k| h[k] = {} }
|
40
|
+
|
41
|
+
# Redefine each method to capture timing information on each call
|
42
|
+
traced.each do |cls, methods|
|
43
|
+
methods.each do |method|
|
44
|
+
old_method = cls.instance_method(method)
|
45
|
+
cls.send(:define_method, method) do |*args|
|
46
|
+
$stderr.puts "#{cls}##{method}\tSTART"
|
47
|
+
|
48
|
+
start = Time.now.utc
|
49
|
+
result = old_method.bind(self).call(*args)
|
50
|
+
elapsed = Time.now.utc - start
|
51
|
+
|
52
|
+
# Allow a block to be called with the timing results
|
53
|
+
yield cls, method, elapsed if block_given?
|
54
|
+
|
55
|
+
$stderr.puts "#{cls}##{method}\tEND\t#{elapsed}"
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
# Save a copy of the old method for later
|
61
|
+
@old_methods[cls][method] = old_method
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Stop tracking function runtime
|
67
|
+
# @return [void]
|
68
|
+
def self.disable
|
69
|
+
@old_methods.each do |cls, methods|
|
70
|
+
methods.each do |method, old_method|
|
71
|
+
cls.send(:define_method, method, old_method)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Remove the saved method definitions
|
76
|
+
@old_methods.clear
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|