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,109 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class PivotTable
|
|
4
|
+
include ActiveModel::Model
|
|
5
|
+
|
|
6
|
+
# @attribute metrics [Array<BaseMetric>]
|
|
7
|
+
# @attribute row_axis [PivotAxis]
|
|
8
|
+
# @attribute column_axis [PivotAxis]
|
|
9
|
+
# @attribute pivot_grain [Array<String>] array of level ids
|
|
10
|
+
attr_accessor :metrics, :row_axis, :column_axis, :pivot_grain, :row_totals, :column_totals, :sort
|
|
11
|
+
attr_reader :elements
|
|
12
|
+
|
|
13
|
+
def initialize(query_context, *args)
|
|
14
|
+
super(*args)
|
|
15
|
+
|
|
16
|
+
# We don't restrict metrics since custom rollups may have dependencies
|
|
17
|
+
@elements = query_context.elements(levels: pivot_grain, sort: sort)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reload
|
|
21
|
+
@_cells = nil
|
|
22
|
+
@_lowest_cells = nil
|
|
23
|
+
row_axis.load_values(cells, reset: true)
|
|
24
|
+
column_axis.load_values(cells, reset: true)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def inspect
|
|
29
|
+
{metrics: metrics.map(&:id), levels: pivot_grain, on_rows: row_axis, on_columns: column_axis, totals: {rows: row_totals, columns: column_totals}}.inspect
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cells
|
|
33
|
+
@_cells ||= sort_cells(lowest_cells + sub_totals)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def lowest_cells
|
|
37
|
+
@_lowest_cells ||= metrics.flat_map do |metric|
|
|
38
|
+
elements.map do |element|
|
|
39
|
+
PivotCell.new metric, element
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sub_totals
|
|
45
|
+
resettable_grain = row_totals ? row_axis.ids : []
|
|
46
|
+
resettable_grain += column_totals ? column_axis.ids : []
|
|
47
|
+
resettable_grain -= [:metrics]
|
|
48
|
+
(0...resettable_grain.length).flat_map do |x|
|
|
49
|
+
reset = resettable_grain[x..-1]
|
|
50
|
+
elements.index_by do |element|
|
|
51
|
+
(pivot_grain - reset).map{|level_id| element[level_id]}
|
|
52
|
+
end.flat_map do |_sub_total_key, representative|
|
|
53
|
+
element = representative.locate reset: reset
|
|
54
|
+
metrics.map do |metric|
|
|
55
|
+
PivotCell.new(metric, element, reset)
|
|
56
|
+
end
|
|
57
|
+
end.reject(:empty?).compact
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def transpose
|
|
62
|
+
self.row_axis, self.column_axis = column_axis, row_axis
|
|
63
|
+
reload
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_chart(name: nil)
|
|
67
|
+
cells.group_by do |cell|
|
|
68
|
+
cell.to_axis_values(row_axis)
|
|
69
|
+
end.map do |row_grain_values, cells|
|
|
70
|
+
data = cells.inject(column_axis.flat_values_nil_hash) { |h, cell| h.merge! cell.to_axis_values(column_axis) => cell.value }
|
|
71
|
+
{name: name || row_grain_values, data: data}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_csv
|
|
76
|
+
CSV.generate do |csv|
|
|
77
|
+
column_axis.add_header_column_cells_to_csv(csv, row_axis)
|
|
78
|
+
prev_row = nil
|
|
79
|
+
rows.each do |row|
|
|
80
|
+
csv << prev_row = row.to_a(previous: prev_row)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def rows
|
|
86
|
+
cells.group_by do |cell|
|
|
87
|
+
cell.to_axis_values(row_axis, flat: false)
|
|
88
|
+
end.map do |row_grain_values, cells|
|
|
89
|
+
PivotRow.new(self, row_grain_values, cells)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def metrics_sort_order
|
|
96
|
+
@_metrics_sort_order ||= Hash[metrics.each_with_index.map{|m, i| [m.id, i]}]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def sort_cells(cells_arr)
|
|
100
|
+
return cells_arr if sort.present?
|
|
101
|
+
cells_arr.sort_by do |cell|
|
|
102
|
+
pivot_grain.map{|level_id| cell[level_id] || '' } + [metrics_sort_order[cell.metric_id]]
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class PivotTableBuilder
|
|
4
|
+
ALL_METRICS_KEY = 'metrics'
|
|
5
|
+
|
|
6
|
+
attr_reader :query_context, :on_columns_args, :on_rows_args, :sort_args, :in_cells_arg, :row_totals, :column_totals
|
|
7
|
+
delegate :standardizer, :definition_from_id, to: :query_context
|
|
8
|
+
|
|
9
|
+
delegate :cells, :elements, :to_chart, :to_csv, to: :table
|
|
10
|
+
|
|
11
|
+
def initialize(query_context)
|
|
12
|
+
@query_context = query_context
|
|
13
|
+
@on_columns_args = []
|
|
14
|
+
@on_rows_args = []
|
|
15
|
+
@sort_args = {}
|
|
16
|
+
@row_totals = false
|
|
17
|
+
@column_totals = false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def select(*metric_ids)
|
|
21
|
+
dup.instance_eval do
|
|
22
|
+
@metrics = standardizer.standardize(metric_ids)
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sort(*args)
|
|
28
|
+
@sort_args.merge! Sorter.args_to_hash(args.length == 1 ? args.first : args)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_columns(*level_ids)
|
|
33
|
+
dup.instance_eval do
|
|
34
|
+
validate_metrics_in_level_ids(level_ids)
|
|
35
|
+
@on_columns_args = level_ids.uniq
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def on_rows(*level_ids)
|
|
41
|
+
dup.instance_eval do
|
|
42
|
+
validate_metrics_in_level_ids(level_ids)
|
|
43
|
+
@on_rows_args = level_ids.uniq
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def with_totals(rows: true, columns: true)
|
|
49
|
+
dup.instance_eval do
|
|
50
|
+
@row_totals = !!rows
|
|
51
|
+
@column_totals = !!columns
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def in_cells(metric_id)
|
|
57
|
+
raise Query::Error.new('Cannot have more than one metric in cells') if metric_id.to_s == ALL_METRICS_KEY
|
|
58
|
+
raise Query::Error.new("#{metric_id} is not a metric") unless metric?(metric_id)
|
|
59
|
+
@in_cells_arg = standardizer.standardize(metric_id)
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build
|
|
64
|
+
raise Query::Error.new('No columns were selected') unless on_columns_args.present?
|
|
65
|
+
raise Query::Error.new('No rows were selected') unless on_rows_args.present?
|
|
66
|
+
raise Query::Error.new('At least one metric has to be defined in pivot') unless metric_definition_count > 0
|
|
67
|
+
raise Query::Error.new('Metrics can either be on columns or rows or in cells') if metric_definition_count > 1
|
|
68
|
+
PivotTable.new(query_context, metrics: metrics, pivot_grain: pivot_grain,
|
|
69
|
+
row_axis: axis_for(on_rows_args), column_axis: axis_for(on_columns_args),
|
|
70
|
+
row_totals: row_totals, column_totals: column_totals, sort: sort_args).reload
|
|
71
|
+
end
|
|
72
|
+
alias_method :table, :build
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def metric?(id)
|
|
77
|
+
return true if id.to_s == ALL_METRICS_KEY
|
|
78
|
+
query_context.metric? standardizer.standardize(id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_metrics_in_level_ids(level_ids)
|
|
82
|
+
level_ids.each do |level_id|
|
|
83
|
+
next if level_id.to_s == ALL_METRICS_KEY
|
|
84
|
+
level_id = standardizer.standardize(level_id)
|
|
85
|
+
raise Query::Error.new("#{level_id}: Cannot pivot on individual metrics. " +
|
|
86
|
+
"Use 'metrics' for `on_columns` or `on_rows`, " +
|
|
87
|
+
"`select` to restrict metrics or `in_cells` to display one metric") if metric?(level_id)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def metric_ids
|
|
92
|
+
return [in_cells_arg] if in_cells_arg
|
|
93
|
+
@metrics || query_context.metric_ids
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def metric_definition_count
|
|
97
|
+
(in_cells_arg ? 1 : 0) + (on_columns_args + on_rows_args).select { |x| metric?(x) }.length
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def without_metrics(collection)
|
|
101
|
+
collection.reject { |x| metric?(x) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def metrics
|
|
105
|
+
metric_ids.map { |x| query_context.metric(x) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def pivot_grain
|
|
109
|
+
without_metrics(on_columns_args + on_rows_args)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def axis_for(collection)
|
|
113
|
+
grain_elements = collection.map do |level_id|
|
|
114
|
+
if level_id.to_s == ALL_METRICS_KEY
|
|
115
|
+
Runtime::PivotGrainElement.new(id: level_id, metrics: metrics)
|
|
116
|
+
else
|
|
117
|
+
Runtime::PivotGrainElement.new(id: level_id, level_definition: query_context.definition_from_id(level_id))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
PivotAxis.new grain_elements: grain_elements
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class MetricDependencyResolver
|
|
4
|
+
attr_reader :cube
|
|
5
|
+
|
|
6
|
+
# @param cube [BaseCube] either virtual or regular cube
|
|
7
|
+
def initialize(cube)
|
|
8
|
+
@cube = cube
|
|
9
|
+
@metrics_by_cube = {}
|
|
10
|
+
@inferred_fact_grain_by_cube = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_hash
|
|
14
|
+
Hash[@metrics_by_cube.map{ |cube_name, arr| [cube_name, arr.keys] }]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inspect
|
|
18
|
+
to_hash.inspect
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @option all [Boolean] send true if all metrics, including dependents, should be retrieved. Otherwise, only
|
|
22
|
+
# explicitly asked-for metrics will be included in the result set
|
|
23
|
+
# @return [Array<BaseMetric>]
|
|
24
|
+
def metrics(all: false)
|
|
25
|
+
metric_entries = @metrics_by_cube.flat_map { |_cube_name, metric_ids_hash| metric_ids_hash.values }
|
|
26
|
+
metric_entries.select! { |entry| entry[:explicit] } unless all
|
|
27
|
+
metric_entries.map { |entry| entry[:metric] }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @return [Array<String>] metric IDs
|
|
31
|
+
def metric_ids(all: false)
|
|
32
|
+
metrics(all: all).map(&:id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param cube_name [String]
|
|
36
|
+
# @option all [Boolean] send true if all metrics, including dependents, should be retrieved. Otherwise, only
|
|
37
|
+
# explicitly asked-for metrics will be included in the result set
|
|
38
|
+
# @return [Array<BaseMetric>]
|
|
39
|
+
def metrics_for(cube_name, all: false)
|
|
40
|
+
relevant_entries_for(cube_name, all: all).map do |_metric_id, metric_entry|
|
|
41
|
+
metric_entry[:metric]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @see metrics_for
|
|
46
|
+
# @return [Array<String>] metric IDs
|
|
47
|
+
def metric_ids_for(cube_name, all: false)
|
|
48
|
+
relevant_entries_for(cube_name, all: all).map(&:first)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Array<String>] of all fact grains combined acrross cubes
|
|
52
|
+
def inferred_fact_grain
|
|
53
|
+
@inferred_fact_grain_by_cube.flat_map{ |_cube_name, levels_lookup_hash| levels_lookup_hash.keys }.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Array<String>] array of level IDs that need to be part of the fact grain in order for the metrics to
|
|
57
|
+
# compute, including any dependent metrics. This does not include the default_fact_grain
|
|
58
|
+
def inferred_fact_grain_for(cube_name)
|
|
59
|
+
@inferred_fact_grain_by_cube[cube_name].try(:keys) || []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @return [Array<String>] array of sub facts that need to be joined in order to support the required metrics,
|
|
63
|
+
# as provided under the `sub_queries` key of the metric definition.
|
|
64
|
+
def sub_facts_for(cube_name)
|
|
65
|
+
metrics_for(cube_name, all: true).flat_map {|metric| metric.sub_queries if metric.respond_to?(:sub_queries) }.compact
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Recursively add the metric and its dependencies
|
|
69
|
+
# @param metric_id [String] fully qualified metric ID (with cube name)
|
|
70
|
+
# @option explicit [Boolean] indicates whether the metric was asked to be included as part of a query.
|
|
71
|
+
# send false for metrics that were added due to dependency.
|
|
72
|
+
def add_metric(metric_id, explicit: true)
|
|
73
|
+
metric = cube.find_metric_id(metric_id)
|
|
74
|
+
add_count_distinct_fact_grain_dependency(metric, explicit)
|
|
75
|
+
return unless register_metric(metric, explicit)
|
|
76
|
+
|
|
77
|
+
add_fact_grain_dependency(metric)
|
|
78
|
+
add_dependent_metrics(metric)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def data_dup
|
|
82
|
+
dup.instance_eval do
|
|
83
|
+
@metrics_by_cube = @metrics_by_cube.deep_dup
|
|
84
|
+
@inferred_fact_grain_by_cube = @inferred_fact_grain_by_cube.deep_dup
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# @param metric [BaseMetric]
|
|
92
|
+
# @param explicit [Boolean] see #add_metric
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
# true if the metric was added for the first time.
|
|
95
|
+
# false if the metric was already added before
|
|
96
|
+
#
|
|
97
|
+
# @note this method makes sure to set the explicit flag to true if explicit param is true. The flag will not be
|
|
98
|
+
# changed if explicit param is false.
|
|
99
|
+
#
|
|
100
|
+
# This makes sure that if the user specifies select('a', 'b') and 'a' depends on 'b', then 'b' will be marked
|
|
101
|
+
# as explicit, despite the fact it was added as explicit=false when 'a' was added.
|
|
102
|
+
#
|
|
103
|
+
def register_metric(metric, explicit)
|
|
104
|
+
@metrics_by_cube[metric.cube_name] ||= {}
|
|
105
|
+
if @metrics_by_cube[metric.cube_name].has_key?(metric.id)
|
|
106
|
+
@metrics_by_cube[metric.cube_name][metric.id][:explicit] = true if explicit
|
|
107
|
+
return false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@metrics_by_cube[metric.cube_name][metric.id] = { metric: metric, explicit: explicit }
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The level which count-distinct metric A depends on is added only if another metric depends on metric A,
|
|
115
|
+
# and only if the user did not specify a custom fact_grain for metric A.
|
|
116
|
+
def add_count_distinct_fact_grain_dependency(metric, explicit)
|
|
117
|
+
return unless !explicit and metric.is_a?(Schema::CountDistinctMetric) and metric.fact_grain.blank?
|
|
118
|
+
store_inferred_fact_grain(metric.cube_name, metric.level_id)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def add_fact_grain_dependency(metric)
|
|
122
|
+
return unless metric.respond_to?(:fact_grain) and metric.fact_grain.present?
|
|
123
|
+
metric.fact_grain.each do |level_id|
|
|
124
|
+
store_inferred_fact_grain(metric.cube_name, level_id)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @param metric [BaseMetric]
|
|
129
|
+
def add_dependent_metrics(metric)
|
|
130
|
+
return unless metric.respond_to?(:depends_on) and metric.depends_on.present?
|
|
131
|
+
metric.depends_on.each do |dependent_metric_id|
|
|
132
|
+
add_metric(dependent_metric_id, explicit: false)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @see metrics_for
|
|
137
|
+
def relevant_entries_for(cube_name, all:)
|
|
138
|
+
candidates = @metrics_by_cube[cube_name] || []
|
|
139
|
+
candidates.select!{ |_metric_id, metric_entry| metric_entry[:explicit] } unless all
|
|
140
|
+
candidates
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def store_inferred_fact_grain(cube_name, level_id)
|
|
144
|
+
@inferred_fact_grain_by_cube[cube_name] ||= {}
|
|
145
|
+
@inferred_fact_grain_by_cube[cube_name][level_id] = true
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
module Martyr
|
|
2
|
+
module Runtime
|
|
3
|
+
class QueryContext
|
|
4
|
+
include Martyr::LevelComparator
|
|
5
|
+
include Martyr::Translations
|
|
6
|
+
|
|
7
|
+
# @attribute metrics [Array<BaseMetric>] metrics that were requested as part of the query, without dependencies
|
|
8
|
+
# @attribute sub_cubes_hash [Hash] of the format { cube_name => Runtime::SubCube }
|
|
9
|
+
# @attribute dimension_scopes [Runtime::DimensionScopeCollection] see BaseCube::build_dimension_scopes
|
|
10
|
+
# @attribute level_ids_in_grain [Array<String>] array of level IDs
|
|
11
|
+
# @attribute virtual_cube [VirtualCube]
|
|
12
|
+
# @attribute virtual_cube_metric_ids [Array<String>]
|
|
13
|
+
|
|
14
|
+
attr_accessor :metrics, :sub_cubes_hash, :dimension_scopes, :level_ids_in_grain, :virtual_cube, :virtual_cube_metric_ids
|
|
15
|
+
attr_reader :data_slice
|
|
16
|
+
delegate :level_scope, :level_scopes, :with_level_scope, :lowest_level_of, :lowest_level_ids_of,
|
|
17
|
+
:levels_and_above_for, :level_ids_and_above_for, :level_loaded?, to: :dimension_scopes
|
|
18
|
+
delegate :slice, to: :memory_slice
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@data_slice = DataSlice.new(self)
|
|
22
|
+
@sub_cubes_hash = {}
|
|
23
|
+
@virtual_cube_metric_ids = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def inspect
|
|
27
|
+
"#<QueryContext metric_ids: #{metric_ids}, grain: #{level_ids_in_grain}, memory_slice: #{memory_slice.to_hash}, data_slice: #{data_slice.to_hash}, sub_cubes: #{sub_cubes}>"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sub_cubes
|
|
31
|
+
sub_cubes_hash.values
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [Array<BaseMetric>] if the current cube is virtual, returns array of metrics of the virtual cube
|
|
35
|
+
def virtual_metrics
|
|
36
|
+
return [] unless self.virtual_cube
|
|
37
|
+
virtual_cube_metric_ids.map do |unique_metric_id|
|
|
38
|
+
metric_id = second_element_from_id(unique_metric_id)
|
|
39
|
+
virtual_cube.metric_definitions.find_or_error(metric_id)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Array<String>] only metric IDs that were requested as part of the query, without dependencies
|
|
44
|
+
def metric_ids
|
|
45
|
+
metrics.map(&:id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Array<BaseMetric>] all metrics, including those that are added by dependencies and virtuals
|
|
49
|
+
def all_metrics
|
|
50
|
+
sub_cubes.flat_map { |sub_cube| sub_cube.metric_objects } + virtual_metrics
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def all_metric_ids
|
|
54
|
+
all_metrics.map(&:id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param id [String] has to be fully qualified (cube_name.metric_name)
|
|
58
|
+
def metric(id)
|
|
59
|
+
metric_ids_lookup[id]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param id [String] has to be fully qualified (cube_name.metric_name)
|
|
63
|
+
def metric?(id)
|
|
64
|
+
!!metric(id)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# = Grain
|
|
68
|
+
|
|
69
|
+
def supported_level_ids
|
|
70
|
+
@_supported_level_ids ||= levels_and_above_for(level_ids_in_grain).map(&:id)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def validate_slice_on!(slice_on)
|
|
74
|
+
slice_on_object = definition_from_id(slice_on)
|
|
75
|
+
raise Query::Error.new("Cannot find `#{slice_on}`") unless slice_on_object
|
|
76
|
+
raise Query::Error.new("Cannot slice on `#{slice_on}`: it is not in the grain") if slice_on_object.is_a?(Martyr::Level) and !supported_level_ids.include?(slice_on)
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# = Memory slices
|
|
81
|
+
|
|
82
|
+
def memory_slice
|
|
83
|
+
@memory_slice ||= MemorySlice.new(data_slice)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [QueryContext] for chaining
|
|
87
|
+
def slice(*args)
|
|
88
|
+
dup_internals.slice!(*args)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [QueryContext] for chaining
|
|
92
|
+
def slice!(*args)
|
|
93
|
+
memory_slice.slice(*args)
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [Array<String>] array of level IDs that are in the memory slice
|
|
98
|
+
def sliced_level_ids
|
|
99
|
+
memory_slice.keys.reject{|id| metric?(id)}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Array<String>] array of level IDs that are in the grain but not sliced
|
|
103
|
+
def unsliced_level_ids_in_grain
|
|
104
|
+
level_ids_in_grain - sliced_level_ids
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# = Run
|
|
108
|
+
|
|
109
|
+
# @option cube_name [String, Symbol] default is first cube
|
|
110
|
+
# @return [Array<Fact>] of the chosen cube
|
|
111
|
+
def facts(cube_name = nil)
|
|
112
|
+
cube_name ||= default_cube.cube_name
|
|
113
|
+
sub_cubes_hash[cube_name.to_s].facts
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A cube that has no grain and no metric doesn't matter - it will end up having one "useless" element with
|
|
117
|
+
# no levels in the grain.
|
|
118
|
+
# TODO: ignore cubes that do not share a grain or slice. Here is an algorithm:
|
|
119
|
+
# Start with a set of all metrics that are needed to be fetched.
|
|
120
|
+
# Add all cubes with metric-slices on them.
|
|
121
|
+
# If the shared grain is missing a level in the grain - add all cubes that support that level.
|
|
122
|
+
# If the shared grain is missing a level in ths slice - add all cubes that support that level.
|
|
123
|
+
#
|
|
124
|
+
# @option sort [Array, Hash] either
|
|
125
|
+
def elements(**options)
|
|
126
|
+
load_bottom_level_primary_keys
|
|
127
|
+
builder = VirtualElementsBuilder.new(memory_slice, unsliced_level_ids_in_grain: unsliced_level_ids_in_grain,
|
|
128
|
+
virtual_metrics: virtual_metrics)
|
|
129
|
+
|
|
130
|
+
sort_args = options.delete(:sort) || {}
|
|
131
|
+
sorter = Sorter.new(standardizer.standardize(sort_args)) { |sort_argument| definition_from_id(sort_argument) }
|
|
132
|
+
|
|
133
|
+
sub_cubes.each do |sub_cube|
|
|
134
|
+
next unless sub_cube.metric_objects.present? or sub_cube.lowest_level_ids_in_grain.present?
|
|
135
|
+
memory_slice_for_cube = memory_slice.for_cube(sub_cube)
|
|
136
|
+
builder.add sub_cube.elements(memory_slice_for_cube, **options), sliced: memory_slice_for_cube.to_hash.present?
|
|
137
|
+
end
|
|
138
|
+
sorter.sort(builder.build)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def total(metrics: nil)
|
|
142
|
+
elements(levels: [], metrics: metrics).first || empty_element(metrics: metrics)
|
|
143
|
+
end
|
|
144
|
+
alias_method :totals, :total
|
|
145
|
+
|
|
146
|
+
def empty_element(metrics: nil)
|
|
147
|
+
return sub_cubes.first.element_locator_for(memory_slice, metrics: metrics).empty_element unless virtual_cube?
|
|
148
|
+
|
|
149
|
+
locators = sub_cubes.map {|sub_cube| sub_cube.element_locator_for(memory_slice, metrics: metrics) }
|
|
150
|
+
VirtualElement.new({}, memory_slice, locators, []).rollup(*virtual_metrics)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def pivot
|
|
154
|
+
Runtime::PivotTableBuilder.new(self)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# = Dispatcher
|
|
158
|
+
|
|
159
|
+
# @return [BaseMetric, DimensionReference, BaseLevelDefinition]
|
|
160
|
+
def definition_from_id(id)
|
|
161
|
+
with_standard_id(id) do |x, y|
|
|
162
|
+
return dimension_scopes[x].try(:dimension_definition) || default_cube.metrics[x] if !y
|
|
163
|
+
return sub_cubes_hash[x].find_metric(y) if sub_cubes_hash[x]
|
|
164
|
+
dimension_scopes.find_level(id).try(:level_definition)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# = As Dimension Bus Role
|
|
169
|
+
|
|
170
|
+
def level_ids_and_above
|
|
171
|
+
level_ids_and_above_for(level_ids_in_grain)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# @param level_id [String] e.g. 'customers.last_name'
|
|
175
|
+
# @param fact_record [Fact]
|
|
176
|
+
def fetch_unsupported_level_value(level_id, fact_record)
|
|
177
|
+
sought_level_definition = dimension_scopes.find_level(level_id).level_definition
|
|
178
|
+
common_denominator_association = fact_record.sub_cube.common_denominator_level_association(level_id, prefer_query: true)
|
|
179
|
+
common_denominator_level_scope = level_scope(common_denominator_association.id)
|
|
180
|
+
common_denominator_level_scope.recursive_lookup_up fact_record.fact_key_for(common_denominator_association.id), level: sought_level_definition
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @param level_id [String] e.g. 'customers.last_name'
|
|
184
|
+
# @param fact_key_value [Integer] the primary key stored in the fact
|
|
185
|
+
def fetch_supported_query_level_record(level_id, fact_key_value)
|
|
186
|
+
level_scope = level_scope(level_id)
|
|
187
|
+
raise Internal::Error.new('level must be query') unless level_scope.query?
|
|
188
|
+
level_scope.recursive_lookup_up fact_key_value, level: level_scope
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def standardizer
|
|
192
|
+
@standardizer ||= Martyr::MetricIdStandardizer.new(default_cube.cube_name, raise_if_not_ok: virtual_cube?)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def dup_internals
|
|
196
|
+
dup.instance_eval do
|
|
197
|
+
@memory_slice = memory_slice.dup_internals
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def element_helper_module
|
|
203
|
+
return @element_helper_module if @element_helper_module
|
|
204
|
+
@element_helper_module = Module.new
|
|
205
|
+
dimension_scopes.register_element_helper_methods(@element_helper_module)
|
|
206
|
+
|
|
207
|
+
all_metric_ids.each do |metric_id|
|
|
208
|
+
metric_name = second_element_from_id(metric_id)
|
|
209
|
+
@element_helper_module.module_eval do
|
|
210
|
+
define_method(metric_name) { fetch(metric_id) }
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
@element_helper_module
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def metric_ids_lookup
|
|
219
|
+
@metric_ids_lookup ||= all_metrics.index_by(&:id)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def virtual_cube?
|
|
223
|
+
sub_cubes.length > 1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def default_cube
|
|
227
|
+
sub_cubes.first
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def load_bottom_level_primary_keys
|
|
231
|
+
return if @bottom_level_primary_keys_loaded
|
|
232
|
+
sub_cubes.each do |sub_cube|
|
|
233
|
+
sub_cube.lowest_level_ids_in_grain.each do |level_id|
|
|
234
|
+
level = level_scope(level_id)
|
|
235
|
+
next unless level.query?
|
|
236
|
+
level.primary_keys_for_load ||= []
|
|
237
|
+
level.primary_keys_for_load += sub_cube.facts.map{|x| x.raw[level.fact_alias]}
|
|
238
|
+
level.primary_keys_for_load = level.primary_keys_for_load.uniq
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
@bottom_level_primary_keys_loaded = true
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|