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