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,108 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class Coordinates
|
|
4
|
+
include ActiveModel::Model
|
|
5
|
+
include Martyr::Translations
|
|
6
|
+
|
|
7
|
+
# @attribute coordinates_hash [Hash] with keys of the form:
|
|
8
|
+
# 'dimension_name.level_name'
|
|
9
|
+
# 'cube_name.metric_name'
|
|
10
|
+
# and values representing the slice
|
|
11
|
+
|
|
12
|
+
attr_reader :grain_hash, :memory_slice_hash
|
|
13
|
+
|
|
14
|
+
# @param grain_hash [Hash] of the structure level_id => value. For query levels, value is the primary key.
|
|
15
|
+
# Note that it does not include metrics, and other background restrictions on the slice
|
|
16
|
+
# @param memory_slice_hash [Hash]
|
|
17
|
+
def initialize(grain_hash, memory_slice_hash)
|
|
18
|
+
@grain_hash = grain_hash
|
|
19
|
+
@memory_slice_hash = memory_slice_hash
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Hash] of coordinates that can get the same element when sent to a QueryContextBuilder#slice
|
|
23
|
+
def to_hash
|
|
24
|
+
memory_slice_hash.merge(grain_coordinates)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Array<String>]
|
|
28
|
+
def grain_level_ids
|
|
29
|
+
grain_hash.keys
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Hash] of the grain in a format that is understood by QueryContextBuilder#slice
|
|
33
|
+
def grain_coordinates
|
|
34
|
+
grain_hash.inject({}) { |h, (level_id, level_value)| h[level_id] = {with: level_value}; h }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def dup
|
|
38
|
+
super.dup_internals
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reset(*args)
|
|
42
|
+
dup.reset!(*args)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set(*args)
|
|
46
|
+
dup.set!(*args)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def locate(*args)
|
|
50
|
+
dup.locate!(*args)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def locate!(slice_hash={}, reset: [])
|
|
54
|
+
reset.each { |reset_on| reset!(reset_on) }
|
|
55
|
+
set!(slice_hash)
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @param reset_on [String, Array<String>]
|
|
60
|
+
# The caller must guarantee that only levels and dimensions are sent to #reset!.
|
|
61
|
+
# Since this object is not hierarchy-aware, the caller must send all levels below the level asked to be reset.
|
|
62
|
+
#
|
|
63
|
+
# There are two acceptable formats:
|
|
64
|
+
# 'dimension_name.level_name' - to remove a particular level
|
|
65
|
+
# 'dimension_name.*' - to remove a dimension with all its levels
|
|
66
|
+
#
|
|
67
|
+
def reset!(reset_on)
|
|
68
|
+
if second_element_from_id(reset_on) == '*'
|
|
69
|
+
reset_dimension first_element_from_id(reset_on)
|
|
70
|
+
else
|
|
71
|
+
grain_hash.except!(reset_on)
|
|
72
|
+
end
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param slice_hash [Hash] of one of the formats:
|
|
77
|
+
# 'dimension_name.level_name' => {with: 'value'}
|
|
78
|
+
# 'dimension_name.level_name' => {'with' => 'value'}
|
|
79
|
+
#
|
|
80
|
+
# The caller must guarantee that only levels and dimensions are sent.
|
|
81
|
+
#
|
|
82
|
+
def set!(slice_hash)
|
|
83
|
+
slice_hash.group_by { |k, _| first_element_from_id(k) }.each do |dimension_name, slice_hashes_arr|
|
|
84
|
+
reset_dimension dimension_name
|
|
85
|
+
slice_hashes_arr.each do |slice_on, slice_definition|
|
|
86
|
+
raise Query::Error.new('incorrect usage of locate') unless slice_definition.stringify_keys.keys == ['with']
|
|
87
|
+
grain_hash.merge! slice_on => slice_definition.stringify_keys['with']
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
protected
|
|
94
|
+
|
|
95
|
+
def dup_internals
|
|
96
|
+
@grain_hash = @grain_hash.dup
|
|
97
|
+
self
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def reset_dimension(dimension_name)
|
|
103
|
+
grain_hash.reject! { |k, _| first_element_from_id(k) == dimension_name }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class Element < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::Translations
|
|
5
|
+
include Martyr::Runtime::ElementCommon
|
|
6
|
+
|
|
7
|
+
# @attribute element_locator [ElementLocator] this is added to an element in the process of building it
|
|
8
|
+
# @attribute helper_module [Module] this is kept for reference, although the element object should already extend it
|
|
9
|
+
attr_accessor :element_locator, :helper_module
|
|
10
|
+
|
|
11
|
+
attr_reader :facts
|
|
12
|
+
delegate :empty?, to: :facts
|
|
13
|
+
delegate :cube_name, to: :element_locator
|
|
14
|
+
delegate :grain_level_ids, :grain_hash, to: :@coordinates
|
|
15
|
+
|
|
16
|
+
# @param coordinates [Coordinates]
|
|
17
|
+
# @param values_hash [Hash] of the structure level_id => value. Unlike `coordinates.grain_hash`, for query levels the
|
|
18
|
+
# value is the string value, not the primary key.
|
|
19
|
+
# @param facts [Array<Fact>]
|
|
20
|
+
def initialize(coordinates, values_hash, facts)
|
|
21
|
+
@coordinates = coordinates
|
|
22
|
+
@facts = facts
|
|
23
|
+
merge! values_hash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param key [String] either metric id or level id
|
|
27
|
+
def fetch(key)
|
|
28
|
+
value = super(key)
|
|
29
|
+
value.is_a?(FutureMetric) ? value.value : value
|
|
30
|
+
end
|
|
31
|
+
alias_method :[], :fetch
|
|
32
|
+
|
|
33
|
+
def coordinates
|
|
34
|
+
@coordinates.to_hash
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def coordinates_object
|
|
38
|
+
@coordinates
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def locate(*args)
|
|
42
|
+
element_locator.locate(grain_hash, *args)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Loads all future values
|
|
46
|
+
def load
|
|
47
|
+
keys.each{|key| fetch(key)}
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def key_for(level_id)
|
|
52
|
+
raise Query::Error.new("Error: `#{level_id}` must be included in the element grain") unless coordinates.keys.include?(level_id)
|
|
53
|
+
facts.first.fact_key_for(level_id)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def record_for(level_id)
|
|
57
|
+
raise Query::Error.new("Error: `#{level_id}` must be included in the element grain") unless coordinates.keys.include?(level_id)
|
|
58
|
+
facts.first.record_for(level_id)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def has_metric_id?(metric_id)
|
|
62
|
+
metric_ids.include? MetricIdStandardizer.new(cube_name).standardize(metric_id)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
|
|
4
|
+
# Requirements:
|
|
5
|
+
# The object must have the #store method
|
|
6
|
+
|
|
7
|
+
module ElementCommon
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
attr_reader :metrics_hash
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Grain and coordinates are different.
|
|
15
|
+
# Coordinates can include "background" multi-value slices such as: 'media.types' => ['a', 'b', 'c']
|
|
16
|
+
# Grain always have one value for each level
|
|
17
|
+
# Coordinates always include the grain.
|
|
18
|
+
|
|
19
|
+
def grain_has_level_id?(level_id)
|
|
20
|
+
grain_level_ids.include?(level_id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def coordinates_have_level_id?(level_id)
|
|
24
|
+
coordinates.keys.include?(level_id)
|
|
25
|
+
end
|
|
26
|
+
alias_method :has_level_id?, :coordinates_have_level_id?
|
|
27
|
+
|
|
28
|
+
def metrics
|
|
29
|
+
metrics_hash.values
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def metric_ids
|
|
33
|
+
metrics_hash.keys
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param metrics [Array<BaseMetric>]
|
|
37
|
+
# @return [self]
|
|
38
|
+
def rollup(*metrics)
|
|
39
|
+
@metrics_hash ||= {}
|
|
40
|
+
metrics.each do |metric|
|
|
41
|
+
next if @metrics_hash[metric.id]
|
|
42
|
+
value = empty? ? 0 : FutureMetric.wrap(self, metric, :rollup)
|
|
43
|
+
store metric.id, value
|
|
44
|
+
@metrics_hash[metric.id] = metric
|
|
45
|
+
end
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
|
|
4
|
+
# This is a service object that allows locating an element given element coordinates and "locate instructions".
|
|
5
|
+
# It is structured to allow locating elements even in the absence of a real element to "start from".
|
|
6
|
+
|
|
7
|
+
# A locator deals with real elements - meaning belonging to one cube.
|
|
8
|
+
|
|
9
|
+
class ElementLocator
|
|
10
|
+
include ActiveModel::Model
|
|
11
|
+
include Martyr::Translations
|
|
12
|
+
|
|
13
|
+
# @attribute metrics [Array<BaseMetric>] the metrics that should be rolled up on the located element
|
|
14
|
+
# @attribute memory_slice [MemorySlice] the current memory slice. Sent to FactIndexer and used to build
|
|
15
|
+
# Coordinate objects.
|
|
16
|
+
# @attribute fact_indexer [#dimension_bus, #get_element, #cube_name]
|
|
17
|
+
# @attribute restrict_level_ids [Array<String>] level IDs that are supported by the cube this locator belongs to.
|
|
18
|
+
# @attribute helper_module [Module] a module to be included into every element
|
|
19
|
+
# @attribute standardizer [MetricIdStandardizer] used to standardize user input given in procs calling locate
|
|
20
|
+
attr_accessor :metrics, :memory_slice, :fact_indexer, :restrict_level_ids, :helper_module, :standardizer
|
|
21
|
+
|
|
22
|
+
delegate :dimension_bus, :cube_name, to: :fact_indexer
|
|
23
|
+
delegate :definition_from_id, to: :dimension_bus
|
|
24
|
+
|
|
25
|
+
# @param level_ids [Array<String>] the granularity at which the elements need to be fetched
|
|
26
|
+
# @return [Array<Element>]
|
|
27
|
+
def all(level_ids)
|
|
28
|
+
Schema::CountDistinctMetric.enable_rollup_strategy_caching(metrics) do
|
|
29
|
+
fact_indexer.elements_by(memory_slice, level_ids).map {|element| finalize_element(element)}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get an element based on coordinates.
|
|
34
|
+
# If the coordinates contain an unsupported level, it returns nil.
|
|
35
|
+
# @param grain_hash [Hash] see Coordinates. We do not need Coordinates here so we prefer to allow sending in a
|
|
36
|
+
# Hash.
|
|
37
|
+
# @param exclude_metric_id [nil, String, Array<String>] @see #finalize_elements
|
|
38
|
+
def get(grain_hash, exclude_metric_id: nil, memory_slice: nil)
|
|
39
|
+
memory_slice ||= self.memory_slice
|
|
40
|
+
|
|
41
|
+
elm = fact_indexer.get_element(memory_slice, grain_hash) unless restrict_level_ids.present? and
|
|
42
|
+
(grain_hash.keys - restrict_level_ids).present?
|
|
43
|
+
|
|
44
|
+
elm ||= unfinalized_empty_element(grain_hash, memory_slice: memory_slice)
|
|
45
|
+
finalize_element(elm, exclude_metric_id: exclude_metric_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get an element based on existing coordinates hash AND changes instructions sent to #locate
|
|
49
|
+
# @param grain_hash [Hash] base coordinates that are to be manipulated.
|
|
50
|
+
# @param *several_variants
|
|
51
|
+
# Variant 1:
|
|
52
|
+
# level_id [String] level ID to slice
|
|
53
|
+
# with [String] value at level
|
|
54
|
+
# Variant 2:
|
|
55
|
+
# slice_hash [Hash] level IDs and their values to slice
|
|
56
|
+
# @option reset [String, Array<String>] level ids to remove from coordinates
|
|
57
|
+
# @option standardizer [MetricIdStandardizer]
|
|
58
|
+
# @option exclude_metric_id [String, Array<String>] @see finalize_element
|
|
59
|
+
#
|
|
60
|
+
# @examples
|
|
61
|
+
# locate(coords, 'customers.country', with: 'USA', reset: '')
|
|
62
|
+
def locate(grain_hash, *several_variants)
|
|
63
|
+
slice_hash, reset_arr, options = sanitize_args_for_locate(*several_variants)
|
|
64
|
+
dimensions_slice_hash, metrics_slice_hash = separate_dimensions_and_metrics(slice_hash)
|
|
65
|
+
metrics_slice_hash = standardizer.standardize(metrics_slice_hash)
|
|
66
|
+
new_memory_slice = metrics_slice_hash.present? ? memory_slice.dup_internals.slice_hash(metrics_slice_hash) : memory_slice
|
|
67
|
+
new_coords = coordinates_from_grain_hash(grain_hash, memory_slice: new_memory_slice).locate(dimensions_slice_hash, reset: reset_arr)
|
|
68
|
+
get(new_coords.grain_hash, memory_slice: new_memory_slice, **options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def empty_element(grain_hash={}, memory_slice: nil)
|
|
72
|
+
finalize_element unfinalized_empty_element(grain_hash, memory_slice: memory_slice)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# @param grain_hash [Hash]
|
|
78
|
+
# @return [Coordinates]
|
|
79
|
+
def coordinates_from_grain_hash(grain_hash, memory_slice: nil)
|
|
80
|
+
memory_slice ||= self.memory_slice
|
|
81
|
+
Coordinates.new(grain_hash, memory_slice.to_hash)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param element [Hash] element that does not have metrics rolled up and whose element_locator is missing
|
|
85
|
+
# @return [Element] fully initialized
|
|
86
|
+
def finalize_element(element, exclude_metric_id: nil)
|
|
87
|
+
exclude_metric_id = Array.wrap(exclude_metric_id)
|
|
88
|
+
element.element_locator = self
|
|
89
|
+
element.helper_module = helper_module
|
|
90
|
+
element.extend(helper_module) if helper_module.present?
|
|
91
|
+
element.rollup *metrics.reject{|m| exclude_metric_id.include? m.id.to_s }
|
|
92
|
+
element
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def sanitize_args_for_locate(*several_variants)
|
|
96
|
+
if several_variants.length == 2
|
|
97
|
+
slice_definition, reset_arr, options = extract_options_for_locate(several_variants[1])
|
|
98
|
+
slice_hash = {several_variants[0] => slice_definition}
|
|
99
|
+
elsif several_variants.length == 1 and several_variants.first.is_a?(Hash)
|
|
100
|
+
slice_hash, reset_arr, options = extract_options_for_locate(several_variants.first)
|
|
101
|
+
else
|
|
102
|
+
raise ArgumentError.new("wrong number of arguments #{several_variants.length} for (1..2)")
|
|
103
|
+
end
|
|
104
|
+
standardizer = options.delete(:standardizer) || MetricIdStandardizer.new
|
|
105
|
+
|
|
106
|
+
validate_no_metrics standardizer.standardize(reset_arr)
|
|
107
|
+
|
|
108
|
+
[standardizer.standardize(slice_hash), standardizer.standardize(reset_arr), options]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def extract_options_for_locate(hash)
|
|
112
|
+
option_keys = [:standardizer, :exclude_metric_id]
|
|
113
|
+
hash_dup = hash.dup
|
|
114
|
+
|
|
115
|
+
options = hash_dup.slice(*option_keys)
|
|
116
|
+
reset = hash_dup.delete(:reset) || hash_dup.delete('reset')
|
|
117
|
+
slice = hash_dup.except!(*option_keys)
|
|
118
|
+
[slice, Array.wrap(reset), options]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @param hash [Hash] of keys and set instructions
|
|
122
|
+
# @return [Hash, Hash] first hash is dimensions, second is metrics
|
|
123
|
+
def separate_dimensions_and_metrics(hash)
|
|
124
|
+
dimension_keys = hash.keys.select{|id| definition_from_id(first_element_from_id(id)).respond_to?(:dimension?)}
|
|
125
|
+
[hash.slice(*dimension_keys), hash.except(*dimension_keys)]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param ids_array [Array] array of fully qualified IDs that contain either metrics or dimensions
|
|
129
|
+
def validate_no_metrics(ids_array)
|
|
130
|
+
ids_array.each do |id|
|
|
131
|
+
raise Query::Error.new('Can only reset on dimensions') unless
|
|
132
|
+
definition_from_id(first_element_from_id(id)).respond_to?(:dimension?)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def unfinalized_empty_element(grain_hash, memory_slice: nil)
|
|
137
|
+
coordinates = coordinates_from_grain_hash(grain_hash, memory_slice: memory_slice)
|
|
138
|
+
Element.new(coordinates, {}, [])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class Fact < Hash
|
|
4
|
+
include Martyr::LevelComparator
|
|
5
|
+
include Martyr::Translations
|
|
6
|
+
|
|
7
|
+
attr_reader :sub_cube, :raw, :grain_level_ids
|
|
8
|
+
delegate :dimension_bus, to: :sub_cube
|
|
9
|
+
|
|
10
|
+
def initialize(sub_cube, query_result_hash)
|
|
11
|
+
@sub_cube = sub_cube
|
|
12
|
+
@raw = query_result_hash
|
|
13
|
+
@grain_level_ids = []
|
|
14
|
+
merge_value_by_levels_hash
|
|
15
|
+
merge_built_in_metrics_hash
|
|
16
|
+
merge_custom_metrics_hash
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
alias_method :hash_fetch, :fetch
|
|
20
|
+
|
|
21
|
+
# @param id [String] either metric id or level id
|
|
22
|
+
def fetch(id)
|
|
23
|
+
value = hash_fetch(fully_qualify_id(id))
|
|
24
|
+
value.is_a?(FutureFactValue) || value.is_a?(FutureMetric) ? value.value : value
|
|
25
|
+
end
|
|
26
|
+
alias_method :[], :fetch
|
|
27
|
+
|
|
28
|
+
# Similar to fetch, but returns the original fact_key_value if such existed for the level
|
|
29
|
+
def fact_key_for(level_id)
|
|
30
|
+
value = hash_fetch(level_id)
|
|
31
|
+
value.is_a?(FutureFactValue) ? (value.fact_key_value || value.value) : value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Similar to fetch, but returns the active record object if such existed for the level
|
|
35
|
+
def record_for(level_id)
|
|
36
|
+
value = hash_fetch(level_id)
|
|
37
|
+
value.is_a?(FutureFactValue) ? (value.active_record || value.value) : value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load
|
|
41
|
+
keys.each{|key| fetch(key)}
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def merge_value_by_levels_hash
|
|
48
|
+
sub_cube.fact_levels_filler_hash.each do |level_id, filler|
|
|
49
|
+
store level_id, filler.value(self)
|
|
50
|
+
@grain_level_ids << level_id
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def merge_built_in_metrics_hash
|
|
55
|
+
sub_cube.built_in_metrics.each do |metric|
|
|
56
|
+
store metric.id, metric.extract(self)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# This has to occur after merging the built_in_metrics_hash so that the user custom code can fetch
|
|
61
|
+
# existing metrics. We merge them one after the other so custom metrics can depend on one another.
|
|
62
|
+
def merge_custom_metrics_hash
|
|
63
|
+
sub_cube.custom_metrics.each do |metric|
|
|
64
|
+
store metric.id, FutureMetric.wrap(self, metric, :extract)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fully_qualify_id(id)
|
|
69
|
+
with_standard_id(id) do |dimension_or_cube_or_metric, level_or_metric|
|
|
70
|
+
level_or_metric ? id : "#{sub_cube.cube_name}.#{dimension_or_cube_or_metric}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def method_missing(method, *args, &block)
|
|
77
|
+
return fetch(method) if has_key? fully_qualify_id(method)
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class FactIndexer
|
|
4
|
+
|
|
5
|
+
attr_reader :sub_cube, :facts
|
|
6
|
+
delegate :dimension_bus, :cube_name, to: :sub_cube
|
|
7
|
+
|
|
8
|
+
def initialize(sub_cube, facts)
|
|
9
|
+
@sub_cube = sub_cube
|
|
10
|
+
@facts = facts
|
|
11
|
+
@indices = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @param memory_slice [MemorySlice] scoped to the current cube
|
|
15
|
+
# @param level_ids [Array<String>] level ids used to group facts by
|
|
16
|
+
# @return [Array<Element>] creates an array of elements. Each elements holds multiple facts based on
|
|
17
|
+
# level_keys_arr.
|
|
18
|
+
#
|
|
19
|
+
# Example:
|
|
20
|
+
# Consider facts with the following grain:
|
|
21
|
+
# media_types.name genres.name customers.country Metric 1 Metric 2
|
|
22
|
+
# 1 MPEG audio file Rock USA 100 20
|
|
23
|
+
# 2 MPEG audio file Pop USA 76 32
|
|
24
|
+
# 3 MPEG audio file Jazz USA 98 16
|
|
25
|
+
# 4 AAC audio file Rock USA 57 25
|
|
26
|
+
# 5 AAC audio file Pop USA 98 72
|
|
27
|
+
# 6 AAC audio file Jazz USA 34 18
|
|
28
|
+
#
|
|
29
|
+
# Running `elements_by 'genres.name'` will return 3 elements:
|
|
30
|
+
# genres.name Contained facts
|
|
31
|
+
# Rock 1,4
|
|
32
|
+
# Pop 2,5
|
|
33
|
+
# Jazz 3,6
|
|
34
|
+
#
|
|
35
|
+
# In addition, each element will have the metrics, rolled-up based on their roll-up function
|
|
36
|
+
#
|
|
37
|
+
def elements_by(memory_slice, level_ids)
|
|
38
|
+
elements_hash(memory_slice, level_ids).values
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @param memory_slice [MemorySlice]
|
|
42
|
+
# @param grain_hash [Hash] see Coordinates
|
|
43
|
+
# @return [Element] that resides in the provided coordinates
|
|
44
|
+
def get_element(memory_slice, grain_hash)
|
|
45
|
+
grain_hash_values_sorted_by_level_id = grain_hash.keys.sort.map{|x| grain_hash[x]}
|
|
46
|
+
elements_hash(memory_slice, grain_hash.keys)[grain_hash_values_sorted_by_level_id]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# @return [Hash] { element_key => Element }
|
|
52
|
+
# where element_key is array of values for levels in the same order of level_ids.
|
|
53
|
+
def elements_hash(memory_slice, level_ids)
|
|
54
|
+
sorted_level_ids = level_ids.sort
|
|
55
|
+
index_key = {slice: memory_slice.to_hash, levels: sorted_level_ids}
|
|
56
|
+
return @indices[index_key] if @indices[index_key]
|
|
57
|
+
|
|
58
|
+
arr = memory_slice.apply_on(facts).group_by do |fact|
|
|
59
|
+
sorted_level_ids.map{|id| fact.fact_key_for(id)}
|
|
60
|
+
end.map do |element_key, facts_arr|
|
|
61
|
+
grain_arr = sorted_level_ids.each_with_index.map{|level_id, i| [level_id, element_key[i]]}
|
|
62
|
+
coordinates = Coordinates.new(Hash[grain_arr], memory_slice.to_hash)
|
|
63
|
+
|
|
64
|
+
representative = facts_arr.first
|
|
65
|
+
values_arr = sorted_level_ids.map{|id| [id, representative.fetch(id)]}
|
|
66
|
+
[element_key, Element.new(coordinates, Hash[values_arr], facts_arr)]
|
|
67
|
+
end
|
|
68
|
+
@indices[index_key] = Hash[arr]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class FutureFactValue
|
|
4
|
+
|
|
5
|
+
attr_reader :fact_record, :level, :fact_key_value, :key_supported
|
|
6
|
+
delegate :dimension_bus, to: :fact_record
|
|
7
|
+
|
|
8
|
+
# @param fact_record [Fact]
|
|
9
|
+
# @param level [BaseLevelDefinition] the level that needs to be fetched
|
|
10
|
+
# @param key_supported [Boolean] if true, this means the fact has direct association with the level, and the
|
|
11
|
+
# value simply needs to be fetched from the dimension. fact_key_value must be provided when true.
|
|
12
|
+
# @param fact_key_value [String, Integer] only relevant if key_supported is true.
|
|
13
|
+
def initialize(fact_record, level, key_supported:, fact_key_value: nil)
|
|
14
|
+
@level = level
|
|
15
|
+
@fact_record = fact_record
|
|
16
|
+
@fact_key_value = fact_key_value.try(:to_i)
|
|
17
|
+
@key_supported = key_supported
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def inspect
|
|
21
|
+
value_loaded? ? @value.inspect : '?'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def value
|
|
25
|
+
return @value if value_loaded?
|
|
26
|
+
if key_supported
|
|
27
|
+
@active_record = dimension_bus.fetch_supported_query_level_record(level.id, fact_key_value)
|
|
28
|
+
@value = level.record_value(@active_record)
|
|
29
|
+
else
|
|
30
|
+
value = dimension_bus.fetch_unsupported_level_value(level.id, fact_record)
|
|
31
|
+
if level.degenerate?
|
|
32
|
+
@value = value
|
|
33
|
+
else
|
|
34
|
+
@active_record = value
|
|
35
|
+
@value = level.record_value(@active_record)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
@value_loaded = true
|
|
39
|
+
@value
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def active_record
|
|
43
|
+
value unless value_loaded?
|
|
44
|
+
@active_record
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def value_loaded?
|
|
48
|
+
!!@value_loaded
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
def ==(other)
|
|
54
|
+
value == other.value and fact_key_value == other.fact_key_value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# This class is used in two cases:
|
|
2
|
+
# For facts:
|
|
3
|
+
# To prevent calculation of custom metrics as user code may rely on dimension initialization during
|
|
4
|
+
# the process of figuring out which keys should be initialized.
|
|
5
|
+
#
|
|
6
|
+
# For elements:
|
|
7
|
+
# To prevent infinite recursion that can be caused if a cube has two custom rollups that call `locate`.
|
|
8
|
+
# The result would be that the calculations oscillates between the two metrics on the newly located element.
|
|
9
|
+
|
|
10
|
+
module Martyr
|
|
11
|
+
module Runtime
|
|
12
|
+
class FutureMetric
|
|
13
|
+
|
|
14
|
+
# @param element_or_fact [Element, Fact]
|
|
15
|
+
# @param metric [BaseMetric]
|
|
16
|
+
# @param method [:rollup, :extract]
|
|
17
|
+
def self.wrap(element_or_fact, metric, method)
|
|
18
|
+
if metric.is_a?(Martyr::Schema::BuiltInMetric)
|
|
19
|
+
metric.send(method, element_or_fact)
|
|
20
|
+
else
|
|
21
|
+
proc = -> { metric.send(method, element_or_fact) }
|
|
22
|
+
new(proc)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(proc)
|
|
27
|
+
@proc = proc
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def inspect
|
|
31
|
+
@value_retrieved ? @value.inspect : '?'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def value
|
|
35
|
+
@value_retrieved = true
|
|
36
|
+
@value ||= @proc.call
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|