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,24 @@
1
+ module Martyr
2
+ module Schema
3
+ class TimeDimension
4
+
5
+ # @param name [Symbol, String]
6
+ # @param column [Symbol, String]
7
+ def initialize(name, column: name)
8
+ # TODO: convert the column into level definitions of week, month, and year
9
+ # super(name: name.to_s) do |dimension|
10
+ # dimension.add_level column, foreign_key: column
11
+ # end
12
+ end
13
+
14
+ def build_data_slice(*args)
15
+ Runtime::TimeDimensionDataSlice.new self, *args
16
+ end
17
+
18
+ def find_level(name)
19
+ # TODO: implement
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Martyr
2
+ module Schema
3
+ class BaseFactDefinition
4
+ attr_reader :cube, :scope, :dimension_associations, :joins_by_default
5
+ alias_method :dimensions, :dimension_associations
6
+
7
+ def supports_dimension_level?(dimension_name, level_name)
8
+ dimension = dimension_associations[dimension_name]
9
+ return false unless dimension
10
+
11
+ lowest_supported_level_i = dimension.lowest_level.to_i
12
+ considered_level_i = dimension_definitions.find_dimension(dimension_name).find_level(level_name).to_i
13
+ considered_level_i <= lowest_supported_level_i
14
+ end
15
+
16
+ def has_dimension_level?(dimension_name, level_name)
17
+ dimension = dimension_associations[dimension_name]
18
+ dimension and dimension.has_level?(level_name)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ module Martyr
2
+ module Schema
3
+ class FactDefinitionCollection < HashWithIndifferentAccess
4
+ include Martyr::Registrable
5
+
6
+ attr_reader :cube
7
+
8
+ def initialize(cube)
9
+ @cube = cube
10
+ end
11
+
12
+ def main_fact
13
+ fetch(:main, nil) || build_main_fact
14
+ end
15
+
16
+ def build_main_fact
17
+ register MainFactDefinition.new(cube)
18
+ end
19
+
20
+ def sub_fact(name, &block)
21
+ raise Schema::Error.new('`main` is a reserved query name') if name.to_s == 'main'
22
+ register SubFactDefinition.new(cube, name, &block)
23
+ end
24
+ alias_method :sub_query, :sub_fact
25
+
26
+ # @param selected_sub_facts [Array<String>] array of sub-fact keys to include in the returned collection.
27
+ # @return [Runtime::FactScopeCollection]
28
+ def build_fact_scopes(selected_sub_facts = [])
29
+ selected_sub_facts_hash = Hash[selected_sub_facts.map{|x| [x.to_s, true]}]
30
+ missing_sub_facts = selected_sub_facts_hash.keys - self.keys
31
+
32
+ raise Schema::Error.new("Could not find #{'sub query'.pluralize(missing_sub_facts.length)} " +
33
+ missing_sub_facts.join(', ')) if missing_sub_facts.present?
34
+
35
+ scope_collection = Runtime::FactScopeCollection.new
36
+ values.each do |scope_definition|
37
+ next unless scope_definition.joins_by_default or selected_sub_facts_hash[scope_definition.name]
38
+ scope_collection.register scope_definition.build
39
+ end
40
+ scope_collection
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ module Martyr
2
+ module Schema
3
+ class MainFactDefinition < BaseFactDefinition
4
+
5
+ delegate :dimension_definitions, to: :@cube
6
+
7
+ delegate :supports_metric?, to: :metric_definitions
8
+ delegate :supports_dimension?, to: :dimension_associations
9
+
10
+ # = DSL
11
+
12
+ delegate :has_dimension_level, :find_dimension_association, :find_level_association, to: :dimension_associations
13
+ delegate :find_metric, :has_count_distinct_metric, :has_min_metric, :has_max_metric, :has_sum_metric,
14
+ :has_custom_metric, :has_custom_rollup, to: :metric_definitions
15
+
16
+ # @param cube [Martyr::Cube]
17
+ def initialize(cube)
18
+ @cube = cube
19
+ @joins_by_default = true
20
+ end
21
+
22
+ def dimension_associations
23
+ @dimension_associations ||= DimensionAssociationCollection.new(dimension_definitions)
24
+ end
25
+
26
+ def metric_definitions
27
+ @metric_definitions ||= Schema::MetricDefinitionCollection.new(@cube)
28
+ end
29
+ alias_method :metrics, :metric_definitions
30
+
31
+ def main_query(&scope)
32
+ @scope = scope
33
+ end
34
+
35
+ def name
36
+ 'main'
37
+ end
38
+
39
+ # @return [Runtime::MainFactScope]
40
+ def build
41
+ Runtime::MainFactScope.new(self)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,44 @@
1
+ module Martyr
2
+ module Schema
3
+ class SubFactDefinition < BaseFactDefinition
4
+ attr_reader :name, :join_clause, :join_on
5
+
6
+ delegate :main_fact, :dimension_definitions, to: :cube
7
+
8
+ delegate :find_level_association, to: :dimension_associations
9
+
10
+ def initialize(cube, name, &block)
11
+ @cube = cube
12
+ @name = name.to_s
13
+ @dimension_associations = DimensionAssociationCollection.new(dimension_definitions)
14
+ @joins_by_default = false
15
+
16
+ scope = instance_eval(&block)
17
+ @scope = -> { scope }
18
+ end
19
+
20
+ def supports_metric?(*)
21
+ false
22
+ end
23
+
24
+ def has_dimension_level(dimension_name, level_name, **args)
25
+ raise Schema::Error.new("Dimension level `#{dimension_name}.#{level_name}` does not exist in main query") unless
26
+ main_fact.has_dimension_level?(dimension_name, level_name)
27
+
28
+ dimension_associations.has_dimension_level(dimension_name, level_name, **args)
29
+ end
30
+
31
+ def joins_with(join_clause, on:, by_default: false)
32
+ @join_clause = join_clause
33
+ @join_on = on
34
+ @joins_by_default = by_default
35
+ end
36
+
37
+ # @return [Runtime::SubFactScope]
38
+ def build
39
+ Runtime::SubFactScope.new(self)
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ module Martyr
2
+ module Schema
3
+ class BaseMetric
4
+ include ActiveModel::Model
5
+ extend Martyr::Translations
6
+
7
+ attr_accessor :cube_name, :name, :rollup_function, :sort, :fact_grain
8
+
9
+ def id
10
+ "#{cube_name}.#{name}"
11
+ end
12
+
13
+ alias_method :slice_id, :id
14
+
15
+ # Used for reflection
16
+ def metric?
17
+ true
18
+ end
19
+
20
+ def human_name
21
+ name.to_s.titleize
22
+ end
23
+
24
+ def build_data_slice(*)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def build_memory_slice(*)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ # @param fact_scopes [Runtime::FactScopeCollection]
33
+ def add_to_select(fact_scopes)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ def extract(fact)
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # @param element [Runtime::Element]
42
+ def rollup(element)
43
+ case rollup_function.to_s
44
+ when 'count'
45
+ element.facts.length
46
+ when 'sum'
47
+ element.facts.map{|x| x.fetch(id) || 0}.reduce(:+)
48
+ when 'min'
49
+ element.facts.map{|x| x.fetch(id)}.compact.min
50
+ when 'max'
51
+ element.facts.map{|x| x.fetch(id)}.compact.max
52
+ when 'none'
53
+
54
+ # Two scenarios:
55
+ # (1) The user does not specify a fact_grain on a rollup: :none custom metric
56
+ # The custom metric operates on the fact grain and is not rolled up when the element contains
57
+ # multiple facts. We return it only if the facts length is 1.
58
+ #
59
+ # (2) fact_grain exists
60
+ # The custom metric operates on the levels provided by the user. There are three scenarios depending
61
+ # on the element grain:
62
+ # - Same level as the fact grain
63
+ # There is only one fact, and facts.first returns it
64
+ #
65
+ # - More detailed level than the fact grain
66
+ # We should be able to pick any random fact.
67
+ #
68
+ # - Less detailed than the fact grain
69
+ # If there is only one fact, we return its value, otherwise we avoid rolling up and return nil
70
+ #
71
+ element.facts.first.fetch(id) if element.facts.length == 1 or
72
+ (fact_grain.present? and (fact_grain - element.grain_level_ids).empty?)
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,38 @@
1
+ module Martyr
2
+ module Schema
3
+ class BuiltInMetric < BaseMetric
4
+
5
+ attr_accessor :statement, :fact_alias, :typecast, :sub_queries
6
+
7
+ def build_data_slice(*args)
8
+ Runtime::MetricDataSlice.new(self, *args)
9
+ end
10
+
11
+ def build_memory_slice(*args)
12
+ Runtime::MetricMemorySlice.new(self, *args)
13
+ end
14
+
15
+ # @param fact_scopes [Runtime::FactScopeCollection]
16
+ def add_to_select(fact_scopes)
17
+ fact_scopes.add_select_operator_for_metric(name) do |operator|
18
+ operator.add_select(statement, as: fact_alias, data_rollup_sql: data_rollup_sql)
19
+ end
20
+ end
21
+
22
+ def extract(fact)
23
+ fact.raw.fetch(fact_alias.to_s).try(:send, typecast || :to_i)
24
+ end
25
+
26
+ private
27
+
28
+ def data_rollup_sql
29
+ if rollup_function.to_s == 'none'
30
+ fact_alias
31
+ else
32
+ "#{rollup_function.to_s.upcase}(#{fact_alias}) AS #{fact_alias}"
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,172 @@
1
+ module Martyr
2
+ module Schema
3
+ class CountDistinctMetric < BuiltInMetric
4
+
5
+ # Allows predetermining and caching the rollup strategy that will be used to rollup metrics of elements. This has
6
+ # performance benefits when rolling up a large array of elements that were the result of the same query.
7
+ #
8
+ # Usage:
9
+ #
10
+ # CountDistinctMetric.enable_rollup_strategy_caching(metrics) do
11
+ # # Code use to rollup (compute) metrics of many elements
12
+ # end
13
+ #
14
+ # @param metrics [Array<BaseMetric>] any metric that is planned to be rolled up. The subset of count distinct
15
+ # metrics will be used.
16
+ # @param element_grain [Array<String>] array of level IDs
17
+ # @param fact_grain [Array<String>] array of level IDs
18
+ def self.enable_rollup_strategy_caching(metrics)
19
+ relevant_metrics = metrics.select {|metric| metric.is_a?(self)}
20
+
21
+ relevant_metrics.each do |count_distinct_metric|
22
+ count_distinct_metric.send(:enable_rollup_strategy_caching)
23
+ end
24
+
25
+ yield
26
+
27
+ ensure
28
+ relevant_metrics.each do |count_distinct_metric|
29
+ count_distinct_metric.send(:cleanup_rollup_strategy)
30
+ end
31
+ end
32
+
33
+ # @attribute level [Schema::LevelAssociation]
34
+ # @attribute null_unless [String] @see MetricDefinitionCollection#has_count_distinct_metric
35
+ attr_accessor :level, :null_unless
36
+ delegate :id, to: :level, prefix: true
37
+
38
+ # @override
39
+ def add_to_select(fact_scopes)
40
+ # Adding an inner SQL statement with the metric's fact_alias is essential for metric slices
41
+ # Note that it is not used for the rollup in the wrapper
42
+ fact_scopes.add_select_operator_for_metric(name) do |operator|
43
+ operator.add_select(inner_sql_statement, as: fact_alias)
44
+ end
45
+
46
+ fact_scopes.add_select_operator_for_metric(name) do |operator|
47
+ operator.add_select(select_statement, as: inner_sql_helper_field, data_rollup_sql: data_rollup_sql)
48
+ end
49
+ end
50
+
51
+ # @override
52
+ def data_rollup_sql
53
+ "COUNT(DISTINCT #{inner_sql_helper_field}) AS #{fact_alias}"
54
+ end
55
+
56
+ # @override
57
+ def rollup(element)
58
+ rollup_strategy(element).run(element)
59
+ end
60
+
61
+ private
62
+
63
+ def inner_sql_statement
64
+ return '1' unless null_unless.present?
65
+ "CASE WHEN #{null_unless} THEN 1 ELSE 0 END"
66
+ end
67
+
68
+ def select_statement
69
+ return level.fact_key unless null_unless.present?
70
+ "CASE WHEN #{null_unless} THEN #{level.fact_key} ELSE NULL END"
71
+ end
72
+
73
+ def inner_sql_helper_field
74
+ return level.fact_alias unless null_unless.present?
75
+ [fact_alias, 'distinct_helper'].join('_')
76
+ end
77
+
78
+ # @return [RollupStrategy] cached version if caching is enabled, otherwise a newly minted object
79
+ def rollup_strategy(element)
80
+ return @rollup_strategy if @rollup_strategy_caching_enabled and @rollup_strategy
81
+ rollup_strategy = RollupStrategy.new(self, element.grain_level_ids, element.facts.first.grain_level_ids)
82
+ @rollup_strategy = rollup_strategy if @rollup_strategy_caching_enabled
83
+ rollup_strategy
84
+ end
85
+
86
+ def enable_rollup_strategy_caching
87
+ @rollup_strategy_caching_enabled = true
88
+ end
89
+
90
+ def cleanup_rollup_strategy
91
+ @rollup_strategy = nil
92
+ @rollup_strategy_caching_enabled = false
93
+ end
94
+
95
+ class RollupStrategy
96
+ attr_reader :metric, :strategy_method
97
+
98
+ # This is the trickiest part.
99
+ # Consider count distinct on `customers.last_name`.
100
+ #
101
+ # The following scenarios exist:
102
+ # 1. The level was not part of the fact to begin with.
103
+ # There are some cases in which double counting cannot be avoided, hence rollup is not always possible.
104
+ # For example, let's say we had a query on `media_types.name` and `genres.name`.
105
+ # For every given combination, the fact holds a distinct count of the number of customers.
106
+ # However, the same customer may have bought different combinations of media types and genres.
107
+ # Therefore, if we remove `genres.name` from the element grain we have no way to correctly rollup the number
108
+ # of distinct customers per media type.
109
+ #
110
+ # There are two exceptions in which rollup is allowed:
111
+ # (a) No levels were dropped between fact grain and element grain.
112
+ # (b) Some levels were dropped, but all of them represent levels above `customers.last_name`.
113
+ #
114
+ # If we had a query on `media_types.name` and `customers.country`, if we drop `customers.country` we can
115
+ # perform a SUM of the facts.
116
+ #
117
+ # 2. The level was part of the fact and is also part of the element grain
118
+ # Can be retrieved from the first fact. All facts should have the same value.
119
+ #
120
+ # 3. The level was part of the fact but is not part of the element grain
121
+ # A DISTINCT count can be done in memory
122
+ #
123
+ def initialize(metric, element_grain, fact_grain)
124
+ @metric = metric
125
+
126
+ # Scenario 1
127
+ unless fact_grain.include?(metric.level_id)
128
+ acceptable_removed_level_ids = metric.level.level_and_above.map(&:id)
129
+ if (element_grain - fact_grain).all? { |level_id| acceptable_removed_level_ids.include?(level_id) }
130
+ @strategy_method = :sum
131
+ else
132
+ @strategy_method = :error
133
+ end
134
+ return
135
+ end
136
+
137
+ # Scenario 2
138
+ if element_grain.include?(metric.level_id)
139
+ @strategy_method = :first
140
+ return
141
+ end
142
+
143
+ # Scenario 3
144
+ @strategy_method = :uniq
145
+ end
146
+
147
+ def run(element)
148
+ send(strategy_method, element)
149
+ end
150
+
151
+ private
152
+
153
+ def sum(element)
154
+ element.facts.map{|fact| fact.fetch(metric.id)}.reduce(:+)
155
+ end
156
+
157
+ def error(_element)
158
+ "Invalid count distinct rollup. Add `#{metric.level_id}` to the grain"
159
+ end
160
+
161
+ def first(element)
162
+ element.facts.first.fetch(metric.id)
163
+ end
164
+
165
+ def uniq(element)
166
+ # We check for fetch(metric.id) > 0 for the sake of the use of +null_unless+ option
167
+ element.facts.select { |x| x.fetch(metric.id) > 0 }.map{ |x| x.fact_key_for(metric.level_id) }.uniq.length
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,26 @@
1
+ module Martyr
2
+ module Schema
3
+ class CustomMetric < BaseMetric
4
+
5
+ attr_accessor :block, :depends_on
6
+
7
+ def build_data_slice(*)
8
+ raise Runtime::Error.new("Custom metrics cannot be sliced: attempted on metric `#{name}`")
9
+ end
10
+
11
+ def build_memory_slice(*args)
12
+ Runtime::MetricMemorySlice.new(self, *args)
13
+ end
14
+
15
+ # @param fact_scopes [Runtime::FactScopeCollection]
16
+ def add_to_select(fact_scopes)
17
+ # no-op
18
+ end
19
+
20
+ def extract(fact)
21
+ fact.instance_exec(&block)
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module Martyr
2
+ module Schema
3
+ class CustomRollup < BaseMetric
4
+ attr_accessor :cube_name, :name, :block, :depends_on
5
+
6
+ def build_data_slice(*)
7
+ raise Runtime::Error.new("Rollups cannot be sliced: attempted on rollup `#{name}`")
8
+ end
9
+
10
+ def build_memory_slice(*)
11
+ raise Runtime::Error.new("Rollups cannot be sliced: attempted on rollup `#{name}`")
12
+ end
13
+
14
+ # @param fact_scopes [Runtime::FactScopeCollection]
15
+ def add_to_select(fact_scopes)
16
+ # no-op
17
+ end
18
+
19
+ def extract(fact)
20
+ raise Runtime::Error.new("Rollups cannot be extracted: attempted on rollup `#{name}`")
21
+ end
22
+
23
+ # @override
24
+ # @param element [Runtime::Element]
25
+ def rollup(element)
26
+ element.instance_exec(&block)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,150 @@
1
+ module Martyr
2
+ module Schema
3
+ class DependencyInferrer
4
+
5
+ attr_reader :level_keys_hash, :metric_keys_hash
6
+
7
+ def initialize
8
+ @cubes = []
9
+ @level_keys_hash = {}
10
+ @metric_keys_hash = {}
11
+ end
12
+
13
+ def add_cube_levels(cube)
14
+ cube.supported_level_definitions.each do |level|
15
+ add_level(level)
16
+ end
17
+ self
18
+ end
19
+
20
+ # We rely on adding metric one-by-one to avoid infinite recursion
21
+ # @param metric [BaseMetric]
22
+ def add_metric(metric)
23
+ @metric_keys_hash[metric.name.to_s] = metric.id
24
+ @metric_keys_hash[metric.id.to_s] = metric.id
25
+ end
26
+
27
+ # @return [#depends_on, #fact_grain] an object responding to these methods
28
+ # The idea is that the block won't be evaluated if the user intervened with one of the options
29
+ def infer_from_block(depends_on: nil, fact_grain: nil, &block)
30
+ if (depends_on.nil? or depends_on == []) and (fact_grain.nil? or fact_grain == [])
31
+ evaluator = BlockEvaluator.new(self)
32
+ evaluator.instance_exec(&block)
33
+ evaluator
34
+ else
35
+ UserValues.new(depends_on, fact_grain)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @param level [BaseLevelDefinition]
42
+ def add_level(level)
43
+ @level_keys_hash[level.id] = level.id
44
+ level.helper_methods.each do |method_name|
45
+ @level_keys_hash[method_name] = level.id
46
+ end
47
+ end
48
+
49
+ class UserValues
50
+ attr_reader :depends_on, :fact_grain
51
+
52
+ def initialize(depends_on, fact_grain)
53
+ @depends_on = depends_on == false ? [] : Array.wrap(depends_on)
54
+ @fact_grain = fact_grain == false ? [] : Array.wrap(fact_grain)
55
+ end
56
+ end
57
+
58
+ class BlockEvaluator
59
+ include Comparable
60
+
61
+ def initialize(inferrer)
62
+ @inferrer = inferrer
63
+ @depends_on_hash = {}
64
+ @fact_grain_hash = {}
65
+ end
66
+
67
+ def depends_on
68
+ @depends_on_hash.keys
69
+ end
70
+
71
+ def fact_grain
72
+ @fact_grain_hash.keys
73
+ end
74
+
75
+ def locate(*args)
76
+ main_arg = args.first
77
+ if main_arg.is_a?(String)
78
+ infer_either(main_arg)
79
+ elsif main_arg.is_a?(Hash)
80
+ main_arg.keys.each do |arg|
81
+ infer_either(arg)
82
+ end
83
+ end
84
+ self
85
+ end
86
+
87
+ def fetch(id)
88
+ infer_either(id)
89
+ self
90
+ end
91
+
92
+ def [](key)
93
+ infer_depends_on(key)
94
+ self
95
+ end
96
+
97
+ def key_for(level_id)
98
+ infer_fact_grain(level_id)
99
+ self
100
+ end
101
+
102
+ def record_for(level_id)
103
+ infer_fact_grain(level_id)
104
+ self
105
+ end
106
+
107
+ def fact_key_for(level_id)
108
+ infer_fact_grain(level_id)
109
+ self
110
+ end
111
+
112
+ def <=>(_other)
113
+ 0
114
+ end
115
+
116
+ def coerce(other)
117
+ [other, other]
118
+ end
119
+
120
+ private
121
+
122
+ # @param candidate [String] a suspicious level ID that should be check against the white list
123
+ # @return [nil, true] true if added
124
+ def infer_fact_grain(candidate)
125
+ level_id = @inferrer.level_keys_hash[candidate.to_s]
126
+ return unless level_id.present?
127
+ @fact_grain_hash[level_id] = true
128
+ end
129
+
130
+ # @param candidate [String] a suspicious metric ID that should be check against the white list
131
+ # @return [nil, true] true if added
132
+ def infer_depends_on(candidate)
133
+ metric_id = @inferrer.metric_keys_hash[candidate.to_s]
134
+ return unless metric_id.present?
135
+ @depends_on_hash[metric_id] = true
136
+ end
137
+
138
+ def infer_either(candidate)
139
+ infer_fact_grain(candidate) || infer_depends_on(candidate)
140
+ end
141
+
142
+ def method_missing(name, *args, &block)
143
+ infer_either(name)
144
+ self
145
+ end
146
+ end
147
+
148
+ end
149
+ end
150
+ end