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