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