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,120 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
# The conversion from and to interval sets is used for merging between data slices and memory slices.
|
|
3
|
+
class MetricSliceDefinition < BaseSliceDefinition
|
|
4
|
+
|
|
5
|
+
OPERATORS = [:gt, :lt, :gte, :lte, :eq, :not]
|
|
6
|
+
attr_accessor :metric, *OPERATORS
|
|
7
|
+
|
|
8
|
+
def self.from_interval_set(interval_set)
|
|
9
|
+
base = {metric: metric}
|
|
10
|
+
new base.merge!(interval_set_to_hash(interval_set))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_hash
|
|
14
|
+
{metric.id => OPERATORS.inject({}) { |h, op| send(op) ? h.merge!(op => send(op)) : h }}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def merge(other)
|
|
18
|
+
self.class.from_interval_set(to_interval_set.intersect(other.to_interval_set))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Array<Array<Hash>>]
|
|
22
|
+
# Every element in the top level array is an array with "OR" statements.
|
|
23
|
+
# The top level statements are to be combined with an "AND".
|
|
24
|
+
#
|
|
25
|
+
# Example:
|
|
26
|
+
# [ [1,2,3], [4], [5] ]
|
|
27
|
+
# # => "(1 OR 2 OR 3) AND (4) AND (5)"
|
|
28
|
+
#
|
|
29
|
+
def combined_statements
|
|
30
|
+
statements = []
|
|
31
|
+
statements << [{data_operator: gt_operator, memory_operator: gt_operator, value: gt_value}] if gt_operator
|
|
32
|
+
statements << [{data_operator: lt_operator, memory_operator: lt_operator, value: lt_value}] if lt_operator
|
|
33
|
+
statements << Array.wrap(eq).map {|value| {data_operator: '=', memory_operator: '==', value: value} } if eq
|
|
34
|
+
|
|
35
|
+
Array.wrap(self.not).each do |value|
|
|
36
|
+
statements << [{data_operator: '!=', memory_operator: '!=', value: value}]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
statements
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
protected
|
|
43
|
+
|
|
44
|
+
def compile_operators
|
|
45
|
+
hash = interval_set_to_hash(to_interval_set)
|
|
46
|
+
OPERATORS.each do |operator|
|
|
47
|
+
instance_variable_set "@#{operator}", hash[operator]
|
|
48
|
+
end
|
|
49
|
+
set_null if hash.compact.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.interval_set_to_hash(interval_set)
|
|
53
|
+
return {} if interval_set.null?
|
|
54
|
+
not_arr = interval_set.extract_and_fill_holes.presence
|
|
55
|
+
eq_arr = interval_set.extract_and_remove_points.presence
|
|
56
|
+
raise Internal::Error.new('Unexpected interval set format') unless interval_set.null? or interval_set.continuous?
|
|
57
|
+
|
|
58
|
+
upper_point = interval_set.upper_bound
|
|
59
|
+
lte = upper_point.x if upper_point.try(:closed?)
|
|
60
|
+
lt = upper_point.x if upper_point.try(:open?)
|
|
61
|
+
|
|
62
|
+
lower_point = interval_set.lower_bound
|
|
63
|
+
gte = lower_point.x if lower_point.try(:closed?)
|
|
64
|
+
gt = lower_point.x if lower_point.try(:open?)
|
|
65
|
+
|
|
66
|
+
{ not: not_arr, eq: eq_arr, lte: lte, lt: lt, gte: gte, gt: gt }
|
|
67
|
+
end
|
|
68
|
+
delegate :interval_set_to_hash, to: 'self.class'
|
|
69
|
+
|
|
70
|
+
# Calculate each time to avoid messing up internal state
|
|
71
|
+
def to_interval_set
|
|
72
|
+
interval_set = IntervalSet.new.add
|
|
73
|
+
merge_eq_interval_set(interval_set)
|
|
74
|
+
merge_not_interval_set(interval_set)
|
|
75
|
+
interval_set.intersect IntervalSet.new(to: lt) if lt
|
|
76
|
+
interval_set.intersect IntervalSet.new(to: [lte]) if lte
|
|
77
|
+
interval_set.intersect IntervalSet.new(from: gt) if gt
|
|
78
|
+
interval_set.intersect IntervalSet.new(from: [gte]) if gte
|
|
79
|
+
interval_set
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def merge_eq_interval_set(interval_set)
|
|
83
|
+
return unless eq.present?
|
|
84
|
+
set = IntervalSet.new
|
|
85
|
+
Array.wrap(eq).each {|x| set.add(from: [x], to: [x]) }
|
|
86
|
+
interval_set.intersect(set)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def merge_not_interval_set(interval_set)
|
|
90
|
+
return unless self.not.present?
|
|
91
|
+
Array.wrap(self.not).
|
|
92
|
+
map {|x| IntervalSet.new(to: x).add(from: x) }.
|
|
93
|
+
inject(interval_set) {|set, hole| set.intersect(hole)}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def gt_value
|
|
97
|
+
gte || gt
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def lt_value
|
|
101
|
+
lte || lt
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def gt_operator
|
|
105
|
+
if gte.present?
|
|
106
|
+
'>='
|
|
107
|
+
elsif gt.present?
|
|
108
|
+
'>'
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def lt_operator
|
|
113
|
+
if lte.present?
|
|
114
|
+
'<='
|
|
115
|
+
elsif lt.present?
|
|
116
|
+
'<'
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
class PlainDimensionLevelSliceDefinition < BaseSliceDefinition
|
|
3
|
+
|
|
4
|
+
# @attribute level [BaseLevelDefinition]
|
|
5
|
+
attr_accessor :level, :with
|
|
6
|
+
|
|
7
|
+
def to_hash
|
|
8
|
+
{level.id => {with: with}}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def merge(other)
|
|
12
|
+
raise Internal::Error.new('Cannot merge two different levels') unless level.id == other.level.id
|
|
13
|
+
merged_with = with.present? && other.with.present? ? with & other.with : with + other.with
|
|
14
|
+
self.class.new(level: level, with: merged_with)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def compile_operators
|
|
20
|
+
@with = Array.wrap(@with).uniq
|
|
21
|
+
set_null unless @with.present?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
module FactFillerStrategies
|
|
4
|
+
|
|
5
|
+
# Instead of every fact object figuring out itself how to extract fact values from the raw data,
|
|
6
|
+
# we calculate a memoized set of strategies in advance.
|
|
7
|
+
|
|
8
|
+
def fact_levels_filler_hash
|
|
9
|
+
return @fact_levels_filler_hash if @fact_levels_filler_hash
|
|
10
|
+
hash = {}
|
|
11
|
+
supported_level_definitions.each do |level_definition|
|
|
12
|
+
level_id = level_definition.id
|
|
13
|
+
if has_association_with_level?(level_id)
|
|
14
|
+
level_association = association_from_id(level_id)
|
|
15
|
+
if level_association.degenerate?
|
|
16
|
+
filler = DegenerateLevelAssociationFillerStrategy.new(level_association)
|
|
17
|
+
else
|
|
18
|
+
filler = QueryLevelAssociationFillerStrategy.new(level_association)
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
filler = UnassociatedLevelFillerStrategy.new(level_definition)
|
|
22
|
+
end
|
|
23
|
+
hash[level_id] = filler
|
|
24
|
+
end
|
|
25
|
+
@fact_levels_filler_hash = hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class DegenerateLevelAssociationFillerStrategy
|
|
29
|
+
def initialize(level_association)
|
|
30
|
+
@fact_alias = level_association.fact_alias
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def value(fact)
|
|
34
|
+
fact.raw.fetch(@fact_alias)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class QueryLevelAssociationFillerStrategy
|
|
39
|
+
def initialize(level_association)
|
|
40
|
+
@level_association = level_association
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def value(fact)
|
|
44
|
+
fact_key_value = fact.raw.fetch(@level_association.fact_alias)
|
|
45
|
+
FutureFactValue.new(fact, @level_association.level_definition, key_supported: true, fact_key_value: fact_key_value)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class UnassociatedLevelFillerStrategy
|
|
50
|
+
def initialize(level_definition)
|
|
51
|
+
@level_definition = level_definition
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def value(fact)
|
|
55
|
+
FutureFactValue.new(fact, @level_definition, key_supported: false)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class QueryMetrics < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::Registrable
|
|
5
|
+
include Martyr::Translations
|
|
6
|
+
|
|
7
|
+
attr_reader :sub_cube
|
|
8
|
+
|
|
9
|
+
delegate :cube, to: :sub_cube
|
|
10
|
+
|
|
11
|
+
include Martyr::Delegators
|
|
12
|
+
each_child_delegator :add_to_select, to: :values
|
|
13
|
+
|
|
14
|
+
alias_method :find_metric, :find_or_error
|
|
15
|
+
|
|
16
|
+
def initialize(sub_cube)
|
|
17
|
+
@sub_cube = sub_cube
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def inspect
|
|
21
|
+
"#<#{self.class} #{to_a}>"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_a
|
|
25
|
+
keys
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_metric(metric_id)
|
|
29
|
+
with_standard_id(metric_id) do |cube_name, metric_name|
|
|
30
|
+
register cube.find_metric(metric_name) if cube_name == sub_cube.cube_name
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def built_in_metrics
|
|
35
|
+
values.select{|x| x.is_a?(Schema::BuiltInMetric)}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def custom_metrics
|
|
39
|
+
values.select{|x| x.is_a?(Schema::CustomMetric)}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def custom_rollups
|
|
43
|
+
values.select{|x| x.is_a?(Schema::CustomRollup)}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def metric_ids
|
|
47
|
+
values.map(&:id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def metric_objects
|
|
51
|
+
values
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class SubCube
|
|
4
|
+
|
|
5
|
+
include Martyr::LevelComparator
|
|
6
|
+
include Martyr::Translations
|
|
7
|
+
include Martyr::Runtime::FactFillerStrategies
|
|
8
|
+
|
|
9
|
+
attr_reader :query_context, :cube, :fact_scopes, :metrics, :grain
|
|
10
|
+
delegate :combined_sql, :test, to: :fact_scopes
|
|
11
|
+
|
|
12
|
+
# TODO: supported_* methods are delegated to the grain, but there are equivalent methods in the cube that mean
|
|
13
|
+
# something else and sometimes needed. #select_supported_level_ids, for instance, is relying on those
|
|
14
|
+
# methods in the cube, not the grain.
|
|
15
|
+
# This is confusing.
|
|
16
|
+
delegate :cube_name, :dimension_associations, :select_supported_level_ids, :standardizer, to: :cube
|
|
17
|
+
delegate :supported_level_associations, :supported_level_definitions, :has_association_with_level?, to: :grain
|
|
18
|
+
|
|
19
|
+
delegate :find_metric, :metric_ids, :metric_objects, :built_in_metrics, :custom_metrics, to: :metrics
|
|
20
|
+
delegate :facts, to: :fact_indexer
|
|
21
|
+
delegate :definition_from_id, to: :query_context
|
|
22
|
+
|
|
23
|
+
alias_method :dimension_bus, :query_context
|
|
24
|
+
|
|
25
|
+
# @param cube [Martyr::Cube]
|
|
26
|
+
def initialize(query_context, cube)
|
|
27
|
+
@query_context = query_context
|
|
28
|
+
@cube = cube
|
|
29
|
+
@metrics = QueryMetrics.new(self)
|
|
30
|
+
@grain = SubCubeGrain.new(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inspect
|
|
34
|
+
to_hash.inspect
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_hash
|
|
38
|
+
{cube_name => {metrics: metrics.to_a, grain: grain.to_a}}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dimension_definitions
|
|
42
|
+
cube.supported_dimension_definitions
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @return [DimensionReference, LevelAssociation]
|
|
46
|
+
def association_from_id(id)
|
|
47
|
+
with_standard_id(id) do |x, y|
|
|
48
|
+
return dimension_associations[x].try(:levels).try(:[], y) if x and y
|
|
49
|
+
dimension_associations[x]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def common_denominator_level_association(level_id, prefer_query: false)
|
|
54
|
+
@_common_denominator_level_association ||= {}
|
|
55
|
+
return @_common_denominator_level_association[level_id] if @_common_denominator_level_association[level_id]
|
|
56
|
+
|
|
57
|
+
level = definition_from_id(level_id)
|
|
58
|
+
dimension_association = dimension_associations.find_dimension_association(level.dimension_name)
|
|
59
|
+
level = find_common_denominator_level(level, dimension_association.level_objects, prefer_query: prefer_query)
|
|
60
|
+
return nil unless level
|
|
61
|
+
|
|
62
|
+
@_common_denominator_level_association[level_id] = dimension_association.levels[level.name]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# = Definitions
|
|
66
|
+
|
|
67
|
+
def set_metrics(metrics_arr)
|
|
68
|
+
return unless metrics_arr.present?
|
|
69
|
+
metrics_arr.each do |metric_id|
|
|
70
|
+
@metrics.add_metric(metric_id)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def set_grain(grain_arr)
|
|
75
|
+
select_supported_level_ids(grain_arr).each do |level_id|
|
|
76
|
+
@grain.add_granularity(level_id)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def set_sub_facts(sub_facts_arr)
|
|
81
|
+
@fact_scopes = cube.build_fact_scopes(sub_facts_arr)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def lowest_level_ids_in_grain
|
|
85
|
+
grain.level_ids
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param data_slice [DataSlice] that is scoped to the cube
|
|
89
|
+
def decorate_all_scopes(data_slice)
|
|
90
|
+
grain.add_to_select(fact_scopes)
|
|
91
|
+
metrics.add_to_select(fact_scopes)
|
|
92
|
+
data_slice.add_to_where(fact_scopes, dimension_bus)
|
|
93
|
+
grain.add_to_group_by(fact_scopes)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# = Running
|
|
97
|
+
|
|
98
|
+
# @param memory_slice [MemorySlice]
|
|
99
|
+
# @option levels [Array<String, Martyr::Level>] array of level IDs or any type of level to group facts by.
|
|
100
|
+
# Default is all levels in the query context.
|
|
101
|
+
# @option metrics [Array<String, BaseMetric>] array of metric IDs or metric objects to roll up in the elements.
|
|
102
|
+
def elements(memory_slice, levels: nil, metrics: nil)
|
|
103
|
+
element_locator_for(memory_slice, metrics: metrics).all(sanitize_levels(levels: levels).map(&:id))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def element_locator_for(memory_slice, metrics: nil)
|
|
107
|
+
ElementLocator.new memory_slice: memory_slice, metrics: sanitize_metrics(metrics: metrics),
|
|
108
|
+
fact_indexer: fact_indexer, helper_module: query_context.element_helper_module,
|
|
109
|
+
restrict_level_ids: grain.supported_level_definition_ids, standardizer: standardizer
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fact_indexer
|
|
113
|
+
@fact_indexer ||= FactIndexer.new(self, grain.null? ? [] : fact_scopes.run.map { |hash| Fact.new(self, hash) })
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# @option metrics [Array<String, BaseMetric>] array of metric IDs or metric objects
|
|
117
|
+
def sanitize_metrics(metrics: nil)
|
|
118
|
+
metric_ids = Array.wrap(metrics).map { |x| to_id(x) }.presence || self.metrics.metric_ids
|
|
119
|
+
metric_ids = metric_ids & self.metrics.metric_ids
|
|
120
|
+
metric_ids.map {|id| query_context.metric(id) }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @option levels [Array<String, Martyr::Level>] array of level IDs or any type of level to group facts by.
|
|
124
|
+
# Default is all levels in grain
|
|
125
|
+
# @return [Array<String, BaseLevelScope>]
|
|
126
|
+
def sanitize_levels(levels: nil)
|
|
127
|
+
level_ids = levels.nil? ? query_context.level_ids_in_grain : Array.wrap(levels).map { |x| to_id(x) }
|
|
128
|
+
level_ids = select_supported_level_ids(level_ids)
|
|
129
|
+
query_context.levels_and_above_for(level_ids)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class SubCubeGrain
|
|
4
|
+
include Martyr::LevelComparator
|
|
5
|
+
|
|
6
|
+
attr_reader :sub_cube, :grain
|
|
7
|
+
delegate :cube, to: :sub_cube
|
|
8
|
+
|
|
9
|
+
def initialize(sub_cube)
|
|
10
|
+
@sub_cube = sub_cube
|
|
11
|
+
@grain = cube.default_fact_grain_level_associations.index_by{|x| x.dimension_name}
|
|
12
|
+
@null = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect
|
|
16
|
+
"#<#{self.class} #{to_a}>"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_a
|
|
20
|
+
supported_level_associations.map(&:id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def level_ids
|
|
24
|
+
grain.values.map(&:id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def null?
|
|
28
|
+
@null
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Maintains for every dimension the lowest supported level
|
|
32
|
+
def add_granularity(level_id)
|
|
33
|
+
level_to_add = sub_cube.common_denominator_level_association(level_id)
|
|
34
|
+
@null = true and return unless level_to_add
|
|
35
|
+
dimension = level_to_add.dimension_name
|
|
36
|
+
@grain[dimension] = more_detailed_level(@grain[dimension], level_to_add)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# TODO: remove
|
|
40
|
+
# def set_all_if_empty
|
|
41
|
+
# return if @grain.present?
|
|
42
|
+
# sub_cube.dimension_associations.each do |dimension_name, dimension_object|
|
|
43
|
+
# @grain[dimension_name.to_s] = dimension_object.lowest_level
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
|
|
47
|
+
# TODO: delete
|
|
48
|
+
# def nullify_scope_if_null(fact_scopes)
|
|
49
|
+
# fact_scopes.set_null_scope if null?
|
|
50
|
+
# end
|
|
51
|
+
|
|
52
|
+
# Adds all supported levels including and above the sliced level
|
|
53
|
+
# @param fact_scopes [Runtime::FactScopeCollection]
|
|
54
|
+
def add_to_select(fact_scopes)
|
|
55
|
+
supported_level_associations.each do |level_object|
|
|
56
|
+
fact_scopes.add_select_operator_for_dimension do |operator|
|
|
57
|
+
operator.add_select(level_object.fact_key, as: level_object.fact_alias)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param fact_scopes [Runtime::FactScopeCollection]
|
|
63
|
+
def add_to_group_by(fact_scopes)
|
|
64
|
+
supported_level_associations.each do |level_object|
|
|
65
|
+
fact_scopes.add_group_operator do |operator|
|
|
66
|
+
operator.add_group(level_object.fact_alias)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Assume the following levels, where (*) denotes has association to the fact
|
|
72
|
+
# L1
|
|
73
|
+
# L2 (*)
|
|
74
|
+
# L3 (*)
|
|
75
|
+
# L4 (*)
|
|
76
|
+
#
|
|
77
|
+
# Assuming the lowest level in the grain is L3:
|
|
78
|
+
#
|
|
79
|
+
# supported_level_associations
|
|
80
|
+
# # => [L2, L3] of LevelAssociation objects
|
|
81
|
+
#
|
|
82
|
+
# supported_level_definitions
|
|
83
|
+
# # => [L1, L2, L3] of BaseLevelDefinition objects
|
|
84
|
+
#
|
|
85
|
+
# [L1, L2, L3, L4].map{|x| x.has_association_with_level(x) }
|
|
86
|
+
# # => [false, true, true, false]
|
|
87
|
+
#
|
|
88
|
+
|
|
89
|
+
def supported_level_associations
|
|
90
|
+
return [] if null?
|
|
91
|
+
@supported_level_associations ||= grain.flat_map do |_dimension_name, lowest_level|
|
|
92
|
+
sub_cube.association_from_id(lowest_level.id).level_and_above
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def supported_level_definitions
|
|
97
|
+
return [] if null?
|
|
98
|
+
@_supported_level_definitions ||= grain.flat_map do |_dimension_name, lowest_level|
|
|
99
|
+
sub_cube.definition_from_id(lowest_level.id).level_and_above
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def supported_level_definition_ids
|
|
104
|
+
supported_level_definitions.map(&:id)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def supported_level_associations_lookup
|
|
108
|
+
@_supported_level_associations_lookup ||= supported_level_associations.index_by(&:id)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def has_association_with_level?(level_id)
|
|
112
|
+
!!supported_level_associations_lookup[level_id]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Schema
|
|
3
|
+
class DimensionAssociationCollection < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::Registrable
|
|
5
|
+
include Martyr::Translations
|
|
6
|
+
|
|
7
|
+
attr_reader :dimension_definitions
|
|
8
|
+
alias_method :find_dimension_association, :find_or_error
|
|
9
|
+
alias_method :supports_dimension?, :has_key?
|
|
10
|
+
|
|
11
|
+
# @param dimension_definitions [DimensionDefinitionCollection]
|
|
12
|
+
def initialize(dimension_definitions)
|
|
13
|
+
@dimension_definitions = dimension_definitions
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [LevelAssociation]
|
|
17
|
+
def has_dimension_level(dimension_name, level_name, **args)
|
|
18
|
+
if has_key?(dimension_name)
|
|
19
|
+
dimension = find_or_nil(dimension_name)
|
|
20
|
+
else
|
|
21
|
+
dimension = dimension_definitions.find_dimension(dimension_name)
|
|
22
|
+
dimension = Martyr::DimensionReference.new(dimension, LevelAssociationCollection)
|
|
23
|
+
register dimension
|
|
24
|
+
end
|
|
25
|
+
dimension.has_dimension_level(level_name, **args)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find_level_association(level_id)
|
|
29
|
+
with_standard_id(level_id) {|dimension, level| find_dimension_association(dimension).find_level(level)}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Schema
|
|
3
|
+
class LevelAssociation
|
|
4
|
+
include Martyr::Level
|
|
5
|
+
|
|
6
|
+
attr_accessor :level, :fact_key, :fact_alias, :sort
|
|
7
|
+
alias_method :level_definition, :level
|
|
8
|
+
|
|
9
|
+
# Important so that the to_i will take into account all levels defined for the dimension, not just the supported one
|
|
10
|
+
delegate :to_i, to: :level
|
|
11
|
+
|
|
12
|
+
delegate :label_key, :label_expression, to: :level
|
|
13
|
+
|
|
14
|
+
# @param collection [LevelAssociationCollection]
|
|
15
|
+
# @param level [BaseLevelDefinition]
|
|
16
|
+
def initialize(collection, level, fact_key: nil, fact_alias: nil, sort: nil)
|
|
17
|
+
@collection = collection
|
|
18
|
+
@level = level
|
|
19
|
+
@fact_key = fact_key || level.fact_key
|
|
20
|
+
@fact_alias = fact_alias || level.fact_alias
|
|
21
|
+
@sort = sort || level.sort
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def supported?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# Delegate everything to level
|
|
31
|
+
def method_missing(method_name, *args, &block)
|
|
32
|
+
level.respond_to?(method_name) ? level.send(method_name, *args, &block) : super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Schema
|
|
3
|
+
class LevelAssociationCollection < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::LevelCollection
|
|
5
|
+
|
|
6
|
+
# @param level [String, Symbol]
|
|
7
|
+
# @return [LevelAssociation]
|
|
8
|
+
def has_dimension_level(level, **args)
|
|
9
|
+
level_definition = dimension_definition.levels[level]
|
|
10
|
+
raise Schema::Error.new("Could not find level `#{level}` for dimension #{dimension_name}") unless level_definition
|
|
11
|
+
level_association = LevelAssociation.new(self, level_definition, **args)
|
|
12
|
+
register level_association
|
|
13
|
+
arr = sort_by{|_name, level| level.to_i}
|
|
14
|
+
clear.merge!(Hash[arr])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Schema
|
|
3
|
+
class DimensionDefinitionCollection < HashWithIndifferentAccess
|
|
4
|
+
include Martyr::Translations
|
|
5
|
+
include Martyr::Registrable
|
|
6
|
+
|
|
7
|
+
alias_method :supports_dimension?, :has_key?
|
|
8
|
+
alias_method :find_dimension, :find_or_error
|
|
9
|
+
alias_method :all, :to_hash
|
|
10
|
+
|
|
11
|
+
# @return [DimensionDefinition]
|
|
12
|
+
def define_dimension(*args, &block)
|
|
13
|
+
register PlainDimensionDefinition.new(*args, &block)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find_level_definition(level_id)
|
|
17
|
+
with_standard_id(level_id) { |dimension, level| find_dimension(dimension).find_level(level) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# # @param name [String, Symbol]
|
|
21
|
+
# # @return [DimensionDefinition] object that was found by traversing up the lookup tree
|
|
22
|
+
# def recursive_lookup(name)
|
|
23
|
+
# fetch(name.to_s, parent_dimension_definitions.try(:recursive_lookup, name.to_s))
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# def find_dimension(name)
|
|
27
|
+
# recursive_lookup(name) || raise(Schema::Error.new("Could not find dimension `#{name}`"))
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
|
|
31
|
+
# # @return [Hash] { dimension_name => PlainDimensionDefinition } including dimension definitions from all superclasses
|
|
32
|
+
# def all
|
|
33
|
+
# return to_hash unless parent_dimension_definitions
|
|
34
|
+
# to_hash.merge(parent_dimension_definitions.all)
|
|
35
|
+
# end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Schema
|
|
3
|
+
class PlainDimensionDefinition
|
|
4
|
+
include ActiveModel::Model
|
|
5
|
+
include Martyr::HasLevelCollection
|
|
6
|
+
|
|
7
|
+
attr_accessor :name, :title
|
|
8
|
+
delegate :degenerate_level, :query_level, to: :levels
|
|
9
|
+
|
|
10
|
+
# @param name [String]
|
|
11
|
+
# @option title [String]
|
|
12
|
+
def initialize(name, **options, &block)
|
|
13
|
+
super name: name.to_s,
|
|
14
|
+
title: options[:title] || name.to_s.titleize
|
|
15
|
+
|
|
16
|
+
@levels = LevelDefinitionCollection.new(dimension: self)
|
|
17
|
+
instance_eval(&block) if block
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dimension_definition
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# For reflection
|
|
25
|
+
def dimension?
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_data_slice(*args)
|
|
30
|
+
Runtime::PlainDimensionDataSlice.new(self, *args)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_memory_slice(*args)
|
|
34
|
+
Runtime::PlainDimensionMemorySlice.new(self, *args)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|