martyr 0.1.74.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.tags +868 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/Rakefile +1 -0
- data/TODO.txt +54 -0
- data/bin/console +62 -0
- data/bin/setup +7 -0
- data/lib/martyr/base_cube.rb +73 -0
- data/lib/martyr/cube.rb +134 -0
- data/lib/martyr/dimension_reference.rb +26 -0
- data/lib/martyr/errors.rb +20 -0
- data/lib/martyr/helpers/delegators.rb +17 -0
- data/lib/martyr/helpers/intervals.rb +222 -0
- data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
- data/lib/martyr/helpers/registrable.rb +15 -0
- data/lib/martyr/helpers/sorter.rb +79 -0
- data/lib/martyr/helpers/translations.rb +34 -0
- data/lib/martyr/level_concern/has_level_collection.rb +11 -0
- data/lib/martyr/level_concern/level.rb +45 -0
- data/lib/martyr/level_concern/level_collection.rb +60 -0
- data/lib/martyr/level_concern/level_comparator.rb +45 -0
- data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
- data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
- data/lib/martyr/runtime/data_set/element.rb +66 -0
- data/lib/martyr/runtime/data_set/element_common.rb +51 -0
- data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
- data/lib/martyr/runtime/data_set/fact.rb +83 -0
- data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
- data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
- data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
- data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
- data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
- data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
- data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
- data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
- data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
- data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
- data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
- data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
- data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
- data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
- data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
- data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
- data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
- data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
- data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
- data/lib/martyr/runtime/query/query_context.rb +246 -0
- data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
- data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
- data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
- data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
- data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
- data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
- data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
- data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
- data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
- data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
- data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
- data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
- data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
- data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
- data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
- data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
- data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
- data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
- data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
- data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
- data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
- data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
- data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
- data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
- data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
- data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
- data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
- data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
- data/lib/martyr/schema/metrics/base_metric.rb +77 -0
- data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
- data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
- data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
- data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
- data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
- data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
- data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
- data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
- data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
- data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
- data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
- data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
- data/lib/martyr/version.rb +3 -0
- data/lib/martyr/virtual_cube.rb +74 -0
- data/lib/martyr.rb +55 -0
- data/martyr.gemspec +41 -0
- 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,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
|