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