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