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