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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/lib/nose/backend/cassandra.rb +390 -0
  3. data/lib/nose/backend/file.rb +185 -0
  4. data/lib/nose/backend/mongo.rb +242 -0
  5. data/lib/nose/backend.rb +557 -0
  6. data/lib/nose/cost/cassandra.rb +33 -0
  7. data/lib/nose/cost/entity_count.rb +27 -0
  8. data/lib/nose/cost/field_size.rb +31 -0
  9. data/lib/nose/cost/request_count.rb +32 -0
  10. data/lib/nose/cost.rb +68 -0
  11. data/lib/nose/debug.rb +45 -0
  12. data/lib/nose/enumerator.rb +199 -0
  13. data/lib/nose/indexes.rb +239 -0
  14. data/lib/nose/loader/csv.rb +99 -0
  15. data/lib/nose/loader/mysql.rb +199 -0
  16. data/lib/nose/loader/random.rb +48 -0
  17. data/lib/nose/loader/sql.rb +105 -0
  18. data/lib/nose/loader.rb +38 -0
  19. data/lib/nose/model/entity.rb +136 -0
  20. data/lib/nose/model/fields.rb +293 -0
  21. data/lib/nose/model.rb +113 -0
  22. data/lib/nose/parser.rb +202 -0
  23. data/lib/nose/plans/execution_plan.rb +282 -0
  24. data/lib/nose/plans/filter.rb +99 -0
  25. data/lib/nose/plans/index_lookup.rb +302 -0
  26. data/lib/nose/plans/limit.rb +42 -0
  27. data/lib/nose/plans/query_planner.rb +361 -0
  28. data/lib/nose/plans/sort.rb +49 -0
  29. data/lib/nose/plans/update.rb +60 -0
  30. data/lib/nose/plans/update_planner.rb +270 -0
  31. data/lib/nose/plans.rb +135 -0
  32. data/lib/nose/proxy/mysql.rb +275 -0
  33. data/lib/nose/proxy.rb +102 -0
  34. data/lib/nose/query_graph.rb +481 -0
  35. data/lib/nose/random/barbasi_albert.rb +48 -0
  36. data/lib/nose/random/watts_strogatz.rb +50 -0
  37. data/lib/nose/random.rb +391 -0
  38. data/lib/nose/schema.rb +89 -0
  39. data/lib/nose/search/constraints.rb +143 -0
  40. data/lib/nose/search/problem.rb +328 -0
  41. data/lib/nose/search/results.rb +200 -0
  42. data/lib/nose/search.rb +266 -0
  43. data/lib/nose/serialize.rb +747 -0
  44. data/lib/nose/statements/connection.rb +160 -0
  45. data/lib/nose/statements/delete.rb +83 -0
  46. data/lib/nose/statements/insert.rb +146 -0
  47. data/lib/nose/statements/query.rb +161 -0
  48. data/lib/nose/statements/update.rb +101 -0
  49. data/lib/nose/statements.rb +645 -0
  50. data/lib/nose/timing.rb +79 -0
  51. data/lib/nose/util.rb +305 -0
  52. data/lib/nose/workload.rb +244 -0
  53. data/lib/nose.rb +37 -0
  54. data/templates/workload.erb +42 -0
  55. 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