martyr 0.1.74.pre

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 (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