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,223 @@
1
+ module Martyr
2
+ module Runtime
3
+ class QueryLevelScope < BaseLevelScope
4
+
5
+ # @attribute primary_keys_for_load [Array<Integer>] primary keys to restrict when loading the level with full_load
6
+ attr_accessor :primary_keys_for_load
7
+
8
+ delegate :record_value, :primary_key, :label_key, :label_expression, :register_element_helper_methods, to: :level
9
+
10
+ def initialize(*args)
11
+ super
12
+ @scope = level.scope
13
+ end
14
+
15
+ def parent_association_name
16
+ level.parent_association_name_with_default
17
+ end
18
+
19
+ def sliceable?
20
+ true
21
+ end
22
+
23
+ def nullify
24
+ decorate_scope do |scope|
25
+ scope.where('0=1')
26
+ end
27
+ end
28
+
29
+ def slice_with(values)
30
+ decorate_scope do |scope|
31
+ scope.where label_key => values
32
+ end
33
+ set_bottom_sliced_level
34
+ end
35
+
36
+ def loaded?
37
+ !!@cache
38
+ end
39
+
40
+ # We prefer to keep reference of the lowest (biggest index) level that is sliced because load_from_level_below
41
+ # is more efficient than load_from_level_above (does not need to join table).
42
+ def set_bottom_sliced_level
43
+ collection.bottom_level_sliced_i = [collection.bottom_level_sliced_i, to_i].compact.max
44
+ end
45
+
46
+ def load
47
+ return true if loaded?
48
+
49
+ if !collection.bottom_level_sliced_i
50
+ set_bottom_sliced_level
51
+ full_load
52
+ elsif to_i == collection.bottom_level_sliced_i
53
+ full_load
54
+ elsif to_i > collection.bottom_level_sliced_i
55
+ load_from_level_above
56
+ elsif to_i < collection.bottom_level_sliced_i
57
+ load_from_level_below
58
+ else
59
+ raise Internal::Error.new("Inconsistency in `#{dimension_name}.#{name}` scope structure")
60
+ end
61
+
62
+ true
63
+ end
64
+
65
+ # @return [Array<ActiveRecord::Base>]
66
+ def all
67
+ self.load and return cached_records
68
+ end
69
+
70
+ def all_values
71
+ all.map{|x| x[level.label_field]}
72
+ end
73
+
74
+ def keys
75
+ self.load and return cached_keys
76
+ end
77
+
78
+ # @return [ActiveRecord::Base]
79
+ def fetch(primary_key_value)
80
+ self.load and return @cache[primary_key_value.to_i]
81
+ end
82
+
83
+ # TODO: this is making the assumption that only degenerate levels can be above a query level
84
+ # This method allows finding the value of the level identified in `level` that is the parent of the record in the
85
+ # current level object that is identified by `primary_key_value`. It traversed the hierarchy UP until reaching
86
+ # the desired `level`.
87
+ #
88
+ # @param primary_key_value [String,Integer]
89
+ # @param level [Martyr::Level] this level must be equal or above the current level
90
+ # @return [ActiveRecord::Base, String] the record if query level, or the value if degenerate
91
+ def recursive_lookup_up(primary_key_value, level:)
92
+ record = fetch(primary_key_value)
93
+ return record if name == level.name
94
+ return record[level.query_level_key] if level_above.degenerate?
95
+
96
+ level_above.recursive_lookup_up(record_parent_primary_key(record), level: level)
97
+ end
98
+
99
+ # TODO: this is making the assumption that only degenerate levels can be above a query level
100
+ # @param records [Array<String>, String, Array<ActiveRecord::Base>, ActiveRecord::Base] two options:
101
+ # - Single or Array of values as evaluated by the level value strategy, e.g. 'invoice-1'
102
+ # - Single or Array of active record objects - this helps DRYing up code in this package that already obtained records
103
+ # @param level [Martyr::Level] this level must be equal or below the current level
104
+ # @return [Array<ActiveRecord::Base>, Array<String>]
105
+ def recursive_lookup_down(records, level:)
106
+ records = Array.wrap(records)
107
+ records = records.flat_map{|value| cached_records_by_value[value]} if records.first.is_a?(String)
108
+
109
+ return records if name == level.name
110
+ return records.map{|r| r[level.query_level_key]}.uniq if level.degenerate?
111
+ child_records = level_below.fetch_by_parent(records.map{|x| record_primary_key(x)})
112
+ level_below.recursive_lookup_down(child_records, level: level)
113
+ end
114
+
115
+ def decorate_scope(&block)
116
+ original_scope = @scope
117
+ @scope = Proc.new do
118
+ block.call(original_scope.call)
119
+ end
120
+ end
121
+
122
+ protected
123
+
124
+ # @param parent_primary_key_values [Array<Integer>]
125
+ # @return [Array<ActiveRecord::Base>] all records whose parent keys were given in parent_primary_key_values
126
+ def fetch_by_parent(parent_primary_key_values)
127
+ self.load and return Array.wrap(parent_primary_key_values).flat_map{|primary_key_value| cached_records_by_parent[primary_key_value]}
128
+ end
129
+
130
+ # TODO: inject one cube if exists
131
+
132
+ # def slice_from_fact_keys
133
+ # decorate_scope do |scope|
134
+ # scope.where primary_key => collection.foreign_keys_from_facts_for(self)
135
+ # end
136
+ # execute_query
137
+ # end
138
+
139
+ # Loading strategies
140
+
141
+ # def load_from_fact
142
+ # return slice_from_fact_keys if common_denominator_with_cube.name == name
143
+ # common_denominator_with_cube.load_from_fact
144
+ # load_from_level_below
145
+ # end
146
+
147
+ def full_load
148
+ if primary_keys_for_load.present?
149
+ set_cache @scope.call.where(primary_key => primary_keys_for_load)
150
+ else
151
+ set_cache @scope.call
152
+ end
153
+ end
154
+
155
+ def load_from_level_above
156
+ raise Schema::Error.new("Cannot infer slice for dimension `#{dimension_name}` level `#{name}`: parent level is not query level") unless level_above.query?
157
+ parent_ids = level_above.all.map { |x| level_above.record_primary_key(x) }
158
+ set_cache @scope.call.joins(parent_association_name.to_sym).where(parent_association.foreign_key => parent_ids)
159
+ end
160
+
161
+ def load_from_level_below
162
+ level_below = query_level_below
163
+ raise Schema::Error.new("Cannot infer slice for dimension `#{dimension_name}` level `#{name}`: child level cannot be found") unless level_below
164
+
165
+ ids_from_child = level_below.all.map { |x| level_below.record_parent_primary_key(x) }.uniq
166
+ set_cache @scope.call.where(primary_key => ids_from_child)
167
+ end
168
+
169
+ # TODO: this is making the assumption that only degenerate levels can be above a query level
170
+ # @return [ActiveRecord::Reflection::AssociationReflection]
171
+ def parent_association
172
+ return nil unless level_above.query?
173
+ return @parent_association if @parent_association
174
+
175
+ relation = @scope.call.klass.reflections[parent_association_name]
176
+ raise Schema::Error.new("Cannot find parent association `#{parent_association_name}` for dimension `#{dimension_name}` level `#{name}`") unless relation
177
+ @parent_association = relation
178
+ end
179
+
180
+ def set_cache(scope)
181
+ @cache = scope.index_by { |x| record_primary_key(x) }
182
+ true
183
+ end
184
+
185
+ # @return [Array<ActiveRecord::Base>]
186
+ def cached_records
187
+ @cache.values
188
+ end
189
+
190
+ def cached_keys
191
+ @cache.keys
192
+ end
193
+
194
+ # @return [Hash] { parent_key1 => Array<ActiveRecord::Base> }
195
+ def cached_records_by_parent
196
+ cached_records_by(parent_association.foreign_key)
197
+ end
198
+
199
+ # @return [Hash] { value1 => Array<ActiveRecord::Base> }
200
+ def cached_records_by_value
201
+ cached_records_by(level.label_field)
202
+ end
203
+
204
+ public
205
+
206
+ # @return [Hash] { key1 => Array<ActiveRecord::Base>, key2 => Array<ActiveRecord::Base> }
207
+ def cached_records_by(key)
208
+ self.load
209
+ @cached_records_by ||= {}
210
+ return @cached_records_by[key] if @cached_records_by[key]
211
+ @cached_records_by[key] = cached_records.group_by{|x| x[key]}
212
+ end
213
+
214
+ def record_primary_key(record)
215
+ record[primary_key].to_i
216
+ end
217
+
218
+ def record_parent_primary_key(record)
219
+ record[parent_association.foreign_key]
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,62 @@
1
+ module Martyr
2
+ module Runtime
3
+ class BaseFactScope
4
+ attr_reader :fact_definition, :scope
5
+ delegate :name, :supports_metric?, :supports_dimension_level?, :dimensions, to: :fact_definition
6
+
7
+ # = Scope accessors
8
+
9
+ # @param fact_definition [Schema::MainFactDefinition]
10
+ def initialize(fact_definition)
11
+ @fact_definition = fact_definition
12
+ @scope = fact_definition.scope
13
+ end
14
+
15
+ def run_scope
16
+ @_run_scope ||= scope.call
17
+ end
18
+
19
+ def scope_sql
20
+ run_scope.try(:to_sql)
21
+ end
22
+
23
+ def null?
24
+ @scope.is_a?(NullScope)
25
+ end
26
+
27
+ def set_null_scope
28
+ @scope = NullScope.new
29
+ end
30
+
31
+ # @return [String] how to add a where condition to the level
32
+ def level_key_for_where(level_id)
33
+ fact_definition.find_level_association(level_id).fact_key
34
+ end
35
+
36
+ # = Scope support check
37
+
38
+ # = Scope updater
39
+
40
+ # Decorator pattern. The block must return a new scope.
41
+ # @example
42
+ # Let @scope be: -> { Invoice.all }
43
+ #
44
+ # decorate_scope {|scope| scope.where(id: 5)}
45
+ #
46
+ # @scope is now: -> { ->{ Invoice.all }.call.where(id: 5) }
47
+ #
48
+ # decorate_scope {|scope| scope.where.not(id: 7)}
49
+ #
50
+ # @scope is now: -> { -> { ->{ Invoice.all }.call.where(id: 5) }.call.where.not(id: 7) }
51
+ #
52
+ def decorate_scope(&block)
53
+ return if null?
54
+ original_scope = self.scope
55
+ @scope = Proc.new do
56
+ block.call(original_scope.call, self)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,127 @@
1
+ module Martyr
2
+ module Runtime
3
+ class FactScopeCollection < HashWithIndifferentAccess
4
+ include Martyr::Registrable
5
+
6
+ alias_method :scopes, :values
7
+ delegate :set_null_scope, :null?, to: :main_fact
8
+
9
+ def operators
10
+ @operators ||= []
11
+ end
12
+
13
+ # Add the add_<operator> methods:
14
+ # add_select_operator_for_metric
15
+ # add_select_operator_for_dimension
16
+ # add_group_opeartor
17
+ # add_where_operator_for_dimension
18
+ # add_where_operator_for_metric
19
+ [GroupOperator, SelectOperatorForMetric, SelectOperatorForDimension,
20
+ WhereOperatorForDimension, WhereOperatorForMetric].each {|op| op.register_to(self)}
21
+
22
+ # = Running
23
+
24
+ def run
25
+ ActiveRecord::Base.connection.execute(combined_sql)
26
+ end
27
+
28
+ def test
29
+ run
30
+ true
31
+ end
32
+
33
+ # = Accessors
34
+
35
+ def sub_facts
36
+ except(:main).values
37
+ end
38
+
39
+ def main_fact
40
+ fetch(:main)
41
+ end
42
+
43
+ # = Access to SQL
44
+
45
+ def combined_sql
46
+ @combined_sql ||= combined_outer_sql
47
+ end
48
+
49
+ private
50
+
51
+ def decorate_inner_scopes
52
+ operators.each do |operator|
53
+ operator.apply_on_inner(main_fact)
54
+ end
55
+ return false if null?
56
+ sub_facts.each do |sub_fact_scope|
57
+ operators.each do |operator|
58
+ operator.apply_on_inner(sub_fact_scope)
59
+ end
60
+ end
61
+ true
62
+ end
63
+
64
+ def join_sub_facts
65
+ sub_facts.each {|x| x.add_to_join(main_fact)}
66
+ end
67
+
68
+ def combined_inner_scope
69
+ decorate_inner_scopes
70
+ join_sub_facts
71
+ main_fact.run_scope
72
+ end
73
+
74
+ def combined_inner_sql
75
+ combined_inner_scope.to_sql
76
+ end
77
+
78
+ def combined_outer_sql
79
+ wrapper = SqlWrapper.new(combined_inner_sql)
80
+ operators.each { |operator| operator.reapply_on_outer_wrapper(wrapper) }
81
+ wrapper.to_sql
82
+ end
83
+
84
+ class SqlWrapper
85
+ attr_reader :from_sql, :where_added
86
+ attr_accessor :select, :group, :where
87
+
88
+ def initialize(from_sql)
89
+ @from_sql = from_sql
90
+ @select = []
91
+ @group = []
92
+
93
+ # TODO: find a better way to solve this other than using Dummy.
94
+ # Note that the same approach is not doable for select and group by, since for these ActiveRecord
95
+ # requires the table to exist.
96
+ @where = Dummy
97
+ @where_added = false
98
+ end
99
+
100
+ def add_to_select(operand)
101
+ @select << operand
102
+ end
103
+
104
+ def add_to_where(*args)
105
+ @where = where.where(*args)
106
+ @where_added = true
107
+ end
108
+
109
+ def add_to_group_by(operand)
110
+ @group << operand
111
+ end
112
+
113
+ def to_sql
114
+ sql = "SELECT #{select.join(', ')}" +
115
+ " FROM (#{from_sql}) martyr_wrapper"
116
+ sql += " WHERE #{where.to_sql.match(/WHERE (.*)$/)[1]}" if where_added
117
+ sql += " GROUP BY #{group.join(', ')}" if @group.present?
118
+ sql
119
+ end
120
+
121
+ class Dummy < ActiveRecord::Base
122
+ end
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,7 @@
1
+ module Martyr
2
+ module Runtime
3
+ class MainFactScope < BaseFactScope
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Martyr
2
+ module Runtime
3
+ class NullScope
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module Martyr
2
+ module Runtime
3
+ class SubFactScope < BaseFactScope
4
+
5
+ delegate :add_to_join, to: :fact_definition
6
+
7
+ def add_to_join(main_fact_scope)
8
+ raise Schema::Error.new("Sub query #{name} does not have a join clause. Did you forget to call `joins_with`?") unless fact_definition.join_clause
9
+ main_fact_scope.decorate_scope do |scope|
10
+ scope.joins("#{fact_definition.join_clause} (#{scope_sql}) #{fact_definition.name} ON #{fact_definition.join_on}")
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Martyr
2
+ module Runtime
3
+ class WrappedFactScope
4
+
5
+ def initialize(sql)
6
+
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,67 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PivotAxis
4
+ include ActiveModel::Model
5
+
6
+ attr_accessor :grain_elements
7
+ attr_reader :values
8
+
9
+ def inspect
10
+ grain_elements.inspect
11
+ end
12
+
13
+ def ids
14
+ grain_elements.map(&:id)
15
+ end
16
+
17
+ # = CSV
18
+
19
+ # Run on column axis only
20
+ def add_header_column_cells_to_csv(csv, row_axis)
21
+ grain_elements.each do |grain|
22
+ prev_value = nil
23
+ column_values = values.map do |x|
24
+ # #chomp is just a trick to avoid removing consecutive (total)
25
+ prev_value.try(:chomp, PivotCell::TOTAL_VALUE) == x[grain.id] ? nil : prev_value = x[grain.id]
26
+ end
27
+ csv << row_axis.csv_empty_row_axis_cells + column_values
28
+ end
29
+ end
30
+
31
+ # Run on row axis only
32
+ def csv_empty_row_axis_cells
33
+ [nil] * grain_elements.length
34
+ end
35
+
36
+ # @return [Array<Hash>] where each hash is of format {level_id_1 => value_1, ... }
37
+ def load_values(cells, reset: false)
38
+ @values = nil if reset
39
+ @values ||= cells.map { |cell| hash_grain_value_for(cell) }.uniq
40
+ end
41
+
42
+ def index_values_lookup
43
+ @index_values_lookup ||= Hash[values.each_with_index.map{|value_hash, i| [value_hash, i]}]
44
+ end
45
+
46
+ def sort_cells_by_values(cells)
47
+ cells.sort_by{|cell| index_values_lookup[hash_grain_value_for(cell)]}
48
+ end
49
+
50
+ def flat_values_nil_hash
51
+ Hash[values.map{|hash| [hash.values.join(' : '), nil]}]
52
+ end
53
+
54
+ def hash_values_nil_hash
55
+ Hash[values.map{|x| [x, nil]}]
56
+ end
57
+
58
+ def flat_grain_value_for(cell)
59
+ grain_elements.map{ |grain| grain.cell_value(cell) }.join(' : ')
60
+ end
61
+
62
+ def hash_grain_value_for(cell)
63
+ Hash[grain_elements.map{ |grain| [grain.id, grain.cell_value(cell)] }]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,54 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PivotCell
4
+ METRIC_COORD_KEY = 'metric'
5
+ TOTAL_VALUE = '(total)'
6
+
7
+ attr_reader :metric_id, :metric_human_name, :element
8
+ delegate :facts, :coordinates, to: :element
9
+
10
+ # @param sub_total_levels [Array<String>]
11
+ def initialize(metric, element, sub_total_levels = [])
12
+ @metric_id = metric.id
13
+ @metric_human_name = metric.human_name
14
+ @element = element
15
+ @sub_total_levels = sub_total_levels
16
+ end
17
+
18
+ def inspect
19
+ to_hash.inspect
20
+ end
21
+
22
+ def to_hash
23
+ {'metric_human_name' => metric_human_name, 'value' => value}.merge!(element.grain_hash)
24
+ end
25
+
26
+ def coordinates
27
+ element.coordinates(metric_id).merge(METRIC_COORD_KEY => metric_id)
28
+ end
29
+
30
+ def to_axis_values(pivot_axis, flat: true)
31
+ flat ? pivot_axis.flat_grain_value_for(self) : pivot_axis.hash_grain_value_for(self)
32
+ end
33
+
34
+ def value
35
+ element.fetch(metric_id)
36
+ end
37
+
38
+ def warning
39
+ element.warning(metric_id)
40
+ end
41
+
42
+ def [](key)
43
+ case key.to_s
44
+ when 'metric'
45
+ metric_id
46
+ when 'value'
47
+ value
48
+ else
49
+ @sub_total_levels.include?(key.to_s) ? TOTAL_VALUE : element.fetch(key)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,22 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PivotGrainElement
4
+ include ActiveModel::Model
5
+
6
+ attr_accessor :id, :metrics, :level_definition
7
+
8
+ def inspect
9
+ id.inspect
10
+ end
11
+
12
+ def cell_value(cell)
13
+ if metrics.present?
14
+ cell.metric_human_name
15
+ else
16
+ cell[level_definition.id]
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ module Martyr
2
+ module Runtime
3
+ class PivotRow
4
+ include ActiveModel::Model
5
+
6
+ attr_reader :pivot_table, :header, :cells
7
+ delegate :column_axis, to: :pivot_table
8
+
9
+ # @attribute header [Hash] array of keys and values for each grain in the axis
10
+ def initialize(pivot_table, header, cells)
11
+ @pivot_table = pivot_table
12
+ @header = header
13
+ @cells = column_axis.sort_cells_by_values(cells)
14
+ end
15
+
16
+ def inspect
17
+ {row_header: header, column_headers: column_headers}.inspect
18
+ end
19
+
20
+ # = CSV
21
+
22
+ def to_a(previous: nil)
23
+ row_arr = header.values + cells_by_column_headers.values.map(&:value)
24
+ return row_arr unless previous
25
+ row_arr
26
+ # row_arr.each_with_index.map{|x, i| i < header.length && x.try(:chomp, PivotCell::TOTAL_VALUE) == previous[i] ? nil : x}
27
+ end
28
+
29
+ # = Value retrieval
30
+
31
+ def cell_at(column_header)
32
+ cells_by_column_headers[column_header]
33
+ end
34
+
35
+ def [](index)
36
+ cell_at column_headers[index]
37
+ end
38
+
39
+ def column_headers
40
+ column_axis.hash_values_nil_hash.keys
41
+ end
42
+
43
+ def cells_by_column_headers
44
+ @cells_by_column_headers ||= Hash[cells.map{|cell| [cell.to_axis_values(column_axis, flat: false), cell]}]
45
+ end
46
+
47
+ end
48
+ end
49
+ end