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