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