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,215 @@
1
+ module Martyr
2
+ module Runtime
3
+ # This is a SubCube builder. The builder method is #execute.
4
+ # It takes the `select`, `slice`, and `granulate` clauses, makes sure they are well defined, and create a sub cube
5
+ # as a result of their application.
6
+ class QueryContextBuilder
7
+
8
+ include Martyr::LevelComparator
9
+ include Martyr::Translations
10
+
11
+ attr_reader :cube, :scope_helper_module
12
+ delegate :elements, :facts, :pivot, :total, :totals, to: :build
13
+
14
+ def initialize(cube, scope_helper_module)
15
+ @cube = cube
16
+ @metric_dependency_resolver = MetricDependencyResolver.new(cube)
17
+ @data_slice = {}
18
+ @granulate_args = []
19
+ @decorations = {}
20
+ @scope_helper_module = scope_helper_module
21
+ extend_scope_helper
22
+ end
23
+
24
+ # select(:a, :b, :c)
25
+ # select(:all)
26
+ def select!(*arr)
27
+ @all_metrics = true and return self if arr.length == 1 and arr.first.to_s == 'all'
28
+ standardize(arr).each do |metric_id|
29
+ @metric_dependency_resolver.add_metric(metric_id)
30
+ end
31
+ self
32
+ end
33
+
34
+ # Variant 1 - full slice as one hash:
35
+ # slice('artist.name' => {with: 'AC/DC'}, 'genre.name' => {with: 'Rock'}, quantity: {gt: 0})
36
+ #
37
+ # Variant 2 - on one dimension or metric
38
+ # slice('artist.name', with: 'AC/DC')
39
+ #
40
+ def slice!(*several_variants)
41
+ if several_variants.length == 1 and several_variants.first.is_a?(Hash)
42
+ several_variants.first.stringify_keys.except(PivotCell::METRIC_COORD_KEY).each do |slice_on, slice_definition|
43
+ @data_slice.merge! standardize(slice_on) => slice_definition
44
+ end
45
+ elsif several_variants.length == 2
46
+ slice_on, slice_definition = several_variants
47
+ @data_slice.merge! standardize(slice_on) => slice_definition
48
+ else
49
+ ArgumentError.new("wrong number of arguments (#{several_variants.length} for 1..2)")
50
+ end
51
+ self
52
+ end
53
+
54
+ # granulate('artist.name', 'genre.name')
55
+ def granulate!(*arr)
56
+ @granulate_args += arr
57
+ self
58
+ end
59
+
60
+ # @param level_id [String]
61
+ # @param *args [Array] will be sent to includes
62
+ def decorate!(level_id, lambda = nil, &block)
63
+ @decorations[level_id] = lambda || block
64
+ self
65
+ end
66
+
67
+ def select(*args)
68
+ data_dup.select!(*args)
69
+ end
70
+
71
+ def slice(*args)
72
+ data_dup.slice!(*args)
73
+ end
74
+
75
+ def granulate(*args)
76
+ data_dup.granulate!(*args)
77
+ end
78
+
79
+ def decorate(*args, &block)
80
+ data_dup.decorate!(*args, &block)
81
+ end
82
+
83
+ def build
84
+ return @context if @context
85
+ context = QueryContext.new
86
+ add_metric_slices_to_metric_dependencies
87
+ add_all_metrics_if_none_selected
88
+ setup_context_grain_and_metrics(context)
89
+ setup_context_dimension_scopes(context)
90
+ setup_context_sub_cubes_metrics_and_grain_and_sub_facts(context)
91
+ setup_context_data_slice(context)
92
+ setup_virtual_cube(context)
93
+ decorate_all_scopes(context)
94
+ @context = context
95
+ end
96
+
97
+ private
98
+
99
+ # = Building steps
100
+
101
+ def add_metric_slices_to_metric_dependencies
102
+ @data_slice.keys.select{ |slice_on| cube.metric?(slice_on) }.each do |metric_id|
103
+ @metric_dependency_resolver.add_metric(metric_id, explicit: false)
104
+ end
105
+ end
106
+
107
+ def add_all_metrics_if_none_selected
108
+ return unless @all_metrics
109
+ return if @metric_dependency_resolver.metric_ids.present?
110
+ cube.metrics.values.each do |metric|
111
+ @metric_dependency_resolver.add_metric(metric.id)
112
+ end
113
+ end
114
+
115
+ # Step 1
116
+ # Add all levels to the query grain
117
+ def setup_context_grain_and_metrics(context)
118
+ context.level_ids_in_grain = Array.wrap(@granulate_args)
119
+ context.metrics = @metric_dependency_resolver.metrics
120
+ end
121
+
122
+ # Step 2 (relies on Steps 1)
123
+ # Build the dimension scope objects supported by the grain AND slices.
124
+ # DegeneratesAndBottomLevels.granulate('genres.name', 'media_types.name').slice('customers.country', with: 'USA')
125
+ def setup_context_dimension_scopes(context)
126
+ relevant_dimensions = (default_grains + context.level_ids_in_grain + @data_slice.keys).map { |x| first_element_from_id(x) }
127
+ context.dimension_scopes = cube.build_dimension_scopes(relevant_dimensions.uniq)
128
+
129
+ @decorations.each do |level_id, proc|
130
+ context.dimension_scopes.find_level(level_id).decorate_scope(&proc)
131
+ end
132
+ end
133
+
134
+ # Step 3
135
+ # Setup the sub cubes, metrics, and grain for all cubes. Note that each cube only takes the levels of dimensions
136
+ # it supports. That said, if dimension has levels L1, L2, L3 and a cube supports only L1 and L2, if L3 is
137
+ # requested it will yield an empty cube.
138
+ # If no metrics were given, select all.
139
+ # If no grain was given, select all levels - separately for each cube.
140
+ def setup_context_sub_cubes_metrics_and_grain_and_sub_facts(context)
141
+ cube.contained_cube_classes.index_by(&:cube_name).each do |cube_name, cube_class|
142
+ metric_ids = @metric_dependency_resolver.metric_ids_for(cube_name, all: true)
143
+ grain = (@granulate_args + @metric_dependency_resolver.inferred_fact_grain_for(cube_name) +
144
+ cube_class.default_fact_grain).uniq
145
+ sub_facts = @metric_dependency_resolver.sub_facts_for(cube_name)
146
+
147
+ sub_cube = Runtime::SubCube.new(context, cube_class)
148
+ context.sub_cubes_hash[cube_name] = sub_cube
149
+ sub_cube.set_metrics(metric_ids)
150
+ sub_cube.set_grain(grain)
151
+ sub_cube.set_sub_facts(sub_facts)
152
+ end
153
+ end
154
+
155
+ # Step 4
156
+ # Sets the data slice. Note that no slicing actually occurs - only setup.
157
+ def setup_context_data_slice(context)
158
+ @data_slice.each do |slice_on, slice_definition|
159
+ context.data_slice.slice(slice_on, slice_definition)
160
+ end
161
+ end
162
+
163
+ # Step 5
164
+ # Sets the virtual cube
165
+ def setup_virtual_cube(context)
166
+ return unless cube.virtual?
167
+ context.virtual_cube = cube
168
+ context.virtual_cube_metric_ids = @metric_dependency_resolver.metric_ids_for(cube.cube_name, all: true)
169
+ end
170
+
171
+ # Step 6 (depends on steps 3 and 4)
172
+ # All scopes are altered to represent the necessary queries - fact scopes (select, where, and group by) and
173
+ # dimension scopes (where)
174
+ def decorate_all_scopes(context)
175
+ context.data_slice.add_to_dimension_scope(context)
176
+ context.sub_cubes_hash.each do |_cube_name, sub_cube|
177
+ sub_cube.decorate_all_scopes context.data_slice.for_cube(sub_cube)
178
+ end
179
+ end
180
+
181
+ protected
182
+
183
+ def extend_scope_helper
184
+ return self unless scope_helper_module
185
+ extend(scope_helper_module)
186
+ end
187
+
188
+ private
189
+
190
+ def data_dup
191
+ dup.instance_eval do
192
+ @decorations = @decorations.dup
193
+ @data_slice = @data_slice.dup
194
+ @granulate_args = @granulate_args.dup
195
+ @metric_dependency_resolver = @metric_dependency_resolver.data_dup
196
+ extend_scope_helper
197
+ self
198
+ end
199
+ end
200
+
201
+ def standardize(object)
202
+ @standardizer ||= Martyr::MetricIdStandardizer.new(cube.contained_cube_classes.first.cube_name,
203
+ raise_if_not_ok: cube.contained_cube_classes.length > 1)
204
+
205
+ @standardizer.standardize(object)
206
+ end
207
+
208
+ def default_grains
209
+ (@metric_dependency_resolver.inferred_fact_grain +
210
+ cube.contained_cube_classes.flat_map(&:default_fact_grain)).uniq
211
+ end
212
+
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,113 @@
1
+ module Martyr
2
+ module Runtime
3
+
4
+ class BaseOperator
5
+ attr_reader :setup_block, :operation_args
6
+ attr_accessor :fact_scope
7
+
8
+ # Defines the method #add_<operator_name> to a class.
9
+ #
10
+ # @param klass [#operators] any class that has an instance method called operators
11
+ def self.register_to(klass)
12
+ current_class = self
13
+ klass.class_eval do
14
+ define_method "add_#{current_class.name.split('::').last.underscore}" do |*args, &block|
15
+ operators << current_class.new(*args, &block)
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(&setup_block)
21
+ @setup_block = setup_block
22
+ end
23
+
24
+ def inspect
25
+ to_hash.inspect
26
+ end
27
+
28
+ def to_hash
29
+ {
30
+ self.class.name.split('::').last.underscore =>
31
+ (instance_variables - [:@setup_block, :@main_setup, :@fact_scope]).map{|x| {x => instance_variable_get(x)}}
32
+ }
33
+ end
34
+
35
+ # = Running
36
+
37
+ # There are 3 types of fact scopes that are handled:
38
+ # Inner main
39
+ # The main query fact scope.
40
+ # Inner sub
41
+ # A sub query fact scope.
42
+ # Outer wrapper
43
+ # The wrapper block on the query.
44
+ #
45
+ # Step 1
46
+ # Run the setup block if it is determined that the block can be supported for the current scope.
47
+ # The setup block is expected to run the appropriate operator "verb" on the provided operator instance, e.g.
48
+ # #add_select, #add_where, etc.
49
+ # The verb saves information that is to be applied on a fact scope - main, sub, or outer.
50
+ # The execution of the setup block is yielded self with the @fact_scope ivar initialized, so that
51
+ # support methods on self are available to the setup block.
52
+ #
53
+ # Step 2
54
+ # If the fact_scope is the main fact scope, a dup of the object is stored in @main_setup ivar, so that the
55
+ # result of running the setup block can be reused when reapplying on the outer wrapper.
56
+ #
57
+ # Step 3
58
+ # The concrete handle_inner is called for the current fact scope, which is assumed to be the main.
59
+ #
60
+
61
+ def apply_on_inner(fact_scope)
62
+ return if fact_scope.is_a?(Runtime::SubFactScope) and !run_for_sub_fact?
63
+ return unless supported_for?(fact_scope)
64
+ with_fact_scope_ivar(fact_scope) { setup_block.call(self) }
65
+ @main_setup = dup unless fact_scope.is_a?(Runtime::SubFactScope)
66
+ dup.send(:handle_inner, fact_scope)
67
+ end
68
+
69
+ def reapply_on_outer_wrapper(wrapper)
70
+ raise Internal::Error.new('@main_setup is not initialized. #apply_on_inner must be run on the ' +
71
+ 'main fact scope before calling #reapply_on_outer_wrapper') unless @main_setup.present?
72
+
73
+ @main_setup.send(:handle_outer, wrapper)
74
+ end
75
+
76
+ # Concrete classes can optionally alias their particular verb (add_where, add_group, etc.) to this method
77
+ def operate(*args)
78
+ @operation_args = args
79
+ end
80
+
81
+ private
82
+
83
+ # = Concrete class methods to override
84
+
85
+ # @return [Boolean] whether the operator should be run for sub facts
86
+ def run_for_sub_fact?
87
+ false
88
+ end
89
+
90
+ # @return [Boolean] whether the particular fact_scope is supported by this operator
91
+ def supported_for?(fact_scope)
92
+ true
93
+ end
94
+
95
+ def handle_inner(fact_scope)
96
+ # no-op
97
+ end
98
+
99
+ def handle_outer(wrapper)
100
+ # no-op
101
+ end
102
+
103
+ # = Helpers
104
+
105
+ def with_fact_scope_ivar(fact_scope)
106
+ @fact_scope = fact_scope
107
+ yield
108
+ ensure
109
+ @fact_scope = nil
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,18 @@
1
+ module Martyr
2
+ module Runtime
3
+ class GroupOperator < BaseOperator
4
+
5
+ attr_reader :field_alias
6
+
7
+ def add_group(field_alias)
8
+ @field_alias = field_alias
9
+ end
10
+
11
+ private
12
+
13
+ def handle_outer(wrapper)
14
+ wrapper.add_to_group_by(field_alias)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module Martyr
2
+ module Runtime
3
+ class SelectOperatorForDimension < BaseOperator
4
+
5
+ attr_reader :what, :as
6
+
7
+ def add_select(what, as:)
8
+ @what = what
9
+ @as = as
10
+ end
11
+
12
+ private
13
+
14
+ def handle_inner(fact_scope)
15
+ fact_scope.decorate_scope {|scope| scope.select("#{what} AS #{as}") }
16
+ end
17
+
18
+ def handle_outer(wrapper)
19
+ wrapper.add_to_select(as)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ module Martyr
2
+ module Runtime
3
+ class SelectOperatorForMetric < BaseOperator
4
+
5
+ attr_reader :metric_name
6
+ attr_reader :what, :as, :data_rollup_sql
7
+
8
+ def initialize(metric_name, &block)
9
+ super(&block)
10
+ @metric_name = metric_name
11
+ end
12
+
13
+ def add_select(what, as:, data_rollup_sql: nil)
14
+ @what = what
15
+ @as = as
16
+ @data_rollup_sql = data_rollup_sql
17
+ end
18
+
19
+ private
20
+
21
+ def handle_inner(fact_scope)
22
+ fact_scope.decorate_scope {|scope| scope.select("#{what} AS #{as}") }
23
+ end
24
+
25
+ def handle_outer(wrapper)
26
+ wrapper.add_to_select(data_rollup_sql) if data_rollup_sql.present?
27
+ end
28
+
29
+ def supported_for?(fact_scope)
30
+ fact_scope.supports_metric?(metric_name)
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ module Martyr
2
+ module Runtime
3
+ class WhereOperatorForDimension < BaseOperator
4
+ include Martyr::LevelComparator
5
+
6
+ attr_reader :dimension_name, :level_name, :block
7
+ delegate :level_key_for_where, to: :fact_scope
8
+
9
+ alias_method :add_where, :operate
10
+
11
+ def initialize(dimension_name, level_name, &block)
12
+ super(&block)
13
+ @dimension_name = dimension_name
14
+ @level_name = level_name
15
+ end
16
+
17
+ # = Support methods (@fact_scope is initialized)
18
+
19
+ def common_denominator_level(level_definition)
20
+ common_level = find_common_denominator_level(level_definition, fact_scope.dimensions[dimension_name].level_objects)
21
+
22
+ # This should never be raised, since the QueryGrain should have already nullified the cube
23
+ raise Internal::Error.new("Internal error: Dimension `#{dimension_name}` slice on level `#{level_name}` has no common denominator.") unless common_level
24
+ common_level
25
+ end
26
+
27
+ private
28
+
29
+ def handle_inner(fact_scope)
30
+ fact_scope.decorate_scope {|scope| scope.where(*operation_args) }
31
+ end
32
+
33
+ def run_for_sub_fact?
34
+ true
35
+ end
36
+
37
+ def supported_for?(fact_scope)
38
+ fact_scope.supports_dimension_level?(dimension_name, level_name)
39
+ end
40
+
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ module Martyr
2
+ module Runtime
3
+ class WhereOperatorForMetric < BaseOperator
4
+
5
+ attr_reader :metric_name
6
+ alias_method :add_where, :operate
7
+
8
+ def initialize(metric_name, &block)
9
+ super(&block)
10
+ @metric_name = metric_name
11
+ end
12
+
13
+ private
14
+
15
+ def handle_outer(wrapper)
16
+ wrapper.add_to_where(*operation_args)
17
+ end
18
+
19
+ def supported_for?(fact_scope)
20
+ fact_scope.supports_metric?(metric_name)
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,70 @@
1
+ module Martyr
2
+ module Runtime
3
+ class DataSlice
4
+
5
+ attr_accessor :slices
6
+ attr_reader :definition_resolver
7
+
8
+ include Martyr::Delegators
9
+ each_child_delegator :add_to_grain, :add_to_where, :add_to_dimension_scope, to: :slice_objects
10
+
11
+ include Martyr::Translations
12
+
13
+ # = Slice definitions
14
+
15
+ # @param definition_resolver [#definition_from_id]
16
+ def initialize(definition_resolver)
17
+ @definition_resolver = definition_resolver
18
+ @slices = ScopeableSliceData.new
19
+ end
20
+
21
+ def inspect
22
+ to_hash.inspect
23
+ end
24
+
25
+ def to_hash
26
+ slice_objects.inject({}) {|h,slice| h.merge! slice.to_hash}
27
+ end
28
+
29
+ # @return [Array<String>] with either metric ID or level ID (contrast with dimension name)
30
+ def keys
31
+ slice_objects.flat_map(&:keys)
32
+ end
33
+
34
+ def definition_object_for(slice_on)
35
+ definition_resolver.definition_from_id(slice_on) || raise(Query::Error.new("Could not find `#{slice_on}` to apply slice on"))
36
+ end
37
+
38
+ # @param slice_on [String] e.g. 'customers.last_name' or 'amount_sold'
39
+ # @param slice_definition [Hash]
40
+ def slice(slice_on, slice_definition)
41
+ slice_on_object = definition_object_for(slice_on)
42
+ slices[slice_on_object.slice_id] ||= slice_on_object.build_data_slice
43
+ slices[slice_on_object.slice_id].set_slice(slice_on_object, **slice_definition.symbolize_keys)
44
+ end
45
+
46
+ def slice_objects
47
+ slices.values
48
+ end
49
+
50
+ # @return [Array<String>]
51
+ def dimension_names
52
+ slices.reject{|_slice_id, slice_object| slice_object.is_a?(MetricDataSlice)}.keys
53
+ end
54
+
55
+ # @param sub_cube [SubCube]
56
+ # @return [DataSlice] new object with a new ScopeableDataSliceData object set to be scoped to the sub cube
57
+ def for_cube(sub_cube)
58
+ dup.for_cube!(sub_cube)
59
+ end
60
+
61
+ # @param sub_cube [SubCube]
62
+ # @return [DataSlice] same object with a new ScopeableDataSliceData object set to be scoped to the sub cube
63
+ def for_cube!(sub_cube)
64
+ self.slices = slices.scope(sub_cube)
65
+ self
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,54 @@
1
+ module Martyr
2
+ module Runtime
3
+ class MetricDataSlice
4
+ attr_reader :metric, :slice_definition
5
+ delegate :to_hash, to: :slice_definition
6
+ delegate :cube_name, to: :metric
7
+ delegate :id, to: :metric, prefix: true
8
+
9
+ def initialize(metric)
10
+ @metric = metric
11
+ end
12
+
13
+ def keys
14
+ [metric_id]
15
+ end
16
+
17
+ def set_slice(metric_definition, **options)
18
+ raise Martyr::Error.new('Internal error. Inconsistent metric received') unless metric_definition.id == metric_id
19
+ @slice_definition = MetricSliceDefinition.new(metric: metric, **options)
20
+ end
21
+
22
+ def get_slice(metric_id_for_get)
23
+ raise Martyr::Error.new('Internal error. Inconsistent metric received') unless metric_id_for_get == metric_id
24
+ @slice_definition
25
+ end
26
+
27
+ def add_to_dimension_scope(*)
28
+ # no-op
29
+ end
30
+
31
+ def add_to_grain(*)
32
+ # no-op
33
+ end
34
+
35
+ # @param fact_scopes [Runtime::FactScopeCollection]
36
+ def add_to_where(fact_scopes, *)
37
+ slice_definition.combined_statements.each do |or_statements|
38
+ fact_scopes.add_where_operator_for_metric(metric_id) do |operator|
39
+ operator.add_where compile_or_statement_group(or_statements),
40
+ *or_statements.map{|slice_statement| slice_statement[:value]}
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def compile_or_statement_group(or_statement_group)
48
+ or_statement_group.map do |slice_statement|
49
+ "#{metric.fact_alias} #{slice_statement[:data_operator]} ?"
50
+ end.join(' OR ')
51
+ end
52
+ end
53
+ end
54
+ end