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,747 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'representable'
|
4
|
+
require 'representable/json'
|
5
|
+
require 'representable/yaml'
|
6
|
+
|
7
|
+
# XXX Caching currently breaks the use of multiple formatting modules
|
8
|
+
# see https://github.com/apotonick/representable/issues/180
|
9
|
+
module Representable
|
10
|
+
# Break caching used by representable to allow multiple representers
|
11
|
+
module Uncached
|
12
|
+
# Create a simple binding which does not use caching
|
13
|
+
def representable_map(options, format)
|
14
|
+
Representable::Binding::Map.new(
|
15
|
+
representable_bindings_for(format, options)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module NoSE
|
22
|
+
# Serialization of workloads and statement execution plans
|
23
|
+
module Serialize
|
24
|
+
# Construct a field from a parsed hash
|
25
|
+
class FieldBuilder
|
26
|
+
include Uber::Callable
|
27
|
+
|
28
|
+
def call(_, fragment:, user_options:, **)
|
29
|
+
field_class = Fields::Field.subtype_class fragment['type']
|
30
|
+
|
31
|
+
# Extract the correct parameters and create a new field instance
|
32
|
+
if field_class == Fields::StringField && !fragment['size'].nil?
|
33
|
+
field = field_class.new fragment['name'], fragment['size']
|
34
|
+
elsif field_class.ancestors.include? Fields::ForeignKeyField
|
35
|
+
entity = user_options[:entity_map][fragment['entity']]
|
36
|
+
field = field_class.new fragment['name'], entity
|
37
|
+
else
|
38
|
+
field = field_class.new fragment['name']
|
39
|
+
end
|
40
|
+
|
41
|
+
field *= fragment['cardinality'] if fragment['cardinality']
|
42
|
+
|
43
|
+
field
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Represents a field just by the entity and name
|
48
|
+
class FieldRepresenter < Representable::Decorator
|
49
|
+
include Representable::Hash
|
50
|
+
include Representable::JSON
|
51
|
+
include Representable::YAML
|
52
|
+
include Representable::Uncached
|
53
|
+
|
54
|
+
property :name
|
55
|
+
|
56
|
+
# The name of the parent entity
|
57
|
+
def parent
|
58
|
+
represented.parent.name
|
59
|
+
end
|
60
|
+
property :parent, exec_context: :decorator
|
61
|
+
end
|
62
|
+
|
63
|
+
# Represents a graph by its nodes and edges
|
64
|
+
class GraphRepresenter < Representable::Decorator
|
65
|
+
include Representable::Hash
|
66
|
+
include Representable::JSON
|
67
|
+
include Representable::YAML
|
68
|
+
include Representable::Uncached
|
69
|
+
|
70
|
+
def nodes
|
71
|
+
represented.nodes.map { |n| n.entity.name }
|
72
|
+
end
|
73
|
+
|
74
|
+
property :nodes, exec_context: :decorator
|
75
|
+
|
76
|
+
def edges
|
77
|
+
represented.unique_edges.map do |edge|
|
78
|
+
FieldRepresenter.represent(edge.key).to_hash
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
property :edges, exec_context: :decorator
|
83
|
+
end
|
84
|
+
|
85
|
+
# Reconstruct indexes with fields from an existing workload
|
86
|
+
class IndexBuilder
|
87
|
+
include Uber::Callable
|
88
|
+
|
89
|
+
def call(_, represented:, fragment:, **)
|
90
|
+
# Extract the entities from the workload
|
91
|
+
model = represented.workload.model
|
92
|
+
|
93
|
+
# Pull the fields from each entity
|
94
|
+
f = lambda do |fields|
|
95
|
+
fields.map { |dict| model[dict['parent']][dict['name']] }
|
96
|
+
end
|
97
|
+
|
98
|
+
graph_entities = fragment['graph']['nodes'].map { |n| model[n] }
|
99
|
+
graph_keys = f.call(fragment['graph']['edges'])
|
100
|
+
graph = QueryGraph::Graph.new graph_entities
|
101
|
+
graph_keys.each { |k| graph.add_edge k.parent, k.entity, k }
|
102
|
+
|
103
|
+
Index.new f.call(fragment['hash_fields']),
|
104
|
+
f.call(fragment['order_fields']),
|
105
|
+
f.call(fragment['extra']), graph, fragment['key']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Represents a simple key for an index
|
110
|
+
class IndexRepresenter < Representable::Decorator
|
111
|
+
include Representable::Hash
|
112
|
+
include Representable::JSON
|
113
|
+
include Representable::YAML
|
114
|
+
include Representable::Uncached
|
115
|
+
|
116
|
+
property :key
|
117
|
+
end
|
118
|
+
|
119
|
+
# Represents index data along with the key
|
120
|
+
class FullIndexRepresenter < IndexRepresenter
|
121
|
+
collection :hash_fields, decorator: FieldRepresenter
|
122
|
+
collection :order_fields, decorator: FieldRepresenter
|
123
|
+
collection :extra, decorator: FieldRepresenter
|
124
|
+
|
125
|
+
property :graph, decorator: GraphRepresenter
|
126
|
+
property :entries
|
127
|
+
property :entry_size
|
128
|
+
property :size
|
129
|
+
property :hash_count
|
130
|
+
property :per_hash_count
|
131
|
+
end
|
132
|
+
|
133
|
+
# Represents all data of a field
|
134
|
+
class EntityFieldRepresenter < Representable::Decorator
|
135
|
+
include Representable::Hash
|
136
|
+
include Representable::JSON
|
137
|
+
include Representable::YAML
|
138
|
+
include Representable::Uncached
|
139
|
+
|
140
|
+
collection_representer class: Object, deserialize: FieldBuilder.new
|
141
|
+
|
142
|
+
property :name
|
143
|
+
property :size
|
144
|
+
property :cardinality
|
145
|
+
property :subtype_name, as: :type
|
146
|
+
|
147
|
+
# The entity name for foreign keys
|
148
|
+
# @return [String]
|
149
|
+
def entity
|
150
|
+
represented.entity.name \
|
151
|
+
if represented.is_a? Fields::ForeignKeyField
|
152
|
+
end
|
153
|
+
property :entity, exec_context: :decorator
|
154
|
+
|
155
|
+
# The cardinality of the relationship
|
156
|
+
# @return [Symbol]
|
157
|
+
def relationship
|
158
|
+
represented.relationship \
|
159
|
+
if represented.is_a? Fields::ForeignKeyField
|
160
|
+
end
|
161
|
+
property :relationship, exec_context: :decorator
|
162
|
+
|
163
|
+
# The reverse
|
164
|
+
# @return [String]
|
165
|
+
def reverse
|
166
|
+
represented.reverse.name \
|
167
|
+
if represented.is_a? Fields::ForeignKeyField
|
168
|
+
end
|
169
|
+
property :reverse, exec_context: :decorator
|
170
|
+
end
|
171
|
+
|
172
|
+
# Reconstruct the fields of an entity
|
173
|
+
class EntityBuilder
|
174
|
+
include Uber::Callable
|
175
|
+
|
176
|
+
def call(_, fragment:, user_options:, **)
|
177
|
+
# Pull the field from the map of all entities
|
178
|
+
entity_map = user_options[:entity_map]
|
179
|
+
entity = entity_map[fragment['name']]
|
180
|
+
|
181
|
+
# Add all fields from the entity
|
182
|
+
fields = EntityFieldRepresenter.represent([])
|
183
|
+
fields = fields.from_hash fragment['fields'],
|
184
|
+
user_options: { entity_map: entity_map }
|
185
|
+
fields.each { |field| entity.send(:<<, field, freeze: false) }
|
186
|
+
|
187
|
+
entity
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Represent the whole entity and its fields
|
192
|
+
class EntityRepresenter < Representable::Decorator
|
193
|
+
include Representable::Hash
|
194
|
+
include Representable::JSON
|
195
|
+
include Representable::YAML
|
196
|
+
include Representable::Uncached
|
197
|
+
|
198
|
+
collection_representer class: Object, deserialize: EntityBuilder.new
|
199
|
+
|
200
|
+
property :name
|
201
|
+
collection :fields, decorator: EntityFieldRepresenter,
|
202
|
+
exec_context: :decorator
|
203
|
+
property :count
|
204
|
+
|
205
|
+
# A simple array of the fields within the entity
|
206
|
+
def fields
|
207
|
+
represented.fields.values + represented.foreign_keys.values
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Conversion of a statement is just the text
|
212
|
+
class StatementRepresenter < Representable::Decorator
|
213
|
+
include Representable::Hash
|
214
|
+
include Representable::JSON
|
215
|
+
include Representable::YAML
|
216
|
+
include Representable::Uncached
|
217
|
+
|
218
|
+
# Represent as the text of the statement
|
219
|
+
def to_hash(*)
|
220
|
+
represented.text
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# Base representation for query plan steps
|
225
|
+
class PlanStepRepresenter < Representable::Decorator
|
226
|
+
include Representable::Hash
|
227
|
+
include Representable::JSON
|
228
|
+
include Representable::YAML
|
229
|
+
include Representable::Uncached
|
230
|
+
|
231
|
+
property :subtype_name, as: :type
|
232
|
+
property :cost
|
233
|
+
|
234
|
+
# The estimated cardinality at this step in the plan
|
235
|
+
def cardinality
|
236
|
+
state = represented.instance_variable_get(:@state)
|
237
|
+
state.cardinality unless state.nil?
|
238
|
+
end
|
239
|
+
property :cardinality, exec_context: :decorator
|
240
|
+
|
241
|
+
# The estimated hash cardinality at this step in the plan
|
242
|
+
# @return [Fixnum]
|
243
|
+
def hash_cardinality
|
244
|
+
state = represented.instance_variable_get(:@state)
|
245
|
+
state.hash_cardinality if state.is_a?(Plans::QueryState)
|
246
|
+
end
|
247
|
+
property :hash_cardinality, exec_context: :decorator
|
248
|
+
end
|
249
|
+
|
250
|
+
# Represent the index for index lookup plan steps
|
251
|
+
class IndexLookupStepRepresenter < PlanStepRepresenter
|
252
|
+
property :index, decorator: IndexRepresenter
|
253
|
+
collection :eq_filter, decorator: FieldRepresenter
|
254
|
+
property :range_filter, decorator: FieldRepresenter
|
255
|
+
collection :order_by, decorator: FieldRepresenter
|
256
|
+
property :limit
|
257
|
+
end
|
258
|
+
|
259
|
+
# Represent the filtered fields in filter plan steps
|
260
|
+
class FilterStepRepresenter < PlanStepRepresenter
|
261
|
+
collection :eq, decorator: FieldRepresenter
|
262
|
+
property :range, decorator: FieldRepresenter
|
263
|
+
end
|
264
|
+
|
265
|
+
# Represent the sorted fields in filter plan steps
|
266
|
+
class SortStepRepresenter < PlanStepRepresenter
|
267
|
+
collection :sort_fields, decorator: FieldRepresenter
|
268
|
+
end
|
269
|
+
|
270
|
+
# Represent the limit for limit plan steps
|
271
|
+
class LimitStepRepresenter < PlanStepRepresenter
|
272
|
+
property :limit
|
273
|
+
end
|
274
|
+
|
275
|
+
# Represent a query plan as a sequence of steps
|
276
|
+
class QueryPlanRepresenter < Representable::Decorator
|
277
|
+
include Representable::Hash
|
278
|
+
include Representable::JSON
|
279
|
+
include Representable::YAML
|
280
|
+
include Representable::Uncached
|
281
|
+
|
282
|
+
property :group
|
283
|
+
property :name
|
284
|
+
property :query, decorator: StatementRepresenter
|
285
|
+
property :cost
|
286
|
+
property :weight
|
287
|
+
collection :each, as: :steps, decorator: (lambda do |options|
|
288
|
+
{
|
289
|
+
index_lookup: IndexLookupStepRepresenter,
|
290
|
+
filter: FilterStepRepresenter,
|
291
|
+
sort: SortStepRepresenter,
|
292
|
+
limit: LimitStepRepresenter
|
293
|
+
}[options[:input].class.subtype_name.to_sym] || PlanStepRepresenter
|
294
|
+
end)
|
295
|
+
end
|
296
|
+
|
297
|
+
# Represent update plan steps
|
298
|
+
class UpdatePlanStepRepresenter < PlanStepRepresenter
|
299
|
+
property :index, decorator: IndexRepresenter
|
300
|
+
collection :fields, decorator: FieldRepresenter
|
301
|
+
|
302
|
+
# Set the hidden type variable
|
303
|
+
# @return [Symbol]
|
304
|
+
def type
|
305
|
+
represented.instance_variable_get(:@type)
|
306
|
+
end
|
307
|
+
|
308
|
+
# Set the hidden type variable
|
309
|
+
# @return [void]
|
310
|
+
def type=(type)
|
311
|
+
represented.instance_variable_set(:@type, type)
|
312
|
+
end
|
313
|
+
|
314
|
+
property :type, exec_context: :decorator
|
315
|
+
|
316
|
+
# The estimated cardinality of entities being updated
|
317
|
+
# @return [Fixnum]
|
318
|
+
def cardinality
|
319
|
+
state = represented.instance_variable_get(:@state)
|
320
|
+
state.cardinality unless state.nil?
|
321
|
+
end
|
322
|
+
|
323
|
+
property :cardinality, exec_context: :decorator
|
324
|
+
end
|
325
|
+
|
326
|
+
# Represent an update plan
|
327
|
+
class UpdatePlanRepresenter < Representable::Decorator
|
328
|
+
include Representable::Hash
|
329
|
+
include Representable::JSON
|
330
|
+
include Representable::YAML
|
331
|
+
include Representable::Uncached
|
332
|
+
|
333
|
+
property :group
|
334
|
+
property :name
|
335
|
+
property :cost
|
336
|
+
property :update_cost
|
337
|
+
property :weight
|
338
|
+
property :statement, decorator: StatementRepresenter
|
339
|
+
property :index, decorator: IndexRepresenter
|
340
|
+
collection :query_plans, decorator: QueryPlanRepresenter, class: Object
|
341
|
+
collection :update_steps, decorator: UpdatePlanStepRepresenter
|
342
|
+
|
343
|
+
# The backend cost model used to cost the updates
|
344
|
+
# @return [Cost::Cost]
|
345
|
+
def cost_model
|
346
|
+
options = represented.cost_model.instance_variable_get(:@options)
|
347
|
+
options[:name] = represented.cost_model.subtype_name
|
348
|
+
options
|
349
|
+
end
|
350
|
+
|
351
|
+
# Look up the cost model by name and attach to the results
|
352
|
+
# @return [void]
|
353
|
+
def cost_model=(options)
|
354
|
+
options = options.deep_symbolize_keys
|
355
|
+
cost_model_class = Cost::Cost.subtype_class(options[:name])
|
356
|
+
represented.cost_model = cost_model_class.new(**options)
|
357
|
+
end
|
358
|
+
|
359
|
+
property :cost_model, exec_context: :decorator
|
360
|
+
end
|
361
|
+
|
362
|
+
# Reconstruct the steps of an update plan
|
363
|
+
class UpdatePlanBuilder
|
364
|
+
include Uber::Callable
|
365
|
+
|
366
|
+
def call(_, fragment:, represented:, **)
|
367
|
+
workload = represented.workload
|
368
|
+
|
369
|
+
if fragment['statement'].nil?
|
370
|
+
statement = OpenStruct.new group: fragment['group']
|
371
|
+
else
|
372
|
+
statement = Statement.parse fragment['statement'], workload.model,
|
373
|
+
group: fragment['group']
|
374
|
+
end
|
375
|
+
|
376
|
+
update_steps = fragment['update_steps'].map do |step_hash|
|
377
|
+
step_class = Plans::PlanStep.subtype_class step_hash['type']
|
378
|
+
index_key = step_hash['index']['key']
|
379
|
+
step_index = represented.indexes.find { |i| i.key == index_key }
|
380
|
+
|
381
|
+
if statement.nil?
|
382
|
+
state = nil
|
383
|
+
else
|
384
|
+
state = Plans::UpdateState.new statement, step_hash['cardinality']
|
385
|
+
end
|
386
|
+
step = step_class.new step_index, state
|
387
|
+
|
388
|
+
# Set the fields to be inserted
|
389
|
+
fields = (step_hash['fields'] || []).map do |dict|
|
390
|
+
workload.model[dict['parent']][dict['name']]
|
391
|
+
end
|
392
|
+
step.instance_variable_set(:@fields, fields) \
|
393
|
+
if step.is_a?(Plans::InsertPlanStep)
|
394
|
+
|
395
|
+
step
|
396
|
+
end
|
397
|
+
|
398
|
+
index_key = fragment['index']['key']
|
399
|
+
index = represented.indexes.find { |i| i.key == index_key }
|
400
|
+
update_plan = Plans::UpdatePlan.new statement, index, [], update_steps,
|
401
|
+
represented.cost_model
|
402
|
+
|
403
|
+
update_plan.instance_variable_set(:@group, fragment['group']) \
|
404
|
+
unless fragment['group'].nil?
|
405
|
+
update_plan.instance_variable_set(:@name, fragment['name']) \
|
406
|
+
unless fragment['name'].nil?
|
407
|
+
update_plan.instance_variable_set(:@weight, fragment['weight'])
|
408
|
+
|
409
|
+
# Reconstruct and assign the query plans
|
410
|
+
builder = QueryPlanBuilder.new
|
411
|
+
query_plans = fragment['query_plans'].map do |plan|
|
412
|
+
builder.call [], represented: represented, fragment: plan
|
413
|
+
end
|
414
|
+
update_plan.instance_variable_set(:@query_plans, query_plans)
|
415
|
+
update_plan.send :update_support_fields
|
416
|
+
|
417
|
+
update_plan
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Represent statements in a workload
|
422
|
+
class WorkloadRepresenter < Representable::Decorator
|
423
|
+
include Representable::Hash
|
424
|
+
include Representable::JSON
|
425
|
+
include Representable::YAML
|
426
|
+
include Representable::Uncached
|
427
|
+
|
428
|
+
collection :statements, decorator: StatementRepresenter
|
429
|
+
property :mix
|
430
|
+
|
431
|
+
# Produce weights of each statement in the workload for each mix
|
432
|
+
# @return [Hash]
|
433
|
+
def weights
|
434
|
+
weights = {}
|
435
|
+
workload_weights = represented \
|
436
|
+
.instance_variable_get(:@statement_weights)
|
437
|
+
workload_weights.each do |mix, mix_weights|
|
438
|
+
weights[mix] = {}
|
439
|
+
mix_weights.each do |statement, weight|
|
440
|
+
statement = StatementRepresenter.represent(statement).to_hash
|
441
|
+
weights[mix][statement] = weight
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
weights
|
446
|
+
end
|
447
|
+
property :weights, exec_context: :decorator
|
448
|
+
end
|
449
|
+
|
450
|
+
# Represent entities in a model
|
451
|
+
class ModelRepresenter < Representable::Decorator
|
452
|
+
include Representable::Hash
|
453
|
+
include Representable::JSON
|
454
|
+
include Representable::YAML
|
455
|
+
include Representable::Uncached
|
456
|
+
|
457
|
+
# A simple array of the entities in the model
|
458
|
+
# @return [Array<Entity>]
|
459
|
+
def entities
|
460
|
+
represented.entities.values
|
461
|
+
end
|
462
|
+
collection :entities, decorator: EntityRepresenter,
|
463
|
+
exec_context: :decorator
|
464
|
+
end
|
465
|
+
|
466
|
+
# Construct a new workload from a parsed hash
|
467
|
+
class WorkloadBuilder
|
468
|
+
include Uber::Callable
|
469
|
+
|
470
|
+
def call(_, input:, fragment:, represented:, **)
|
471
|
+
workload = input.represented
|
472
|
+
workload.instance_variable_set :@model, represented.model
|
473
|
+
|
474
|
+
# Add all statements to the workload
|
475
|
+
statement_weights = Hash.new { |h, k| h[k] = {} }
|
476
|
+
fragment['weights'].each do |mix, weights|
|
477
|
+
mix = mix.to_sym
|
478
|
+
weights.each do |statement, weight|
|
479
|
+
statement_weights[statement][mix] = weight
|
480
|
+
end
|
481
|
+
end
|
482
|
+
fragment['statements'].each do |statement|
|
483
|
+
workload.add_statement statement, statement_weights[statement],
|
484
|
+
group: fragment['group']
|
485
|
+
end
|
486
|
+
|
487
|
+
workload.mix = fragment['mix'].to_sym unless fragment['mix'].nil?
|
488
|
+
|
489
|
+
workload
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
class ModelBuilder
|
494
|
+
include Uber::Callable
|
495
|
+
|
496
|
+
def call(_, input:, fragment:, **)
|
497
|
+
model = input.represented
|
498
|
+
entity_map = add_entities model, fragment['entities']
|
499
|
+
add_reverse_foreign_keys entity_map, fragment['entities']
|
500
|
+
|
501
|
+
model
|
502
|
+
end
|
503
|
+
|
504
|
+
private
|
505
|
+
|
506
|
+
# Reconstruct entities and add them to the given model
|
507
|
+
def add_entities(model, entity_fragment)
|
508
|
+
# Recreate all the entities
|
509
|
+
entity_map = {}
|
510
|
+
entity_fragment.each do |entity_hash|
|
511
|
+
entity_map[entity_hash['name']] = Entity.new entity_hash['name']
|
512
|
+
end
|
513
|
+
|
514
|
+
# Populate the entities and add them to the workload
|
515
|
+
entities = EntityRepresenter.represent([])
|
516
|
+
entities = entities.from_hash entity_fragment,
|
517
|
+
user_options: { entity_map: entity_map }
|
518
|
+
entities.each { |entity| model.add_entity entity }
|
519
|
+
|
520
|
+
entity_map
|
521
|
+
end
|
522
|
+
|
523
|
+
# Add all the reverse foreign keys
|
524
|
+
# @return [void]
|
525
|
+
def add_reverse_foreign_keys(entity_map, entity_fragment)
|
526
|
+
entity_fragment.each do |entity|
|
527
|
+
entity['fields'].each do |field_hash|
|
528
|
+
if field_hash['type'] == 'foreign_key'
|
529
|
+
field = entity_map[entity['name']] \
|
530
|
+
.foreign_keys[field_hash['name']]
|
531
|
+
field.reverse = field.entity.foreign_keys[field_hash['reverse']]
|
532
|
+
field.instance_variable_set :@relationship,
|
533
|
+
field_hash['relationship'].to_sym
|
534
|
+
end
|
535
|
+
field.freeze
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
# Reconstruct the steps of a query plan
|
542
|
+
class QueryPlanBuilder
|
543
|
+
include Uber::Callable
|
544
|
+
|
545
|
+
def call(_, represented:, fragment:, **)
|
546
|
+
workload = represented.workload
|
547
|
+
|
548
|
+
if fragment['query'].nil?
|
549
|
+
query = OpenStruct.new group: fragment['group']
|
550
|
+
state = nil
|
551
|
+
else
|
552
|
+
query = Statement.parse fragment['query'], workload.model,
|
553
|
+
group: fragment['group']
|
554
|
+
state = Plans::QueryState.new query, workload
|
555
|
+
end
|
556
|
+
|
557
|
+
plan = build_plan query, represented.cost_model, fragment
|
558
|
+
add_plan_steps plan, workload, fragment['steps'], represented.indexes,
|
559
|
+
state
|
560
|
+
|
561
|
+
plan
|
562
|
+
end
|
563
|
+
|
564
|
+
private
|
565
|
+
|
566
|
+
# Build a new query plan
|
567
|
+
# @return [Plans::QueryPlan]
|
568
|
+
def build_plan(query, cost_model, fragment)
|
569
|
+
plan = Plans::QueryPlan.new query, cost_model
|
570
|
+
|
571
|
+
plan.instance_variable_set(:@name, fragment['name']) \
|
572
|
+
unless fragment['name'].nil?
|
573
|
+
plan.instance_variable_set(:@weight, fragment['weight'])
|
574
|
+
|
575
|
+
plan
|
576
|
+
end
|
577
|
+
|
578
|
+
# Loop over all steps in the plan and reconstruct them
|
579
|
+
# @return [void]
|
580
|
+
def add_plan_steps(plan, workload, steps_fragment, indexes, state)
|
581
|
+
parent = Plans::RootPlanStep.new state
|
582
|
+
f = ->(field) { workload.model[field['parent']][field['name']] }
|
583
|
+
|
584
|
+
steps_fragment.each do |step_hash|
|
585
|
+
step = build_step step_hash, state, parent, indexes, f
|
586
|
+
rebuild_step_state step, step_hash
|
587
|
+
plan << step
|
588
|
+
parent = step
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
# Rebuild a step from a hash using the given set of indexes
|
593
|
+
# The final parameter is a function which maps field names to instances
|
594
|
+
# @return [Plans::PlanStep]
|
595
|
+
def build_step(step_hash, state, parent, indexes, f)
|
596
|
+
send "build_#{step_hash['type']}_step".to_sym,
|
597
|
+
step_hash, state, parent, indexes, f
|
598
|
+
end
|
599
|
+
|
600
|
+
# Rebuild a limit step
|
601
|
+
# @return [Plans::LimitPlanStep]
|
602
|
+
def build_limit_step(step_hash, _state, parent, _indexes, _f)
|
603
|
+
limit = step_hash['limit'].to_i
|
604
|
+
Plans::LimitPlanStep.new limit, parent.state
|
605
|
+
end
|
606
|
+
|
607
|
+
# Rebuild a sort step
|
608
|
+
# @return [Plans::SortPlanStep]
|
609
|
+
def build_sort_step(step_hash, _state, parent, _indexes, f)
|
610
|
+
sort_fields = step_hash['sort_fields'].map(&f)
|
611
|
+
Plans::SortPlanStep.new sort_fields, parent.state
|
612
|
+
end
|
613
|
+
|
614
|
+
# Rebuild a filter step
|
615
|
+
# @return [Plans::FilterPlanStep]
|
616
|
+
def build_filter_step(step_hash, _state, parent, _indexes, f)
|
617
|
+
eq = step_hash['eq'].map(&f)
|
618
|
+
range = f.call(step_hash['range']) if step_hash['range']
|
619
|
+
Plans::FilterPlanStep.new eq, range, parent.state
|
620
|
+
end
|
621
|
+
|
622
|
+
# Rebuild an index lookup step
|
623
|
+
# @return [Plans::IndexLookupPlanStep]
|
624
|
+
def build_index_lookup_step(step_hash, state, parent, indexes, f)
|
625
|
+
index_key = step_hash['index']['key']
|
626
|
+
step_index = indexes.find { |i| i.key == index_key }
|
627
|
+
step = Plans::IndexLookupPlanStep.new step_index, state, parent
|
628
|
+
add_index_lookup_filters step, step_hash, f
|
629
|
+
|
630
|
+
order_by = (step_hash['order_by'] || []).map(&f)
|
631
|
+
step.instance_variable_set(:@order_by, order_by)
|
632
|
+
|
633
|
+
limit = step_hash['limit']
|
634
|
+
step.instance_variable_set(:@limit, limit.to_i) unless limit.nil?
|
635
|
+
|
636
|
+
step
|
637
|
+
end
|
638
|
+
|
639
|
+
# Add filters to a constructed index lookup step
|
640
|
+
# @return [void]
|
641
|
+
def add_index_lookup_filters(step, step_hash, f)
|
642
|
+
eq_filter = (step_hash['eq_filter'] || []).map(&f)
|
643
|
+
step.instance_variable_set(:@eq_filter, eq_filter)
|
644
|
+
|
645
|
+
range_filter = step_hash['range_filter']
|
646
|
+
range_filter = f.call(range_filter) unless range_filter.nil?
|
647
|
+
step.instance_variable_set(:@range_filter, range_filter)
|
648
|
+
end
|
649
|
+
|
650
|
+
# Rebuild the state of the step from the provided hash
|
651
|
+
# @return [void]
|
652
|
+
def rebuild_step_state(step, step_hash)
|
653
|
+
return if step.state.nil?
|
654
|
+
|
655
|
+
# Copy the correct cardinality
|
656
|
+
# XXX This may not preserve all the necessary state
|
657
|
+
state = step.state.dup
|
658
|
+
state.instance_variable_set :@cardinality, step_hash['cardinality']
|
659
|
+
step.instance_variable_set :@cost, step_hash['cost']
|
660
|
+
step.state = state.freeze
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
# Represent results of a search operation
|
665
|
+
class SearchResultRepresenter < Representable::Decorator
|
666
|
+
include Representable::Hash
|
667
|
+
include Representable::JSON
|
668
|
+
include Representable::YAML
|
669
|
+
include Representable::Uncached
|
670
|
+
|
671
|
+
extend Forwardable
|
672
|
+
|
673
|
+
delegate :revision= => :represented
|
674
|
+
delegate :command= => :represented
|
675
|
+
|
676
|
+
property :model, decorator: ModelRepresenter,
|
677
|
+
class: Model,
|
678
|
+
deserialize: ModelBuilder.new
|
679
|
+
property :workload, decorator: WorkloadRepresenter,
|
680
|
+
class: Workload,
|
681
|
+
deserialize: WorkloadBuilder.new
|
682
|
+
collection :indexes, decorator: FullIndexRepresenter,
|
683
|
+
class: Object,
|
684
|
+
deserialize: IndexBuilder.new
|
685
|
+
collection :enumerated_indexes, decorator: FullIndexRepresenter,
|
686
|
+
class: Object,
|
687
|
+
deserialize: IndexBuilder.new
|
688
|
+
|
689
|
+
# The backend cost model used to generate the schema
|
690
|
+
# @return [Hash]
|
691
|
+
def cost_model
|
692
|
+
options = represented.cost_model.instance_variable_get(:@options)
|
693
|
+
options[:name] = represented.cost_model.subtype_name
|
694
|
+
options
|
695
|
+
end
|
696
|
+
|
697
|
+
# Look up the cost model by name and attach to the results
|
698
|
+
# @return [void]
|
699
|
+
def cost_model=(options)
|
700
|
+
options = options.deep_symbolize_keys
|
701
|
+
cost_model_class = Cost::Cost.subtype_class(options[:name])
|
702
|
+
represented.cost_model = cost_model_class.new(**options)
|
703
|
+
end
|
704
|
+
|
705
|
+
property :cost_model, exec_context: :decorator
|
706
|
+
|
707
|
+
collection :plans, decorator: QueryPlanRepresenter,
|
708
|
+
class: Object,
|
709
|
+
deserialize: QueryPlanBuilder.new
|
710
|
+
collection :update_plans, decorator: UpdatePlanRepresenter,
|
711
|
+
class: Object,
|
712
|
+
deserialize: UpdatePlanBuilder.new
|
713
|
+
property :total_size
|
714
|
+
property :total_cost
|
715
|
+
|
716
|
+
# Include the revision of the code used to generate this output
|
717
|
+
# @return [String]
|
718
|
+
def revision
|
719
|
+
`git rev-parse HEAD 2> /dev/null`.strip
|
720
|
+
end
|
721
|
+
|
722
|
+
property :revision, exec_context: :decorator
|
723
|
+
|
724
|
+
# The time the results were generated
|
725
|
+
# @return [Time]
|
726
|
+
def time
|
727
|
+
Time.now.rfc2822
|
728
|
+
end
|
729
|
+
|
730
|
+
# Reconstruct the time object from the timestamp
|
731
|
+
# @return [void]
|
732
|
+
def time=(time)
|
733
|
+
represented.time = Time.rfc2822 time
|
734
|
+
end
|
735
|
+
|
736
|
+
property :time, exec_context: :decorator
|
737
|
+
|
738
|
+
# The full command used to generate the results
|
739
|
+
# @return [String]
|
740
|
+
def command
|
741
|
+
"#{$PROGRAM_NAME} #{ARGV.join ' '}"
|
742
|
+
end
|
743
|
+
|
744
|
+
property :command, exec_context: :decorator
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|