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