Issues (393)

lib/nose/serialize.rb (18 issues)

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
The Assignment, Branch, Condition size for call is considered too high. [<7, 19, 6> 21.12/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
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
The Assignment, Branch, Condition size for call is considered too high. [<9, 28, 3> 29.56/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
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
Use safe navigation (&.) instead of checking if an object exists before calling the method.
Loading history...
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
Use safe navigation (&.) instead of checking if an object exists before calling the method.
Loading history...
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
This method is 40 lines long. Your coding style permits a maximum length of 20.
Loading history...
The Assignment, Branch, Condition size for call is considered too high. [<21, 54, 15> 59.85/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
The method call seems to be too complex. Perceived cyclomatic complexity is 12 with a maxiumum of 10 permitted.
Loading history...
Complexity Coding Style introduced by
The method call seems to be too complex. Perceived complexity is 14 with a maxiumum of 10 permitted.
Loading history...
379
        workload = represented.workload
380
381
        if fragment['statement'].nil?
0 ignored issues
show
Use the return of the conditional for variable assignment and comparison.
Loading history...
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
Use the return of the conditional for variable assignment and comparison.
Loading history...
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
The Assignment, Branch, Condition size for call is considered too high. [<13, 21, 4> 25.02/20]. The ABC size is based on assignments, branches (method calls), and conditions.
Loading history...
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
Missing top-level documentation comment for class NoSE::Serialize::ModelBuilder.
Loading history...
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
Method parameter must be at least 3 characters long.
Loading history...
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
Method parameter must be at least 3 characters long.
Loading history...
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
Method parameter must be at least 3 characters long.
Loading history...
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
Method parameter must be at least 3 characters long.
Loading history...
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
Method parameter must be at least 3 characters long.
Loading history...
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
Method parameter must be at least 3 characters long.
Loading history...
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