martyr 0.1.74.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.tags +868 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/Rakefile +1 -0
- data/TODO.txt +54 -0
- data/bin/console +62 -0
- data/bin/setup +7 -0
- data/lib/martyr/base_cube.rb +73 -0
- data/lib/martyr/cube.rb +134 -0
- data/lib/martyr/dimension_reference.rb +26 -0
- data/lib/martyr/errors.rb +20 -0
- data/lib/martyr/helpers/delegators.rb +17 -0
- data/lib/martyr/helpers/intervals.rb +222 -0
- data/lib/martyr/helpers/metric_id_standardizer.rb +47 -0
- data/lib/martyr/helpers/registrable.rb +15 -0
- data/lib/martyr/helpers/sorter.rb +79 -0
- data/lib/martyr/helpers/translations.rb +34 -0
- data/lib/martyr/level_concern/has_level_collection.rb +11 -0
- data/lib/martyr/level_concern/level.rb +45 -0
- data/lib/martyr/level_concern/level_collection.rb +60 -0
- data/lib/martyr/level_concern/level_comparator.rb +45 -0
- data/lib/martyr/level_concern/level_definitions_by_dimension.rb +24 -0
- data/lib/martyr/runtime/data_set/coordinates.rb +108 -0
- data/lib/martyr/runtime/data_set/element.rb +66 -0
- data/lib/martyr/runtime/data_set/element_common.rb +51 -0
- data/lib/martyr/runtime/data_set/element_locator.rb +143 -0
- data/lib/martyr/runtime/data_set/fact.rb +83 -0
- data/lib/martyr/runtime/data_set/fact_indexer.rb +72 -0
- data/lib/martyr/runtime/data_set/future_fact_value.rb +58 -0
- data/lib/martyr/runtime/data_set/future_metric.rb +40 -0
- data/lib/martyr/runtime/data_set/virtual_element.rb +131 -0
- data/lib/martyr/runtime/data_set/virtual_elements_builder.rb +202 -0
- data/lib/martyr/runtime/dimension_scopes/base_level_scope.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/degenerate_level_scope.rb +76 -0
- data/lib/martyr/runtime/dimension_scopes/dimension_scope_collection.rb +78 -0
- data/lib/martyr/runtime/dimension_scopes/level_scope_collection.rb +20 -0
- data/lib/martyr/runtime/dimension_scopes/query_level_scope.rb +223 -0
- data/lib/martyr/runtime/fact_scopes/base_fact_scope.rb +62 -0
- data/lib/martyr/runtime/fact_scopes/fact_scope_collection.rb +127 -0
- data/lib/martyr/runtime/fact_scopes/main_fact_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/null_scope.rb +7 -0
- data/lib/martyr/runtime/fact_scopes/sub_fact_scope.rb +16 -0
- data/lib/martyr/runtime/fact_scopes/wrapped_fact_scope.rb +11 -0
- data/lib/martyr/runtime/pivot/pivot_axis.rb +67 -0
- data/lib/martyr/runtime/pivot/pivot_cell.rb +54 -0
- data/lib/martyr/runtime/pivot/pivot_grain_element.rb +22 -0
- data/lib/martyr/runtime/pivot/pivot_row.rb +49 -0
- data/lib/martyr/runtime/pivot/pivot_table.rb +109 -0
- data/lib/martyr/runtime/pivot/pivot_table_builder.rb +125 -0
- data/lib/martyr/runtime/query/metric_dependency_resolver.rb +149 -0
- data/lib/martyr/runtime/query/query_context.rb +246 -0
- data/lib/martyr/runtime/query/query_context_builder.rb +215 -0
- data/lib/martyr/runtime/scope_operators/base_operator.rb +113 -0
- data/lib/martyr/runtime/scope_operators/group_operator.rb +18 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_dimension.rb +24 -0
- data/lib/martyr/runtime/scope_operators/select_operator_for_metric.rb +35 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_dimension.rb +43 -0
- data/lib/martyr/runtime/scope_operators/where_operator_for_metric.rb +25 -0
- data/lib/martyr/runtime/slices/data_slices/data_slice.rb +70 -0
- data/lib/martyr/runtime/slices/data_slices/metric_data_slice.rb +54 -0
- data/lib/martyr/runtime/slices/data_slices/plain_dimension_data_slice.rb +109 -0
- data/lib/martyr/runtime/slices/data_slices/time_dimension_data_slice.rb +9 -0
- data/lib/martyr/runtime/slices/has_scoped_levels.rb +29 -0
- data/lib/martyr/runtime/slices/memory_slices/TO_DELETE.md +188 -0
- data/lib/martyr/runtime/slices/memory_slices/memory_slice.rb +84 -0
- data/lib/martyr/runtime/slices/memory_slices/metric_memory_slice.rb +59 -0
- data/lib/martyr/runtime/slices/memory_slices/plain_dimension_memory_slice.rb +48 -0
- data/lib/martyr/runtime/slices/scopeable_slice_data.rb +73 -0
- data/lib/martyr/runtime/slices/slice_definitions/base_slice_definition.rb +30 -0
- data/lib/martyr/runtime/slices/slice_definitions/metric_slice_definition.rb +120 -0
- data/lib/martyr/runtime/slices/slice_definitions/plain_dimension_level_slice_definition.rb +26 -0
- data/lib/martyr/runtime/sub_cubes/fact_filler_strategies.rb +61 -0
- data/lib/martyr/runtime/sub_cubes/query_metrics.rb +56 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube.rb +134 -0
- data/lib/martyr/runtime/sub_cubes/sub_cube_grain.rb +117 -0
- data/lib/martyr/schema/dimension_associations/dimension_association_collection.rb +33 -0
- data/lib/martyr/schema/dimension_associations/level_association.rb +37 -0
- data/lib/martyr/schema/dimension_associations/level_association_collection.rb +18 -0
- data/lib/martyr/schema/dimensions/dimension_definition_collection.rb +39 -0
- data/lib/martyr/schema/dimensions/plain_dimension_definition.rb +39 -0
- data/lib/martyr/schema/dimensions/time_dimension_definition.rb +24 -0
- data/lib/martyr/schema/facts/base_fact_definition.rb +22 -0
- data/lib/martyr/schema/facts/fact_definition_collection.rb +44 -0
- data/lib/martyr/schema/facts/main_fact_definition.rb +45 -0
- data/lib/martyr/schema/facts/sub_fact_definition.rb +44 -0
- data/lib/martyr/schema/metrics/base_metric.rb +77 -0
- data/lib/martyr/schema/metrics/built_in_metric.rb +38 -0
- data/lib/martyr/schema/metrics/count_distinct_metric.rb +172 -0
- data/lib/martyr/schema/metrics/custom_metric.rb +26 -0
- data/lib/martyr/schema/metrics/custom_rollup.rb +31 -0
- data/lib/martyr/schema/metrics/dependency_inferrer.rb +150 -0
- data/lib/martyr/schema/metrics/metric_definition_collection.rb +94 -0
- data/lib/martyr/schema/named_scopes/named_scope.rb +19 -0
- data/lib/martyr/schema/named_scopes/named_scope_collection.rb +42 -0
- data/lib/martyr/schema/plain_dimension_levels/base_level_definition.rb +39 -0
- data/lib/martyr/schema/plain_dimension_levels/degenerate_level_definition.rb +75 -0
- data/lib/martyr/schema/plain_dimension_levels/level_definition_collection.rb +15 -0
- data/lib/martyr/schema/plain_dimension_levels/query_level_definition.rb +99 -0
- data/lib/martyr/version.rb +3 -0
- data/lib/martyr/virtual_cube.rb +74 -0
- data/lib/martyr.rb +55 -0
- data/martyr.gemspec +41 -0
- metadata +296 -0
@@ -0,0 +1,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,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,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
|