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