michaelmior /
NoSE
| 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
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
|
|||
| 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 |