martyr 0.1.74.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.tags +868 -0
  7. data/.travis.yml +3 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +265 -0
  11. data/Rakefile +1 -0
  12. data/TODO.txt +54 -0
  13. data/bin/console +62 -0
  14. data/bin/setup +7 -0
  15. data/lib/martyr/base_cube.rb +73 -0
  16. data/lib/martyr/cube.rb +134 -0
  17. data/lib/martyr/dimension_reference.rb +26 -0
  18. data/lib/martyr/errors.rb +20 -0
  19. data/lib/martyr/helpers/delegators.rb +17 -0
  20. data/lib/martyr/helpers/intervals.rb +222 -0
  21. data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
  22. data/lib/martyr/helpers/registrable.rb +15 -0
  23. data/lib/martyr/helpers/sorter.rb +79 -0
  24. data/lib/martyr/helpers/translations.rb +34 -0
  25. data/lib/martyr/level_concern/has_level_collection.rb +11 -0
  26. data/lib/martyr/level_concern/level.rb +45 -0
  27. data/lib/martyr/level_concern/level_collection.rb +60 -0
  28. data/lib/martyr/level_concern/level_comparator.rb +45 -0
  29. data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
  30. data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
  31. data/lib/martyr/runtime/data_set/element.rb +66 -0
  32. data/lib/martyr/runtime/data_set/element_common.rb +51 -0
  33. data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
  34. data/lib/martyr/runtime/data_set/fact.rb +83 -0
  35. data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
  36. data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
  37. data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
  38. data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
  39. data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
  40. data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
  41. data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
  42. data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
  43. data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
  44. data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
  45. data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
  46. data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
  47. data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
  48. data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
  49. data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
  50. data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
  51. data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
  52. data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
  53. data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
  54. data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
  55. data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
  56. data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
  57. data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
  58. data/lib/martyr/runtime/query/query_context.rb +246 -0
  59. data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
  60. data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
  61. data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
  62. data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
  63. data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
  64. data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
  65. data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
  66. data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
  67. data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
  68. data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
  69. data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
  70. data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
  71. data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
  72. data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
  73. data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
  74. data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
  75. data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
  76. data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
  77. data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
  78. data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
  79. data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
  80. data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
  81. data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
  82. data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
  83. data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
  84. data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
  85. data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
  86. data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
  87. data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
  88. data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
  89. data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
  90. data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
  91. data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
  92. data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
  93. data/lib/martyr/schema/metrics/base_metric.rb +77 -0
  94. data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
  95. data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
  96. data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
  97. data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
  98. data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
  99. data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
  100. data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
  101. data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
  102. data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
  103. data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
  104. data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
  105. data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
  106. data/lib/martyr/version.rb +3 -0
  107. data/lib/martyr/virtual_cube.rb +74 -0
  108. data/lib/martyr.rb +55 -0
  109. data/martyr.gemspec +41 -0
  110. metadata +296 -0
