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