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,131 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
|
|
4
|
+
# A virtual element is an element that does not really exist, because its grain is comprised of multiple cubes
|
|
5
|
+
# that do not make sense together.
|
|
6
|
+
#
|
|
7
|
+
# It is only useful in the context of relaxing some dimensions in the process of building totals.
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
#
|
|
11
|
+
# Cube 1
|
|
12
|
+
# genres.name media_types.name playlists.name tracks_count
|
|
13
|
+
# Rock CD My playlist 10
|
|
14
|
+
#
|
|
15
|
+
# Cube 2
|
|
16
|
+
# genres.name media_types.name customers.country units_sold
|
|
17
|
+
# Rock CD France 95
|
|
18
|
+
#
|
|
19
|
+
# Virtual elements
|
|
20
|
+
# genres.name media_types.name playlists.name customers.country tracks_count units_sold
|
|
21
|
+
# Rock CD My playlist
|
|
22
|
+
#
|
|
23
|
+
|
|
24
|
+
class VirtualElement
|
|
25
|
+
include Martyr::Runtime::ElementCommon
|
|
26
|
+
|
|
27
|
+
attr_reader :grain_hash, :locators, :real_elements, :memory_slice, :future_virtual_metrics_hash
|
|
28
|
+
delegate :inspect, :has_key?, to: :to_hash
|
|
29
|
+
delegate :store, to: :future_virtual_metrics_hash
|
|
30
|
+
|
|
31
|
+
def to_hash
|
|
32
|
+
grains_and_virtual_metrics = grain_hash.merge(future_virtual_metrics_hash)
|
|
33
|
+
real_elements.inject(grains_and_virtual_metrics) {|h, element| h.merge element.to_hash}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param memory_slice [MemorySlice] cross cubes memory slice
|
|
37
|
+
# @option real_elements [Array<Element>] note that an empty array is a valid input, representing an empty virtual
|
|
38
|
+
# element.
|
|
39
|
+
def initialize(grain_hash, memory_slice, locators, real_elements = nil)
|
|
40
|
+
@grain_hash = grain_hash
|
|
41
|
+
@memory_slice = memory_slice
|
|
42
|
+
@locators = locators
|
|
43
|
+
@real_elements = real_elements || find_real_elements(:get)
|
|
44
|
+
@future_virtual_metrics_hash = {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def empty?
|
|
48
|
+
real_elements.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def coordinates
|
|
52
|
+
coordinates_object.to_hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def coordinates_object
|
|
56
|
+
representative.coordinates_object
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def facts(cube_name = nil)
|
|
60
|
+
facts = Hash[real_elements.map {|x| [x.cube_name, x.facts]}]
|
|
61
|
+
cube_name ? facts[cube_name] : facts
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch(key)
|
|
65
|
+
send(:[], key)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def [](key)
|
|
69
|
+
value = to_hash[key]
|
|
70
|
+
value.is_a?(FutureMetric) ? value.value : value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def locate(*args)
|
|
74
|
+
new_real_elements = find_real_elements(:locate, *args)
|
|
75
|
+
VirtualElement.new(representative(*args).grain_hash, memory_slice, locators, new_real_elements)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def key_for(level_id)
|
|
79
|
+
representative.key_for(level_id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def record_for(level_id)
|
|
83
|
+
representative.record_for(level_id)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def grain_level_ids
|
|
87
|
+
grain_hash.keys
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def has_metric_id?(metric_id)
|
|
91
|
+
metric_ids.include?(metric_id)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def warnings
|
|
95
|
+
arr = real_elements.flat_map do |elm|
|
|
96
|
+
elm.metrics.map{|metric| [metric.id, warning(metric.id)]}
|
|
97
|
+
end
|
|
98
|
+
Hash[arr]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def warning(metric_id)
|
|
102
|
+
unsupported_keys_in_memory_slice = memory_slice.keys - coordinates(metric_id).keys
|
|
103
|
+
return [] if unsupported_keys_in_memory_slice.blank?
|
|
104
|
+
"Metric `#{metric_id}` does not support slice on: `#{unsupported_keys_in_memory_slice.join('`, `')}`"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def load
|
|
108
|
+
real_elements.try(:each, &:load)
|
|
109
|
+
future_virtual_metrics_hash.values.each(&:value)
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# @param method_name [:get, :locate]
|
|
116
|
+
# @param args [Array] args for locate
|
|
117
|
+
def find_real_elements(method_name, *args)
|
|
118
|
+
locators.map do |locator|
|
|
119
|
+
locator.send(method_name, grain_hash, *args)
|
|
120
|
+
end.reject(&:empty?).compact
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Allows falling back to an empty real element if no real elements are found to calculate the new grain hash
|
|
124
|
+
# @return [Element] could be empty element
|
|
125
|
+
def representative(*locate_args)
|
|
126
|
+
real_elements.first || locators.first.locate(grain_hash, *locate_args)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class VirtualElementsBuilder
|
|
4
|
+
include Martyr::Translations
|
|
5
|
+
|
|
6
|
+
def initialize(memory_slice, unsliced_level_ids_in_grain:, virtual_metrics:)
|
|
7
|
+
@entries = []
|
|
8
|
+
@unsliced_level_ids_in_grain = unsliced_level_ids_in_grain
|
|
9
|
+
@memory_slice = memory_slice
|
|
10
|
+
@lookups_by_level_id = {}
|
|
11
|
+
@virtual_metrics = virtual_metrics
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param elements [Array<Element>]
|
|
15
|
+
# @param sliced [Boolean] true if cube has at least one sliced level or metric
|
|
16
|
+
def add(elements, sliced:)
|
|
17
|
+
return unless elements.present?
|
|
18
|
+
@entries << ElementsFromSubCube.new(elements, sliced, @lookups_by_level_id, @unsliced_level_ids_in_grain)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Let's take 3 cubes:
|
|
22
|
+
#
|
|
23
|
+
# Schools:
|
|
24
|
+
# Country
|
|
25
|
+
# City
|
|
26
|
+
# Name
|
|
27
|
+
#
|
|
28
|
+
# Courses:
|
|
29
|
+
# Topic
|
|
30
|
+
# Course name
|
|
31
|
+
#
|
|
32
|
+
# Students:
|
|
33
|
+
# Country
|
|
34
|
+
# City
|
|
35
|
+
# Name
|
|
36
|
+
#
|
|
37
|
+
# Cube 1 - city budgets for schools
|
|
38
|
+
# (D) schools.country
|
|
39
|
+
# (D) schools.city
|
|
40
|
+
# (M) budget
|
|
41
|
+
#
|
|
42
|
+
# Cube 2 - Student performance
|
|
43
|
+
# (D) schools.country
|
|
44
|
+
# (D) schools.city
|
|
45
|
+
# (D) schools.name
|
|
46
|
+
# (D) students.country
|
|
47
|
+
# (D) students.city
|
|
48
|
+
# (D) students.name
|
|
49
|
+
# (D) courses.topic
|
|
50
|
+
# (D) courses.name
|
|
51
|
+
# (M) grade
|
|
52
|
+
#
|
|
53
|
+
# Cube 3 - International statistics
|
|
54
|
+
# (D) courses.topic
|
|
55
|
+
# (M) importance_factor
|
|
56
|
+
#
|
|
57
|
+
# Cube 4 = National statistics
|
|
58
|
+
# (D) schools.country
|
|
59
|
+
# (D) courses.topic
|
|
60
|
+
# (M) national_importance_factor
|
|
61
|
+
#
|
|
62
|
+
# = Reports
|
|
63
|
+
#
|
|
64
|
+
# City budget vs. average grade
|
|
65
|
+
# schools.country schools.city courses.topic budget average grade
|
|
66
|
+
# USA Boston (total) 1500 8.3
|
|
67
|
+
# USA Boston Math 7.5
|
|
68
|
+
# USA Boston Literature 9.2
|
|
69
|
+
#
|
|
70
|
+
# Cube 3:
|
|
71
|
+
# Budgets for cities that have schools with courses of high international importance factor
|
|
72
|
+
# => NOT SUPPORTED. Cube 3 cannot be added because it shares no grain level
|
|
73
|
+
# schools.country schools.city budget
|
|
74
|
+
# USA Boston 1500
|
|
75
|
+
#
|
|
76
|
+
# Alternative via Cube 4:
|
|
77
|
+
# Budgets for cities whose countries have schools with courses of high national importance factor
|
|
78
|
+
#
|
|
79
|
+
# Slices on cubes 1 and 3 get applied on cube 2:
|
|
80
|
+
# Cube3.importance_factor = high (courses with high importance factor)
|
|
81
|
+
# Cube1.budget > 1000 (countries with at least one city budget bigger than 1000)
|
|
82
|
+
# schools.country = [USA, France, ...]
|
|
83
|
+
#
|
|
84
|
+
# schools.country courses.topic grade
|
|
85
|
+
#
|
|
86
|
+
#
|
|
87
|
+
# The algorithm is as follows:
|
|
88
|
+
# Go over every "sliced cube" - a cube that has at least one sliced level or metric.
|
|
89
|
+
# Take every dependent level and restrict its values
|
|
90
|
+
# Apply the restricted on every cube, including the dependent ones
|
|
91
|
+
#
|
|
92
|
+
# A cube that has no grain and no metric doesn't matter - it can't help with the dependent elements.
|
|
93
|
+
#
|
|
94
|
+
# USA
|
|
95
|
+
# schools.city
|
|
96
|
+
#
|
|
97
|
+
#
|
|
98
|
+
# Authors Posts Comments Reactions
|
|
99
|
+
# Country City Genre Name Type Rating Rating
|
|
100
|
+
#
|
|
101
|
+
# Cube 1: L1.1 L1.2 L2.1 L2.2 L3.1 L3.2
|
|
102
|
+
# Cube 2: L2.1 L2.2 L3.1
|
|
103
|
+
# Cube 3: L3.1 L3.2 L4.1
|
|
104
|
+
#
|
|
105
|
+
# Scenarios:
|
|
106
|
+
# L1.1 L1.2 L2.1 L2.2 L3.1 L3.2 L4.1
|
|
107
|
+
# 1-Slice x x
|
|
108
|
+
# 1-Grain x x
|
|
109
|
+
# #=> Show comment count by post name and comment type for all posts made by authors from Boston with a positive reaction.
|
|
110
|
+
#
|
|
111
|
+
# 2-Slice x
|
|
112
|
+
# 2-Grain x x
|
|
113
|
+
# #=> Show comment count by post name and comment rating for the post named 'My Thoughts'
|
|
114
|
+
#
|
|
115
|
+
#
|
|
116
|
+
# When we are in #build, the memory slice already occurred, meaning it restricted the elements from cube 1
|
|
117
|
+
# to include only the relevant (L2.2, L3.1) combinations and elements from cube 3 to include only relevant
|
|
118
|
+
# (L3.1) elements.
|
|
119
|
+
# #build then looks at the +intersection+ of L2.2 values and L3.1 values across all cubes.
|
|
120
|
+
#
|
|
121
|
+
def build
|
|
122
|
+
return @entries.first.elements if @entries.length == 1
|
|
123
|
+
@entries.each(&:restrict)
|
|
124
|
+
|
|
125
|
+
@entries.flat_map do |entry|
|
|
126
|
+
entry.restricted_elements.map do |element|
|
|
127
|
+
next if (level_ids_in_grain - element.grain_level_ids).present?
|
|
128
|
+
element.grain_hash
|
|
129
|
+
end.compact
|
|
130
|
+
end.uniq.map do |grain_hash|
|
|
131
|
+
elm = VirtualElement.new grain_hash, @memory_slice, @entries.map(&:element_locator)
|
|
132
|
+
elm.rollup(*@virtual_metrics)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def level_ids_in_grain
|
|
137
|
+
@level_ids_in_grain ||= @entries.map(&:level_ids).flatten.uniq.sort_by{|x| first_element_from_id(x)}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def grain_hash_with_nils
|
|
141
|
+
Hash[level_ids_in_grain.map{|level_id| [level_id, nil]}]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class ElementsFromSubCube
|
|
145
|
+
include ActiveModel::Model
|
|
146
|
+
|
|
147
|
+
attr_accessor :element_locator, :elements, :sliced
|
|
148
|
+
attr_reader :lookups_by_level_id, :unsliced_level_ids_in_grain, :level_ids
|
|
149
|
+
|
|
150
|
+
def initialize(elements, sliced, lookups_by_level_id, unsliced_level_ids_in_grain)
|
|
151
|
+
@elements = elements
|
|
152
|
+
@sliced = sliced
|
|
153
|
+
@lookups_by_level_id = lookups_by_level_id
|
|
154
|
+
|
|
155
|
+
representative = @elements.first
|
|
156
|
+
@element_locator = representative.element_locator
|
|
157
|
+
@level_ids = representative.grain_level_ids
|
|
158
|
+
@unsliced_level_ids_in_grain = @level_ids - (@level_ids - unsliced_level_ids_in_grain)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def restrict
|
|
162
|
+
return unless sliced
|
|
163
|
+
unsliced_level_ids_in_grain.each do |level_id|
|
|
164
|
+
restrict_one_level(level_id)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# @return [Array<Element>] elements whose non-sliced levels are restricted to the
|
|
169
|
+
# intersection of all participating cubes
|
|
170
|
+
def restricted_elements
|
|
171
|
+
unsliced_level_ids_in_grain.inject(elements) do |selected_elements, level_id|
|
|
172
|
+
selected_elements.select do |element|
|
|
173
|
+
restricted_values = lookups_by_level_id[level_id]
|
|
174
|
+
next(true) unless restricted_values.present?
|
|
175
|
+
|
|
176
|
+
value_in_element_for_level = element[level_id]
|
|
177
|
+
restricted_values[value_in_element_for_level]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def restrict_one_level(level_id)
|
|
185
|
+
if lookups_by_level_id.has_key?(level_id)
|
|
186
|
+
# Restrict if the level already exists
|
|
187
|
+
lookups_by_level_id[level_id].slice! *(lookups_by_level_id[level_id].keys & values_for(level_id))
|
|
188
|
+
else
|
|
189
|
+
# Add to lookup if this is first time the level_id is encountered
|
|
190
|
+
lookup_arr = values_for(level_id).map{|x| [x, true]}
|
|
191
|
+
lookups_by_level_id[level_id] = Hash[lookup_arr]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def values_for(level_id)
|
|
196
|
+
elements.map{|elm| elm[level_id]}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class BaseLevelScope
|
|
4
|
+
include Martyr::Level
|
|
5
|
+
include Martyr::LevelComparator
|
|
6
|
+
|
|
7
|
+
attr_accessor :level
|
|
8
|
+
attr_reader :collection
|
|
9
|
+
delegate :level_definition, :name, :dimension_name, :query?, :degenerate?, :to_i, to: :level
|
|
10
|
+
delegate :fact_key, :fact_alias, to: :level_definition
|
|
11
|
+
|
|
12
|
+
def initialize(collection, level)
|
|
13
|
+
@collection = collection
|
|
14
|
+
@level = level
|
|
15
|
+
@cache = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class DegenerateLevelScope < BaseLevelScope
|
|
4
|
+
|
|
5
|
+
delegate :query_level_with_finder, :query_level_key, :register_element_helper_methods, to: :level
|
|
6
|
+
delegate :nullify, to: :query_level_below
|
|
7
|
+
|
|
8
|
+
def sliceable?
|
|
9
|
+
!!query_level_below
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def slice_with(values)
|
|
13
|
+
query_level_below.decorate_scope do |scope|
|
|
14
|
+
query_level_with_finder.call(scope, values)
|
|
15
|
+
end
|
|
16
|
+
query_level_below.set_bottom_sliced_level
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def loaded?
|
|
20
|
+
query_level_below.loaded?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# TODO: load from fact when sliceable? is false
|
|
24
|
+
def load
|
|
25
|
+
return true if loaded?
|
|
26
|
+
query_level_below.load
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def all
|
|
31
|
+
loaded_cache.keys
|
|
32
|
+
end
|
|
33
|
+
alias_method :keys, :all
|
|
34
|
+
alias_method :all_values, :all
|
|
35
|
+
|
|
36
|
+
# Useful when the fact is attempting to resolve Degenerate 1 value:
|
|
37
|
+
# Degenerate 1
|
|
38
|
+
# Degenerate 2 ---- Cube
|
|
39
|
+
# Query 3
|
|
40
|
+
#
|
|
41
|
+
# @return [String] the value of the "Degenerate 1" parent
|
|
42
|
+
def recursive_lookup_up(degenerate_value, level:)
|
|
43
|
+
return degenerate_value if name == level.name
|
|
44
|
+
query_level_primary_key = query_level_below.record_primary_key(loaded_cache[degenerate_value])
|
|
45
|
+
query_level_below.recursive_lookup_up(query_level_primary_key, level: level)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def recursive_lookup_down(degenerate_values, level:)
|
|
49
|
+
degenerate_values = Array.wrap(degenerate_values)
|
|
50
|
+
return degenerate_values if name == level.name
|
|
51
|
+
query_level_records = degenerate_values.flat_map{|value| query_level_below.cached_records_by(query_level_key)[value]}
|
|
52
|
+
query_level_below.recursive_lookup_down(query_level_records, level: level)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
# The cache stored is basically one representative of the query level for each degenerate value.
|
|
58
|
+
# Assuming the hierarchy is strict, i.e. the state parent of "San Francisco" is always "California", we are able
|
|
59
|
+
# to infer correctly the hierarchy
|
|
60
|
+
def cache
|
|
61
|
+
@cache ||= query_level_below.all.index_by{|x| degenerate_value_from_query_record(x) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def loaded_cache
|
|
65
|
+
load and return cache
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
public
|
|
69
|
+
|
|
70
|
+
def degenerate_value_from_query_record(record)
|
|
71
|
+
record.send(query_level_key)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class DimensionScopeCollection < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::Registrable
|
|
5
|
+
include Martyr::Translations
|
|
6
|
+
|
|
7
|
+
attr_reader :dimension_definitions
|
|
8
|
+
alias_method :find_dimension, :find_or_error
|
|
9
|
+
alias_method :has_dimension?, :has_key?
|
|
10
|
+
|
|
11
|
+
def initialize(dimension_definitions)
|
|
12
|
+
@dimension_definitions = dimension_definitions
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
values.flat_map{|x| x.level_objects.map(&:id)}.inspect
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param level [Martyr::Level]
|
|
20
|
+
def register_level(level)
|
|
21
|
+
dimension_scope = find_or_nil(level.dimension_name) ||
|
|
22
|
+
register(Martyr::DimensionReference.new(level.dimension_definition, Runtime::LevelScopeCollection))
|
|
23
|
+
dimension_scope.register_level(level)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def find_level(level_id)
|
|
27
|
+
with_standard_id(level_id) { |dimension, level| find_dimension(dimension).find_level(level) }
|
|
28
|
+
end
|
|
29
|
+
alias_method :level_scope, :find_level
|
|
30
|
+
|
|
31
|
+
def level_scopes(level_ids)
|
|
32
|
+
Array.wrap(level_ids).map{|x| level_scope(x)}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param level_id [String]
|
|
36
|
+
# @yieldparam [BaseLevelScope]
|
|
37
|
+
def with_level_scope(level_id)
|
|
38
|
+
yield level_scope(level_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def level_loaded?(level_id)
|
|
42
|
+
level_scope(level_id).loaded?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param level_ids [Array<String>, String]
|
|
46
|
+
# @return [Array<BaseLevelScope>] lowest levels from the levels array in each dimension
|
|
47
|
+
def lowest_level_of(level_ids)
|
|
48
|
+
LevelDefinitionsByDimension.new(level_scopes(level_ids)).lowest_levels
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param level_ids [Array<String>, String]
|
|
52
|
+
# @return [String] lowest levels from the levels array in each dimension
|
|
53
|
+
def lowest_level_ids_of(level_ids)
|
|
54
|
+
lowest_level_of(level_ids).map(&:id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param level_ids [Array<String>, String]
|
|
58
|
+
# @return [Array<BaseLevelScope>] lowest levels from the levels array in each dimension, and all the level
|
|
59
|
+
# scopes above them
|
|
60
|
+
def levels_and_above_for(level_ids)
|
|
61
|
+
lowest_level_of(level_ids).flat_map(&:level_and_above)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param level_ids [Array<Martyr::Level>]
|
|
65
|
+
# @return [Array<String>]
|
|
66
|
+
def level_ids_and_above_for(level_ids)
|
|
67
|
+
levels_and_above_for(level_ids).map(&:id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def register_element_helper_methods(mod)
|
|
71
|
+
values.each do |dimension_reference|
|
|
72
|
+
dimension_reference.register_element_helper_methods(mod)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class LevelScopeCollection < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::LevelCollection
|
|
5
|
+
|
|
6
|
+
attr_accessor :bottom_level_sliced_i
|
|
7
|
+
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
super
|
|
10
|
+
@bottom_level_sliced_i = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param level [Martyr::Level]
|
|
14
|
+
def register_level(level)
|
|
15
|
+
register level.build(self)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|