martyr 0.1.74.pre

Sign up to get free protection for your applications and to get access to all the features.
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