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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.tags +868 -0
  7. data/.travis.yml +3 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +265 -0
  11. data/Rakefile +1 -0
  12. data/TODO.txt +54 -0
  13. data/bin/console +62 -0
  14. data/bin/setup +7 -0
  15. data/lib/martyr/base_cube.rb +73 -0
  16. data/lib/martyr/cube.rb +134 -0
  17. data/lib/martyr/dimension_reference.rb +26 -0
  18. data/lib/martyr/errors.rb +20 -0
  19. data/lib/martyr/helpers/delegators.rb +17 -0
  20. data/lib/martyr/helpers/intervals.rb +222 -0
  21. data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
  22. data/lib/martyr/helpers/registrable.rb +15 -0
  23. data/lib/martyr/helpers/sorter.rb +79 -0
  24. data/lib/martyr/helpers/translations.rb +34 -0
  25. data/lib/martyr/level_concern/has_level_collection.rb +11 -0
  26. data/lib/martyr/level_concern/level.rb +45 -0
  27. data/lib/martyr/level_concern/level_collection.rb +60 -0
  28. data/lib/martyr/level_concern/level_comparator.rb +45 -0
  29. data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
  30. data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
  31. data/lib/martyr/runtime/data_set/element.rb +66 -0
  32. data/lib/martyr/runtime/data_set/element_common.rb +51 -0
  33. data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
  34. data/lib/martyr/runtime/data_set/fact.rb +83 -0
  35. data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
  36. data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
  37. data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
  38. data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
  39. data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
  40. data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
  41. data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
  42. data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
  43. data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
  44. data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
  45. data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
  46. data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
  47. data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
  48. data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
  49. data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
  50. data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
  51. data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
  52. data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
  53. data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
  54. data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
  55. data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
  56. data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
  57. data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
  58. data/lib/martyr/runtime/query/query_context.rb +246 -0
  59. data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
  60. data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
  61. data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
  62. data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
  63. data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
  64. data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
  65. data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
  66. data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
  67. data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
  68. data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
  69. data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
  70. data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
  71. data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
  72. data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
  73. data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
  74. data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
  75. data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
  76. data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
  77. data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
  78. data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
  79. data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
  80. data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
  81. data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
  82. data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
  83. data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
  84. data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
  85. data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
  86. data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
  87. data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
  88. data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
  89. data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
  90. data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
  91. data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
  92. data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
  93. data/lib/martyr/schema/metrics/base_metric.rb +77 -0
  94. data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
  95. data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
  96. data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
  97. data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
  98. data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
  99. data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
  100. data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
  101. data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
  102. data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
  103. data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
  104. data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
  105. data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
  106. data/lib/martyr/version.rb +3 -0
  107. data/lib/martyr/virtual_cube.rb +74 -0
  108. data/lib/martyr.rb +55 -0
  109. data/martyr.gemspec +41 -0
  110. 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