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 PlainDimensionDataSlice
4
+ include Martyr::Runtime::HasScopedLevels
5
+
6
+ # @attribute levels [Hash<String => PlainDimensionLevelSliceDefinition>]
7
+ attr_reader :levels
8
+
9
+ attr_reader :dimension_definition
10
+ delegate :keys, to: :sorted_levels
11
+
12
+ def initialize(dimension_definition)
13
+ @dimension_definition = dimension_definition
14
+ @levels = {}
15
+ end
16
+
17
+ def sorted_levels
18
+ arr = scoped_levels.sort_by{|_level_id, slice| slice.level.to_i}
19
+ Hash[arr]
20
+ end
21
+
22
+ def to_hash
23
+ sorted_levels.values.inject({}) {|h, slice| h.merge! slice.to_hash}
24
+ end
25
+
26
+ def dimension_name
27
+ dimension_definition.name
28
+ end
29
+
30
+ # @param level [BaseLevelDefinition]
31
+ def set_slice(level, **options)
32
+ @levels[level.id] = PlainDimensionLevelSliceDefinition.new(level: level, **options)
33
+ end
34
+
35
+ # @return [PlainDimensionLevelSliceDefinition]
36
+ def get_slice(level_id)
37
+ scoped_levels[level_id]
38
+ end
39
+
40
+ # = Slicing the dimension
41
+
42
+ def add_to_dimension_scope(dimension_bus)
43
+ scoped_levels.keys.each do |level_id|
44
+ level_scope = dimension_bus.level_scope(level_id)
45
+ slice_definition = levels[level_id]
46
+
47
+ add_slice_to_dimension_level(level_scope, slice_definition)
48
+ end
49
+ end
50
+
51
+ def add_slice_to_dimension_level(level_scope, slice_definition)
52
+ return unless level_scope.sliceable?
53
+ if slice_definition.null?
54
+ level_scope.nullify
55
+ elsif slice_definition.with.present?
56
+ level_scope.slice_with(slice_definition.with)
57
+ end
58
+ end
59
+
60
+ # = Slicing the fact
61
+
62
+ def add_to_grain(grain)
63
+ scoped_levels.keys.each {|level_id| grain.add_granularity(level_id) }
64
+ end
65
+
66
+ # @param fact_scopes [Runtime::FactScopeCollection]
67
+ # @param dimension_bus [Runtime::QueryContext]
68
+ def add_to_where(fact_scopes, dimension_bus)
69
+ scoped_levels.keys.each do |level_id|
70
+ add_one_level_to_where(fact_scopes, level_id, dimension_bus)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ # @return [FactScopeOperatorForDimension]
77
+ def add_one_level_to_where(fact_scopes, level_id, dimension_bus)
78
+ level_scope = dimension_bus.level_scope(level_id)
79
+ slice_definition = get_slice(level_id)
80
+
81
+ # Building operator used to slice the fact
82
+ fact_scopes.add_where_operator_for_dimension(level_scope.dimension_name, level_scope.name) do |operator|
83
+ if slice_definition.null?
84
+ operator.decorate_scope {|fact_scope| fact_scope.where('0=1')}
85
+ next
86
+ end
87
+
88
+ common_denominator_level = operator.common_denominator_level(level_scope.level_definition)
89
+ if common_denominator_level.name == level_scope.name
90
+ add_to_where_using_fact_strategy(level_scope, slice_definition, operator)
91
+ else
92
+ add_to_where_using_join_strategy(operator, dimension_bus.level_scope(common_denominator_level.id))
93
+ end
94
+ end
95
+ end
96
+
97
+ def add_to_where_using_fact_strategy(level_scope, slice_definition, operator)
98
+ return unless slice_definition.with.present?
99
+ level_key = operator.level_key_for_where(level_scope.id)
100
+ operator.add_where(level_key => slice_definition.with)
101
+ end
102
+
103
+ def add_to_where_using_join_strategy(operator, common_level_scope)
104
+ level_key = operator.level_key_for_where(common_level_scope.id)
105
+ operator.add_where(level_key => common_level_scope.keys)
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,9 @@
1
+ module Martyr
2
+ module Runtime
3
+ class TimeDimensionDataSlice
4
+
5
+ attr_accessor :before, :after
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,29 @@
1
+ module Martyr
2
+ module Runtime
3
+ module HasScopedLevels
4
+
5
+ def scope(supported_level_ids)
6
+ dup.scope!(supported_level_ids)
7
+ end
8
+
9
+ protected
10
+
11
+ def scope!(supported_level_ids)
12
+ @supported_level_ids = Array.wrap(supported_level_ids)
13
+ @scoped_levels = nil
14
+ self
15
+ end
16
+
17
+ private
18
+
19
+ def scoped_levels
20
+ return levels unless @supported_level_ids.present?
21
+ return @scoped_levels if @scoped_levels
22
+ unsupported_keys = levels.keys - @supported_level_ids
23
+ supported_keys = levels.keys - unsupported_keys
24
+ @scoped_levels = levels.slice(*supported_keys)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,188 @@
1
+ # Memory slices are used for two aims:
2
+ # 1. Allow slicing a sub cube that was already built. The slice in this scenario runs on the +facts+.
3
+ # So if the grain was on 'customers.city', fact records would like like this:
4
+ # customers.city customers.state customers.country amount_sold
5
+ # San Francisco CA USA 100
6
+ # New York NY USA 150
7
+ #
8
+ # and we could apply a slice on any of the levels in the grain:
9
+ # slice('customers.state', with: 'CA')
10
+ #
11
+ #
12
+ # Rule 1: Memory slice on a dimension has to be supported by the sub cube grain
13
+ # We wouldn't be able to slice on a level like 'media_types.name' because we don't have that info:
14
+ # slice('media_types.name', with: 'MPEG')
15
+ # # => Error
16
+ #
17
+ # Now let's assume the sub cube has a slice on a level that is also in it's grain. Maybe like this:
18
+ # sub_cube.slice('customers.state', with: 'CA').granulate('customers.city')
19
+ # customers.city customers.state customers.country amount_sold
20
+ # San Francisco CA USA 100
21
+ # Palo Alto CA USA 37
22
+ #
23
+ # In this case:
24
+ # slice('customers.city', with: 'Some City')
25
+ # # => Is fine
26
+ #
27
+ # slice('customers.state', with: 'NY')
28
+ # # => Is fine, but empty
29
+ #
30
+ #
31
+ # Rule 2: Memory slice on a metric can simply be merged with the sub cube:
32
+ # Sub Cube Slice Memory Slice Combined Slice
33
+ # :amount_sold, gt: 100 :amount_sold, gt: 150 :amount_sold, gt: 150
34
+ #
35
+ #
36
+ # 2. Allow changing the current element for custom rollup calculations
37
+ # Custom rollup naturally occurs within an element.
38
+ #
39
+ # Elements has a +memory-grain+ which could be different than the sub cube grain.
40
+ # That said, memory grains MUST be contained within the supported levels of the sub cube grain.
41
+ #
42
+ # For example:
43
+ # sub_cube.elements(levels: 'customers.country')
44
+ # # => Every +element+ would have the 'customers.country' on the grain, which is different than the sub cube's
45
+ # # 'customers.city' grain. The sub cube supports 'customers.country' because it is a higher level than the
46
+ # 'customers.city' level defined in the grain.
47
+ #
48
+ # sub_cube.elements(levels: 'media_types.name')
49
+ # # => Error. media types is not in the sub cube grain.
50
+ #
51
+ # Every element has two types of slices:
52
+ # - Endogenous: the current value of every level in the memory grain. E.g.: ['customers.country', with: 'USA']
53
+ # - Exogenous: the sub cube slice on metrics or levels that are not in the memory grain.
54
+ # E.g.: ['metrics.units_sold', gt: 100] or ['media_types.name', with: 'MPEG']
55
+ #
56
+ # With custom rollups, we can do a few things:
57
+ # (a) ask to override an endogenous level - this is "moving" to a different element within the same memory grain
58
+ # slice('customers.city', with: 'Paris')
59
+ # # => Before: {'customers.country' => 'USA', 'customers.city' => {with: 'Boston'} }
60
+ # After: {'customers.country' => 'France', 'customers.city' => {with: 'Paris'} }
61
+ # # => Also note that the entire dimension is reevaluated
62
+ #
63
+ # (b) ask to remove an endogenous level - this is similar to "bring parent"
64
+ # reset('customers.city')
65
+ # # => So if current element coordinates were: {'customers.country' => 'USA', 'customers.city' => {with: 'Boston'} }
66
+ # we will get an element in a different grain, with coordinates: {'customers.country' => 'USA' }
67
+ #
68
+ # (c) ask to add exogenous slice - this makes sense only if the slice is supported by the sub cube grain, of course
69
+ # slice('genres.name', with: 'Rock')
70
+ # # => Before: {'customers.country' => 'USA', 'customers.city' => {with: 'Boston'} }
71
+ # After: {'customers.country' => 'USA', 'customers.city' => {with: 'Boston'}, 'genres.name' => {with: 'Rock'}}
72
+ #
73
+ #
74
+ # Consider this scenario:
75
+ #
76
+ # Supported by Sub cube
77
+ # Sub cube slice Memory Grain
78
+ # D1.1 * *
79
+ # D1.2 * *
80
+ # D1.3 * *
81
+ # D1.4 *
82
+ # D2.1 *
83
+ # D2.2
84
+ #
85
+ # This means that the following scenarios are possible:
86
+ # 1. Override D1.1 - what happens to the current value of D1.3?
87
+ # Answer:
88
+ # The element coordinates really represent a slice on D1.3, not D1.1.
89
+ # When memory slice override is requested on D1.1, it is really requested on D1, which has the effect
90
+ # of resetting the current slice on this dimension - D1.3, and setting it to D1.1.
91
+ #
92
+ # To summarize:
93
+ # Grain: [D1.3] => [D1.1]
94
+ #
95
+ # 2. Override D1.3 - what happens to the current value of D1.1?
96
+ # Answer:
97
+ # Grain does not change. The element coordinates are still [D1.3], but a new element is fetched based
98
+ # on the requested value of D1.3. Of course, D1.1 moves together with D1.3.
99
+ #
100
+ # 3. Set slice on D1.2 - what happens to the current value of D1.3 and D1.1?
101
+ # Answer:
102
+ # Grain changes to [D1.2]. Any value for [D1.3] is reset. [D1.1] is changed with it.
103
+ #
104
+ # 4. Set slice on D1.4 - what happens to current values of D1?
105
+ # Answer:
106
+ # Grain changes to [D1.4]. Any value for [D1.3, D1.1] are moved based on the new value for [D1.4]
107
+ #
108
+ # 5. Remove slice [D1.3]
109
+ # Grain changes to [D1.1]
110
+ #
111
+ # 6. Add slice on [D2.1]
112
+ # Grain changes to [D1.3, D2.1].
113
+ #
114
+ # Let's look again at scenarios a through c when a sub cube slice existed in addition on the same dimensions we
115
+ # are compounding a memory slice on:
116
+ #
117
+ # # (a) - Overriding element's endogenous slice
118
+ #
119
+ # (a) Sub Cube grain: ['customers.last_name']
120
+ # Sub Cube Slice: {'customers.country', with: 'USA'}
121
+ # Memory Grain: ['customers.city', 'customers.state']
122
+ # Operation: slice('customers.city', with: 'Paris')
123
+ # Current coordinates: {'customers.city' => 'Boston', 'customers.country' => 'USA' }
124
+ # New coordinates: <Empty Cell>
125
+
126
+ #
127
+ # (a) Sub Cube grain: ['customers.last_name']
128
+ # Sub Cube Slice: {'customers.country', with: 'USA'}
129
+ # Memory Grain: ['customers.city', 'customers.state']
130
+ # Operation: slice('customers.city', with: 'San Francisco')
131
+ # Current coordinates: {'customers.state' => 'MI', 'customers.city' => 'Boston', 'customers.country' => 'USA' }
132
+ # New coordinates: {'customers.state' => 'CA', 'customers.city' => 'San Francisco', 'customers.country' => 'USA' }
133
+ #
134
+ # (a) Sub Cube grain: ['customers.last_name']
135
+ # Sub Cube Slice: {'customers.city', with: 'San Francisco', 'Boston', 'New York'}
136
+ # Memory Grain: ['customers.city', 'customers.state']
137
+ # Operation: slice('customers.city', with: 'San Francisco')
138
+ # Current coordinates: {'customers.state' => 'MI', 'customers.city' => 'Boston' }
139
+ # New coordinates: {'customers.state' => 'CA', 'customers.city' => 'San Francisco' }
140
+ #
141
+ # # (b) - Removing endogenous slice
142
+ #
143
+ # (b) Sub Cube grain: ['customers.last_name']
144
+ # Sub Cube Slice: {'customers.state', with: 'CA'}
145
+ # Memory Grain: ['customers.city', 'customers.country', 'customers.state']
146
+ # Operation: reset('customers.city')
147
+ # Current coordinates: {'customers.country' => 'USA', 'customers.city' => 'Boston', 'customers.state' => 'CA' }
148
+ # New coordinates: {'customers.country' => 'USA', 'customers.state' => 'CA' }
149
+ #
150
+ # # NOTE that while the endogenous level is removed, the coordinates keep the exogenous slice!
151
+ # (b) Sub Cube grain: ['customers.last_name']
152
+ # Sub Cube Slice: {'customers.state', with: 'CA'}
153
+ # Memory Grain: ['customers.city', 'customers.country', 'customers.state']
154
+ # Operation: reset('customers.state')
155
+ # Current coordinates: {'customers.country' => 'USA', 'customers.city' => 'Boston', 'customers.state' => 'CA' }
156
+ # New coordinates: {'customers.country' => 'USA', 'customers.state' => 'CA' }
157
+ #
158
+ # # (c) - Adding exogenous slice
159
+ #
160
+ # # When the exogenous slice is not part of the memory grain
161
+ # (c) Sub Cube grain: ['customers.last_name', 'media_types.name']
162
+ # Sub Cube Slice: {'customers.state', with: 'CA'}
163
+ # Memory Grain: ['customers.city', 'customers.country']
164
+ # Operation: slice('media_types.name', with: 'MPEG')
165
+ # Current coordinates: {'customers.country' => 'USA', 'customers.city' => 'Boston', 'customers.state' => 'CA' }
166
+ # New coordinates: {'customers.country' => 'USA', 'customers.city' => 'Boston', 'customers.state' => 'CA', 'media_types.name' => 'MPEG' }
167
+ #
168
+ # # When exogesnour
169
+ # (c) Sub Cube grain: ['customers.last_name']
170
+ # Sub Cube Slice: {'customers.city', with: 'San Francisco', 'Boston', 'New York'}
171
+ # Memory Grain: ['customers.state']
172
+ # Operation: slice('customers.city', with: 'San Francisco')
173
+ # Current coordinates: {'customers.state' => 'MI' }
174
+ # New coordinates: <Empty Element>
175
+ #
176
+ #
177
+ # q = Cube.slice('customers.state', with: ['CA', 'NY', 'YS', 'BC']).granulate('customers.last_name', 'media_types.name').build
178
+ # all = q.elements(levels: ['customers.country', 'customers.city'])
179
+ # slice1 = q.slice('customers.state', with: 'NY').elements(levels: ['customers.country', 'customers.city'])
180
+ # # => The merging of slices is simple - it's on the same level
181
+ # slice2 = q.slice('customers.country', with: 'Canada').elements(levels: ['customers.country', 'customers.city'])
182
+ # # => The merging of slices is hard - it needs to go to the dimension, similar to coordinates resolver
183
+ # slice3 = q.slice('customers.city', with: 'Paris').elements(levels: ['customers.country', 'customers.city'])
184
+ # # => The merging of slices is medium - it will either be null or Paris.
185
+ #
186
+ # Memory slice and coordinate resolver are very similar! Coordinate resolver is basically applying a memory slice
187
+ # on the current element values. The only exception is that for coordinate resolver, slice3 is easy - it is assumed
188
+ # to exist.
@@ -0,0 +1,84 @@
1
+ module Martyr
2
+ module Runtime
3
+ class MemorySlice
4
+ include Martyr::Translations
5
+
6
+ attr_accessor :data_slice, :memory_slice_overrides
7
+ delegate :definition_resolver, :definition_object_for, to: :data_slice
8
+
9
+ def initialize(data_slice)
10
+ @data_slice = data_slice
11
+ @memory_slice_overrides = ScopeableSliceData.new
12
+ end
13
+
14
+ def inspect
15
+ to_hash.inspect
16
+ end
17
+
18
+ def override_values
19
+ memory_slice_overrides.values
20
+ end
21
+
22
+ def keys
23
+ (override_values.flat_map(&:keys) + data_slice.keys).uniq
24
+ end
25
+
26
+ def to_hash
27
+ overrides = override_values.inject({}) {|h, slice| h.merge! slice.to_hash}
28
+ data_slice.to_hash.merge!(overrides).slice(*keys)
29
+ end
30
+
31
+ # @param slice_on [String]
32
+ # @param slice_definition [Hash]
33
+ def slice(slice_on, slice_definition)
34
+ definition_resolver.validate_slice_on!(slice_on)
35
+ slice_on_object = definition_object_for(slice_on)
36
+ slice_id = slice_on_object.slice_id
37
+ memory_slice_overrides[slice_id] ||= slice_on_object.build_memory_slice(data_slice.slices[slice_id])
38
+ memory_slice_overrides[slice_id].set_slice(slice_on_object, **slice_definition.symbolize_keys)
39
+ self
40
+ end
41
+
42
+ # @see ElementLocator#locate. Currently only used for metrics slices.
43
+ def slice_hash(slice_hash)
44
+ slice_hash.each do |slice_on, slice_definition|
45
+ slice(slice_on, slice_definition)
46
+ end
47
+ self
48
+ end
49
+
50
+ # = Applying slices
51
+
52
+ # @param facts [Array<Fact>]
53
+ def apply_on(facts)
54
+ override_values.inject(facts.dup) do |selected_facts, memory_slice_override|
55
+ memory_slice_override.apply_on(selected_facts)
56
+ end
57
+ end
58
+
59
+ # @param sub_cube [SubCube]
60
+ # @return [MemorySlice] new object with new DataSlice and ScopeableDataSliceData objects, both set to be scoped
61
+ # to the sub cube
62
+ def for_cube(sub_cube)
63
+ dup.for_cube!(sub_cube)
64
+ end
65
+
66
+ # @param sub_cube [SubCube]
67
+ # @return [MemorySlice] same object with new DataSlice and ScopeableDataSliceData objects, both set to be scoped
68
+ # to the sub cube
69
+ def for_cube!(sub_cube)
70
+ self.data_slice = data_slice.for_cube(sub_cube)
71
+ self.memory_slice_overrides = memory_slice_overrides.scope(sub_cube)
72
+ self
73
+ end
74
+
75
+ def dup_internals
76
+ dup.instance_eval do
77
+ @memory_slice_overrides = memory_slice_overrides.data_dup
78
+ self
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,59 @@
1
+ module Martyr
2
+ module Runtime
3
+ class MetricMemorySlice
4
+ attr_reader :metric
5
+ delegate :to_hash, to: :get_slice
6
+ delegate :cube_name, to: :metric
7
+ delegate :id, to: :metric, prefix: true
8
+
9
+ # @param metric [BaseMetricDefinition]
10
+ # @option data_slice [MetricDataSlice, nil] data slice from sub cube if exists
11
+ def initialize(metric, data_slice = nil)
12
+ @metric = metric
13
+ @data_slice = data_slice
14
+ end
15
+
16
+ def keys
17
+ [metric_id]
18
+ end
19
+
20
+ def set_slice(_metric_definition, **options)
21
+ raise Martyr::Error.new('Internal error. Inconsistent metric received') unless _metric_definition.id == metric_id
22
+ new_slice_definition = MetricSliceDefinition.new(metric: metric, **options)
23
+ if @data_slice.blank?
24
+ if @slice_definition.blank?
25
+ @slice_definition = new_slice_definition
26
+ else
27
+ @slice_definition = new_slice_definition.merge(@slice_definition)
28
+ end
29
+ else
30
+ @slice_definition = new_slice_definition.merge(data_slice)
31
+ end
32
+ end
33
+
34
+ def get_slice(_metric_id = nil)
35
+ validate_consistency!(_metric_id || self.metric_id) and @slice_definition
36
+ end
37
+
38
+ # = Applying
39
+
40
+ def apply_on(facts)
41
+ get_slice.combined_statements.inject(facts) do |selected_facts, or_statement_group|
42
+ selected_facts.select do |fact|
43
+ or_statement_group.inject(false) do |logic_resolve, statement|
44
+ logic_resolve or fact.fetch(metric_id).send(statement[:memory_operator], statement[:value])
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def validate_consistency!(metric_id)
53
+ raise Martyr::Error.new('Internal error. Inconsistent metric received') unless metric_id == self.metric_id
54
+ true
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,48 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PlainDimensionMemorySlice
4
+
5
+ include Martyr::Runtime::HasScopedLevels
6
+
7
+ attr_reader :dimension_definition, :data_slice, :levels
8
+ delegate :keys, to: :levels
9
+
10
+ # @param dimension_definition [PlainDimensionDefinition]
11
+ # @option data_slice [PlainDimensionDataSlice, nil] data slice from sub cube if exists
12
+ def initialize(dimension_definition, data_slice = nil)
13
+ @dimension_definition = dimension_definition
14
+ @data_slice = data_slice
15
+ @levels = {}
16
+ end
17
+
18
+ def to_hash
19
+ arr = scoped_levels.values.sort_by{|slice| slice.level.to_i}.inject({}){|h, slice| h.merge!(slice.to_hash) }
20
+ Hash[arr]
21
+ end
22
+
23
+ def set_slice(level, **options)
24
+ new_slice_definition = PlainDimensionLevelSliceDefinition.new(level: level, **options)
25
+ if data_slice.try(:get_slice, level.id).blank?
26
+ @levels[level.id] = new_slice_definition
27
+ else
28
+ @levels[level.id] = new_slice_definition.merge(data_slice.get_slice(level.id))
29
+ end
30
+ end
31
+
32
+ # @return [PlainDimensionLevelSliceDefinition]
33
+ def get_slice(level_id)
34
+ scoped_levels[level_id]
35
+ end
36
+
37
+ def apply_on(facts)
38
+ scoped_levels.keys.inject(facts) do |selected_facts, level_id|
39
+ whitelist_arr = get_slice(level_id).with.map(&:to_s)
40
+ selected_facts.select do |fact|
41
+ whitelist_arr.include? fact.fact_key_for(level_id).to_s
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,73 @@
1
+ module Martyr
2
+ module Runtime
3
+ class ScopeableSliceData
4
+
5
+ # Scoping applies on #keys, #values, #to_hash.
6
+
7
+ delegate :[], :[]=, :select!, :reject!, to: :@data
8
+ delegate :keys, :values, to: :to_hash
9
+
10
+ def initialize(data = {})
11
+ @data = data
12
+ end
13
+
14
+ # @return [ScopeableDataSliceData] new instance scoped to cube
15
+ def scope(sub_cube)
16
+ dup.scope!(sub_cube)
17
+ end
18
+
19
+ def scope!(sub_cube)
20
+ @sub_cube = sub_cube
21
+ self
22
+ end
23
+
24
+ def to_hash
25
+ scoped_data
26
+ end
27
+
28
+ def select(&block)
29
+ obj = data_dup
30
+ obj.select!(&block)
31
+ obj
32
+ end
33
+
34
+ def reject(&block)
35
+ obj = data_dup
36
+ obj.reject!(&block)
37
+ obj
38
+ end
39
+
40
+ def data_dup
41
+ self.class.new(@data.dup)
42
+ end
43
+
44
+ private
45
+
46
+ def scoped_data
47
+ return @data unless @sub_cube
48
+ return @scoped_data if @scoped_data
49
+
50
+ cube_name = @sub_cube.cube_name
51
+ metrics_hash = @data.select do |_key, object|
52
+ object.respond_to?(:cube_name) and object.cube_name == cube_name
53
+ end
54
+
55
+ # We go through the cube so that ScopeableSliceData is the same regardless if we're dealing with data slice
56
+ # (can always perform WHERE clause) or memory slice (can only be applied on levels in the grain). The downside
57
+ # is that for memory slices the scoped_data may contain levels that cannot be sliced
58
+ supported_level_ids = @sub_cube.cube.supported_level_ids
59
+ supported_dimension_names = @sub_cube.dimension_definitions.keys
60
+ dimensions_arr = @data.select do |key, object|
61
+ !object.respond_to?(:cube_name) and
62
+ supported_dimension_names.include?(key) and
63
+ (object.keys - (object.keys - supported_level_ids)).present?
64
+ end.map do |key, object|
65
+ [key, object.scope(supported_level_ids)]
66
+ end
67
+
68
+ @scoped_data = metrics_hash.merge! Hash[dimensions_arr]
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ module Martyr
2
+ class BaseSliceDefinition
3
+ include ActiveModel::Model
4
+
5
+ def initialize(*)
6
+ super
7
+ compile_operators
8
+ end
9
+
10
+ def null?
11
+ !!@null
12
+ end
13
+
14
+ def self.null
15
+ obj = new
16
+ obj.send(:set_null) and return obj
17
+ end
18
+
19
+ protected
20
+
21
+ def set_null
22
+ @null = true
23
+ end
24
+
25
+ def compile_operators
26
+ raise NotImplementedError
27
+ end
28
+
29
+ end
30
+ end