nose 0.1.0pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|