@@ -0,0 +1,109 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PivotTable
4
+ include ActiveModel::Model
5
+
6
+ # @attribute metrics [Array<BaseMetric>]
7
+ # @attribute row_axis [PivotAxis]
8
+ # @attribute column_axis [PivotAxis]
9
+ # @attribute pivot_grain [Array<String>] array of level ids
10
+ attr_accessor :metrics, :row_axis, :column_axis, :pivot_grain, :row_totals, :column_totals, :sort
11
+ attr_reader :elements
12
+
13
+ def initialize(query_context, *args)
14
+ super(*args)
15
+
16
+ # We don't restrict metrics since custom rollups may have dependencies
17
+ @elements = query_context.elements(levels: pivot_grain, sort: sort)
18
+ end
19
+
20
+ def reload
21
+ @_cells = nil
22
+ @_lowest_cells = nil
23
+ row_axis.load_values(cells, reset: true)
24
+ column_axis.load_values(cells, reset: true)
25
+ self
26
+ end
27
+
28
+ def inspect
29
+ {metrics: metrics.map(&:id), levels: pivot_grain, on_rows: row_axis, on_columns: column_axis, totals: {rows: row_totals, columns: column_totals}}.inspect
30
+ end
31
+
32
+ def cells
33
+ @_cells ||= sort_cells(lowest_cells + sub_totals)
34
+ end
35
+
36
+ def lowest_cells
37
+ @_lowest_cells ||= metrics.flat_map do |metric|
38
+ elements.map do |element|
39
+ PivotCell.new metric, element
40
+ end
41
+ end
42
+ end
43
+
44
+ def sub_totals
45
+ resettable_grain = row_totals ? row_axis.ids : []
46
+ resettable_grain += column_totals ? column_axis.ids : []
47
+ resettable_grain -= [:metrics]
48
+ (0...resettable_grain.length).flat_map do |x|
49
+ reset = resettable_grain[x..-1]
50
+ elements.index_by do |element|
51
+ (pivot_grain - reset).map{|level_id| element[level_id]}
52
+ end.flat_map do |_sub_total_key, representative|
53
+ element = representative.locate reset: reset
54
+ metrics.map do |metric|
55
+ PivotCell.new(metric, element, reset)
56
+ end
57
+ end.reject(:empty?).compact
58
+ end
59
+ end
60
+
61
+ def transpose
62
+ self.row_axis, self.column_axis = column_axis, row_axis
63
+ reload
64
+ end
65
+
66
+ def to_chart(name: nil)
67
+ cells.group_by do |cell|
68
+ cell.to_axis_values(row_axis)
69
+ end.map do |row_grain_values, cells|
70
+ data = cells.inject(column_axis.flat_values_nil_hash) { |h, cell| h.merge! cell.to_axis_values(column_axis) => cell.value }
71
+ {name: name || row_grain_values, data: data}
72
+ end
73
+ end
74
+
75
+ def to_csv
76
+ CSV.generate do |csv|
77
+ column_axis.add_header_column_cells_to_csv(csv, row_axis)
78
+ prev_row = nil
79
+ rows.each do |row|
80
+ csv << prev_row = row.to_a(previous: prev_row)
81
+ end
82
+ end
83
+ end
84
+
85
+ def rows
86
+ cells.group_by do |cell|
87
+ cell.to_axis_values(row_axis, flat: false)
88
+ end.map do |row_grain_values, cells|
89
+ PivotRow.new(self, row_grain_values, cells)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def metrics_sort_order
96
+ @_metrics_sort_order ||= Hash[metrics.each_with_index.map{|m, i| [m.id, i]}]
97
+ end
98
+
99
+ def sort_cells(cells_arr)
100
+ return cells_arr if sort.present?
101
+ cells_arr.sort_by do |cell|
102
+ pivot_grain.map{|level_id| cell[level_id] || '' } + [metrics_sort_order[cell.metric_id]]
103
+ end
104
+ end
105
+
106
+ end
107
+ end
108
+ end
109
+
@@ -0,0 +1,125 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PivotTableBuilder
4
+ ALL_METRICS_KEY = 'metrics'
5
+
6
+ attr_reader :query_context, :on_columns_args, :on_rows_args, :sort_args, :in_cells_arg, :row_totals, :column_totals
7
+ delegate :standardizer, :definition_from_id, to: :query_context
8
+
9
+ delegate :cells, :elements, :to_chart, :to_csv, to: :table
10
+
11
+ def initialize(query_context)
12
+ @query_context = query_context
13
+ @on_columns_args = []
14
+ @on_rows_args = []
15
+ @sort_args = {}
16
+ @row_totals = false
17
+ @column_totals = false
18
+ end
19
+
20
+ def select(*metric_ids)
21
+ dup.instance_eval do
22
+ @metrics = standardizer.standardize(metric_ids)
23
+ self
24
+ end
25
+ end
26
+
27
+ def sort(*args)
28
+ @sort_args.merge! Sorter.args_to_hash(args.length == 1 ? args.first : args)
29
+ self
30
+ end
31
+
32
+ def on_columns(*level_ids)
33
+ dup.instance_eval do
34
+ validate_metrics_in_level_ids(level_ids)
35
+ @on_columns_args = level_ids.uniq
36
+ self
37
+ end
38
+ end
39
+
40
+ def on_rows(*level_ids)
41
+ dup.instance_eval do
42
+ validate_metrics_in_level_ids(level_ids)
43
+ @on_rows_args = level_ids.uniq
44
+ self
45
+ end
46
+ end
47
+
48
+ def with_totals(rows: true, columns: true)
49
+ dup.instance_eval do
50
+ @row_totals = !!rows
51
+ @column_totals = !!columns
52
+ self
53
+ end
54
+ end
55
+
56
+ def in_cells(metric_id)
57
+ raise Query::Error.new('Cannot have more than one metric in cells') if metric_id.to_s == ALL_METRICS_KEY
58
+ raise Query::Error.new("#{metric_id} is not a metric") unless metric?(metric_id)
59
+ @in_cells_arg = standardizer.standardize(metric_id)
60
+ self
61
+ end
62
+
63
+ def build
64
+ raise Query::Error.new('No columns were selected') unless on_columns_args.present?
65
+ raise Query::Error.new('No rows were selected') unless on_rows_args.present?
66
+ raise Query::Error.new('At least one metric has to be defined in pivot') unless metric_definition_count > 0
67
+ raise Query::Error.new('Metrics can either be on columns or rows or in cells') if metric_definition_count > 1
68
+ PivotTable.new(query_context, metrics: metrics, pivot_grain: pivot_grain,
69
+ row_axis: axis_for(on_rows_args), column_axis: axis_for(on_columns_args),
70
+ row_totals: row_totals, column_totals: column_totals, sort: sort_args).reload
71
+ end
72
+ alias_method :table, :build
73
+
74
+ private
75
+
76
+ def metric?(id)
77
+ return true if id.to_s == ALL_METRICS_KEY
78
+ query_context.metric? standardizer.standardize(id)
79
+ end
80
+
81
+ def validate_metrics_in_level_ids(level_ids)
82
+ level_ids.each do |level_id|
83
+ next if level_id.to_s == ALL_METRICS_KEY
84
+ level_id = standardizer.standardize(level_id)
85
+ raise Query::Error.new("#{level_id}: Cannot pivot on individual metrics. " +
86
+ "Use 'metrics' for `on_columns` or `on_rows`, " +
87
+ "`select` to restrict metrics or `in_cells` to display one metric") if metric?(level_id)
88
+ end
89
+ end
90
+
91
+ def metric_ids
92
+ return [in_cells_arg] if in_cells_arg
93
+ @metrics || query_context.metric_ids
94
+ end
95
+
96
+ def metric_definition_count
97
+ (in_cells_arg ? 1 : 0) + (on_columns_args + on_rows_args).select { |x| metric?(x) }.length
98
+ end
99
+
100
+ def without_metrics(collection)
101
+ collection.reject { |x| metric?(x) }
102
+ end
103
+
104
+ def metrics
105
+ metric_ids.map { |x| query_context.metric(x) }
106
+ end
107
+
108
+ def pivot_grain
109
+ without_metrics(on_columns_args + on_rows_args)
110
+ end
111
+
112
+ def axis_for(collection)
113
+ grain_elements = collection.map do |level_id|
114
+ if level_id.to_s == ALL_METRICS_KEY
115
+ Runtime::PivotGrainElement.new(id: level_id, metrics: metrics)
116
+ else
117
+ Runtime::PivotGrainElement.new(id: level_id, level_definition: query_context.definition_from_id(level_id))
118
+ end
119
+ end
120
+ PivotAxis.new grain_elements: grain_elements
121
+ end
122
+
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,149 @@
1
+ module Martyr
2
+ module Runtime
3
+ class MetricDependencyResolver
4
+ attr_reader :cube
5
+
6
+ # @param cube [BaseCube] either virtual or regular cube
7
+ def initialize(cube)
8
+ @cube = cube
9
+ @metrics_by_cube = {}
10
+ @inferred_fact_grain_by_cube = {}
11
+ end
12
+
13
+ def to_hash
14
+ Hash[@metrics_by_cube.map{ |cube_name, arr| [cube_name, arr.keys] }]
15
+ end
16
+
17
+ def inspect
18
+ to_hash.inspect
19
+ end
20
+
21
+ # @option all [Boolean] send true if all metrics, including dependents, should be retrieved. Otherwise, only
22
+ # explicitly asked-for metrics will be included in the result set
23
+ # @return [Array<BaseMetric>]
24
+ def metrics(all: false)
25
+ metric_entries = @metrics_by_cube.flat_map { |_cube_name, metric_ids_hash| metric_ids_hash.values }
26
+ metric_entries.select! { |entry| entry[:explicit] } unless all
27
+ metric_entries.map { |entry| entry[:metric] }
28
+ end
29
+
30
+ # @return [Array<String>] metric IDs
31
+ def metric_ids(all: false)
32
+ metrics(all: all).map(&:id)
33
+ end
34
+
35
+ # @param cube_name [String]
36
+ # @option all [Boolean] send true if all metrics, including dependents, should be retrieved. Otherwise, only
37
+ # explicitly asked-for metrics will be included in the result set
38
+ # @return [Array<BaseMetric>]
39
+ def metrics_for(cube_name, all: false)
40
+ relevant_entries_for(cube_name, all: all).map do |_metric_id, metric_entry|
41
+ metric_entry[:metric]
42
+ end
43
+ end
44
+
45
+ # @see metrics_for
46
+ # @return [Array<String>] metric IDs
47
+ def metric_ids_for(cube_name, all: false)
48
+ relevant_entries_for(cube_name, all: all).map(&:first)
49
+ end
50
+
51
+ # @return [Array<String>] of all fact grains combined acrross cubes
52
+ def inferred_fact_grain
53
+ @inferred_fact_grain_by_cube.flat_map{ |_cube_name, levels_lookup_hash| levels_lookup_hash.keys }.uniq
54
+ end
55
+
56
+ # @return [Array<String>] array of level IDs that need to be part of the fact grain in order for the metrics to
57
+ # compute, including any dependent metrics. This does not include the default_fact_grain
58
+ def inferred_fact_grain_for(cube_name)
59
+ @inferred_fact_grain_by_cube[cube_name].try(:keys) || []
60
+ end
61
+
62
+ # @return [Array<String>] array of sub facts that need to be joined in order to support the required metrics,
63
+ # as provided under the `sub_queries` key of the metric definition.
64
+ def sub_facts_for(cube_name)
65
+ metrics_for(cube_name, all: true).flat_map {|metric| metric.sub_queries if metric.respond_to?(:sub_queries) }.compact
66
+ end
67
+
68
+ # Recursively add the metric and its dependencies
69
+ # @param metric_id [String] fully qualified metric ID (with cube name)
70
+ # @option explicit [Boolean] indicates whether the metric was asked to be included as part of a query.
71
+ # send false for metrics that were added due to dependency.
72
+ def add_metric(metric_id, explicit: true)
73
+ metric = cube.find_metric_id(metric_id)
74
+ add_count_distinct_fact_grain_dependency(metric, explicit)
75
+ return unless register_metric(metric, explicit)
76
+
77
+ add_fact_grain_dependency(metric)
78
+ add_dependent_metrics(metric)
79
+ end
80
+
81
+ def data_dup
82
+ dup.instance_eval do
83
+ @metrics_by_cube = @metrics_by_cube.deep_dup
84
+ @inferred_fact_grain_by_cube = @inferred_fact_grain_by_cube.deep_dup
85
+ self
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ # @param metric [BaseMetric]
92
+ # @param explicit [Boolean] see #add_metric
93
+ # @return [Boolean]
94
+ # true if the metric was added for the first time.
95
+ # false if the metric was already added before
96
+ #
97
+ # @note this method makes sure to set the explicit flag to true if explicit param is true. The flag will not be
98
+ # changed if explicit param is false.
99
+ #
100
+ # This makes sure that if the user specifies select('a', 'b') and 'a' depends on 'b', then 'b' will be marked
101
+ # as explicit, despite the fact it was added as explicit=false when 'a' was added.
102
+ #
103
+ def register_metric(metric, explicit)
104
+ @metrics_by_cube[metric.cube_name] ||= {}
105
+ if @metrics_by_cube[metric.cube_name].has_key?(metric.id)
106
+ @metrics_by_cube[metric.cube_name][metric.id][:explicit] = true if explicit
107
+ return false
108
+ end
109
+
110
+ @metrics_by_cube[metric.cube_name][metric.id] = { metric: metric, explicit: explicit }
111
+ true
112
+ end
113
+
114
+ # The level which count-distinct metric A depends on is added only if another metric depends on metric A,
115
+ # and only if the user did not specify a custom fact_grain for metric A.
116
+ def add_count_distinct_fact_grain_dependency(metric, explicit)
117
+ return unless !explicit and metric.is_a?(Schema::CountDistinctMetric) and metric.fact_grain.blank?
118
+ store_inferred_fact_grain(metric.cube_name, metric.level_id)
119
+ end
120
+
121
+ def add_fact_grain_dependency(metric)
122
+ return unless metric.respond_to?(:fact_grain) and metric.fact_grain.present?
123
+ metric.fact_grain.each do |level_id|
124
+ store_inferred_fact_grain(metric.cube_name, level_id)
125
+ end
126
+ end
127
+
128
+ # @param metric [BaseMetric]
129
+ def add_dependent_metrics(metric)
130
+ return unless metric.respond_to?(:depends_on) and metric.depends_on.present?
131
+ metric.depends_on.each do |dependent_metric_id|
132
+ add_metric(dependent_metric_id, explicit: false)
133
+ end
134
+ end
135
+
136
+ # @see metrics_for
137
+ def relevant_entries_for(cube_name, all:)
138
+ candidates = @metrics_by_cube[cube_name] || []
139
+ candidates.select!{ |_metric_id, metric_entry| metric_entry[:explicit] } unless all
140
+ candidates
141
+ end
142
+
143
+ def store_inferred_fact_grain(cube_name, level_id)
144
+ @inferred_fact_grain_by_cube[cube_name] ||= {}
145
+ @inferred_fact_grain_by_cube[cube_name][level_id] = true
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,246 @@
1
+ module Martyr
2
+ module Runtime
3
+ class QueryContext
4
+ include Martyr::LevelComparator
5
+ include Martyr::Translations
6
+
7
+ # @attribute metrics [Array<BaseMetric>] metrics that were requested as part of the query, without dependencies
8
+ # @attribute sub_cubes_hash [Hash] of the format { cube_name => Runtime::SubCube }
9
+ # @attribute dimension_scopes [Runtime::DimensionScopeCollection] see BaseCube::build_dimension_scopes
10
+ # @attribute level_ids_in_grain [Array<String>] array of level IDs
11
+ # @attribute virtual_cube [VirtualCube]
12
+ # @attribute virtual_cube_metric_ids [Array<String>]
13
+
14
+ attr_accessor :metrics, :sub_cubes_hash, :dimension_scopes, :level_ids_in_grain, :virtual_cube, :virtual_cube_metric_ids
15
+ attr_reader :data_slice
16
+ delegate :level_scope, :level_scopes, :with_level_scope, :lowest_level_of, :lowest_level_ids_of,
17
+ :levels_and_above_for, :level_ids_and_above_for, :level_loaded?, to: :dimension_scopes
18
+ delegate :slice, to: :memory_slice
19
+
20
+ def initialize
21
+ @data_slice = DataSlice.new(self)
22
+ @sub_cubes_hash = {}
23
+ @virtual_cube_metric_ids = []
24
+ end
25
+
26
+ def inspect
27
+ "#<QueryContext metric_ids: #{metric_ids}, grain: #{level_ids_in_grain}, memory_slice: #{memory_slice.to_hash}, data_slice: #{data_slice.to_hash}, sub_cubes: #{sub_cubes}>"
28
+ end
29
+
30
+ def sub_cubes
31
+ sub_cubes_hash.values
32
+ end
33
+
34
+ # @return [Array<BaseMetric>] if the current cube is virtual, returns array of metrics of the virtual cube
35
+ def virtual_metrics
36
+ return [] unless self.virtual_cube
37
+ virtual_cube_metric_ids.map do |unique_metric_id|
38
+ metric_id = second_element_from_id(unique_metric_id)
39
+ virtual_cube.metric_definitions.find_or_error(metric_id)
40
+ end
41
+ end
42
+
43
+ # @return [Array<String>] only metric IDs that were requested as part of the query, without dependencies
44
+ def metric_ids
45
+ metrics.map(&:id)
46
+ end
47
+
48
+ # @return [Array<BaseMetric>] all metrics, including those that are added by dependencies and virtuals
49
+ def all_metrics
50
+ sub_cubes.flat_map { |sub_cube| sub_cube.metric_objects } + virtual_metrics
51
+ end
52
+
53
+ def all_metric_ids
54
+ all_metrics.map(&:id)
55
+ end
56
+
57
+ # @param id [String] has to be fully qualified (cube_name.metric_name)
58
+ def metric(id)
59
+ metric_ids_lookup[id]
60
+ end
61
+
62
+ # @param id [String] has to be fully qualified (cube_name.metric_name)
63
+ def metric?(id)
64
+ !!metric(id)
65
+ end
66
+
67
+ # = Grain
68
+
69
+ def supported_level_ids
70
+ @_supported_level_ids ||= levels_and_above_for(level_ids_in_grain).map(&:id)
71
+ end
72
+
73
+ def validate_slice_on!(slice_on)
74
+ slice_on_object = definition_from_id(slice_on)
75
+ raise Query::Error.new("Cannot find `#{slice_on}`") unless slice_on_object
76
+ raise Query::Error.new("Cannot slice on `#{slice_on}`: it is not in the grain") if slice_on_object.is_a?(Martyr::Level) and !supported_level_ids.include?(slice_on)
77
+ true
78
+ end
79
+
80
+ # = Memory slices
81
+
82
+ def memory_slice
83
+ @memory_slice ||= MemorySlice.new(data_slice)
84
+ end
85
+
86
+ # @return [QueryContext] for chaining
87
+ def slice(*args)
88
+ dup_internals.slice!(*args)
89
+ end
90
+
91
+ # @return [QueryContext] for chaining
92
+ def slice!(*args)
93
+ memory_slice.slice(*args)
94
+ self
95
+ end
96
+
97
+ # @return [Array<String>] array of level IDs that are in the memory slice
98
+ def sliced_level_ids
99
+ memory_slice.keys.reject{|id| metric?(id)}
100
+ end
101
+
102
+ # @return [Array<String>] array of level IDs that are in the grain but not sliced
103
+ def unsliced_level_ids_in_grain
104
+ level_ids_in_grain - sliced_level_ids
105
+ end
106
+
107
+ # = Run
108
+
109
+ # @option cube_name [String, Symbol] default is first cube
110
+ # @return [Array<Fact>] of the chosen cube
111
+ def facts(cube_name = nil)
112
+ cube_name ||= default_cube.cube_name
113
+ sub_cubes_hash[cube_name.to_s].facts
114
+ end
115
+
116
+ # A cube that has no grain and no metric doesn't matter - it will end up having one "useless" element with
117
+ # no levels in the grain.
118
+ # TODO: ignore cubes that do not share a grain or slice. Here is an algorithm:
119
+ # Start with a set of all metrics that are needed to be fetched.
120
+ # Add all cubes with metric-slices on them.
121
+ # If the shared grain is missing a level in the grain - add all cubes that support that level.
122
+ # If the shared grain is missing a level in ths slice - add all cubes that support that level.
123
+ #
124
+ # @option sort [Array, Hash] either
125
+ def elements(**options)
126
+ load_bottom_level_primary_keys
127
+ builder = VirtualElementsBuilder.new(memory_slice, unsliced_level_ids_in_grain: unsliced_level_ids_in_grain,
128
+ virtual_metrics: virtual_metrics)
129
+
130
+ sort_args = options.delete(:sort) || {}
131
+ sorter = Sorter.new(standardizer.standardize(sort_args)) { |sort_argument| definition_from_id(sort_argument) }
132
+
133
+ sub_cubes.each do |sub_cube|
134
+ next unless sub_cube.metric_objects.present? or sub_cube.lowest_level_ids_in_grain.present?
135
+ memory_slice_for_cube = memory_slice.for_cube(sub_cube)
136
+ builder.add sub_cube.elements(memory_slice_for_cube, **options), sliced: memory_slice_for_cube.to_hash.present?
137
+ end
138
+ sorter.sort(builder.build)
139
+ end
140
+
141
+ def total(metrics: nil)
142
+ elements(levels: [], metrics: metrics).first || empty_element(metrics: metrics)
143
+ end
144
+ alias_method :totals, :total
145
+
146
+ def empty_element(metrics: nil)
147
+ return sub_cubes.first.element_locator_for(memory_slice, metrics: metrics).empty_element unless virtual_cube?
148
+
149
+ locators = sub_cubes.map {|sub_cube| sub_cube.element_locator_for(memory_slice, metrics: metrics) }
150
+ VirtualElement.new({}, memory_slice, locators, []).rollup(*virtual_metrics)
151
+ end
152
+
153
+ def pivot
154
+ Runtime::PivotTableBuilder.new(self)
155
+ end
156
+
157
+ # = Dispatcher
158
+
159
+ # @return [BaseMetric, DimensionReference, BaseLevelDefinition]
160
+ def definition_from_id(id)
161
+ with_standard_id(id) do |x, y|
162
+ return dimension_scopes[x].try(:dimension_definition) || default_cube.metrics[x] if !y
163
+ return sub_cubes_hash[x].find_metric(y) if sub_cubes_hash[x]
164
+ dimension_scopes.find_level(id).try(:level_definition)
165
+ end
166
+ end
167
+
168
+ # = As Dimension Bus Role
169
+
170
+ def level_ids_and_above
171
+ level_ids_and_above_for(level_ids_in_grain)
172
+ end
173
+
174
+ # @param level_id [String] e.g. 'customers.last_name'
175
+ # @param fact_record [Fact]
176
+ def fetch_unsupported_level_value(level_id, fact_record)
177
+ sought_level_definition = dimension_scopes.find_level(level_id).level_definition
178
+ common_denominator_association = fact_record.sub_cube.common_denominator_level_association(level_id, prefer_query: true)
179
+ common_denominator_level_scope = level_scope(common_denominator_association.id)
180
+ common_denominator_level_scope.recursive_lookup_up fact_record.fact_key_for(common_denominator_association.id), level: sought_level_definition
181
+ end
182
+
183
+ # @param level_id [String] e.g. 'customers.last_name'
184
+ # @param fact_key_value [Integer] the primary key stored in the fact
185
+ def fetch_supported_query_level_record(level_id, fact_key_value)
186
+ level_scope = level_scope(level_id)
187
+ raise Internal::Error.new('level must be query') unless level_scope.query?
188
+ level_scope.recursive_lookup_up fact_key_value, level: level_scope
189
+ end
190
+
191
+ def standardizer
192
+ @standardizer ||= Martyr::MetricIdStandardizer.new(default_cube.cube_name, raise_if_not_ok: virtual_cube?)
193
+ end
194
+
195
+ def dup_internals
196
+ dup.instance_eval do
197
+ @memory_slice = memory_slice.dup_internals
198
+ self
199
+ end
200
+ end
201
+
202
+ def element_helper_module
203
+ return @element_helper_module if @element_helper_module
204
+ @element_helper_module = Module.new
205
+ dimension_scopes.register_element_helper_methods(@element_helper_module)
206
+
207
+ all_metric_ids.each do |metric_id|
208
+ metric_name = second_element_from_id(metric_id)
209
+ @element_helper_module.module_eval do
210
+ define_method(metric_name) { fetch(metric_id) }
211
+ end
212
+ end
213
+ @element_helper_module
214
+ end
215
+
216
+ private
217
+
218
+ def metric_ids_lookup
219
+ @metric_ids_lookup ||= all_metrics.index_by(&:id)
220
+ end
221
+
222
+ def virtual_cube?
223
+ sub_cubes.length > 1
224
+ end
225
+
226
+ def default_cube
227
+ sub_cubes.first
228
+ end
229
+
230
+ def load_bottom_level_primary_keys
231
+ return if @bottom_level_primary_keys_loaded
232
+ sub_cubes.each do |sub_cube|
233
+ sub_cube.lowest_level_ids_in_grain.each do |level_id|
234
+ level = level_scope(level_id)
235
+ next unless level.query?
236
+ level.primary_keys_for_load ||= []
237
+ level.primary_keys_for_load += sub_cube.facts.map{|x| x.raw[level.fact_alias]}
238
+ level.primary_keys_for_load = level.primary_keys_for_load.uniq
239
+ end
240
+ end
241
+ @bottom_level_primary_keys_loaded = true
242
+ end
243
+
244
+ end
245
+ end
246
+ end