martyr 0.1.74.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.tags +868 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/Rakefile +1 -0
- data/TODO.txt +54 -0
- data/bin/console +62 -0
- data/bin/setup +7 -0
- data/lib/martyr/base_cube.rb +73 -0
- data/lib/martyr/cube.rb +134 -0
- data/lib/martyr/dimension_reference.rb +26 -0
- data/lib/martyr/errors.rb +20 -0
- data/lib/martyr/helpers/delegators.rb +17 -0
- data/lib/martyr/helpers/intervals.rb +222 -0
- data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
- data/lib/martyr/helpers/registrable.rb +15 -0
- data/lib/martyr/helpers/sorter.rb +79 -0
- data/lib/martyr/helpers/translations.rb +34 -0
- data/lib/martyr/level_concern/has_level_collection.rb +11 -0
- data/lib/martyr/level_concern/level.rb +45 -0
- data/lib/martyr/level_concern/level_collection.rb +60 -0
- data/lib/martyr/level_concern/level_comparator.rb +45 -0
- data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
- data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
- data/lib/martyr/runtime/data_set/element.rb +66 -0
- data/lib/martyr/runtime/data_set/element_common.rb +51 -0
- data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
- data/lib/martyr/runtime/data_set/fact.rb +83 -0
- data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
- data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
- data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
- data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
- data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
- data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
- data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
- data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
- data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
- data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
- data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
- data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
- data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
- data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
- data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
- data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
- data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
- data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
- data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
- data/lib/martyr/runtime/query/query_context.rb +246 -0
- data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
- data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
- data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
- data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
- data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
- data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
- data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
- data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
- data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
- data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
- data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
- data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
- data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
- data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
- data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
- data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
- data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
- data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
- data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
- data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
- data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
- data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
- data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
- data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
- data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
- data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
- data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
- data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
- data/lib/martyr/schema/metrics/base_metric.rb +77 -0
- data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
- data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
- data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
- data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
- data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
- data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
- data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
- data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
- data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
- data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
- data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
- data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
- data/lib/martyr/version.rb +3 -0
- data/lib/martyr/virtual_cube.rb +74 -0
- data/lib/martyr.rb +55 -0
- data/martyr.gemspec +41 -0
- metadata +296 -0
@@ -0,0 +1,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
|