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