active_reporting 0.6.1 → 1.0.0

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.
@@ -4,14 +4,32 @@ require 'forwardable'
4
4
  module ActiveReporting
5
5
  class Report
6
6
  AGGREGATE_FUNCTION_OPERATORS = {
7
- eq: '=',
8
- gt: '>',
9
- gte: '>=',
10
- lt: '<',
11
- lte: '<='
7
+ eq: '=',
8
+ gt: '>',
9
+ gte: '>=',
10
+ lt: '<',
11
+ lte: '<='
12
12
  }.freeze
13
13
 
14
+ DIMENSION_FILTER_ON_VALUES = [true, 'true'].freeze
15
+ DIMENSION_FILTER_OFF_VALUES = [false, 'false'].freeze
16
+
14
17
  extend Forwardable
18
+
19
+ # Converts a metric filter's operators to symbols, raising if any operator is not supported
20
+ #
21
+ # @param metric_filter [Hash] operator (e.g. `:gt` or `'gt'`) to value
22
+ # @return [Hash]
23
+ def self.normalize_metric_filter(metric_filter)
24
+ metric_filter.to_h.transform_keys do |operator|
25
+ next operator.to_sym if AGGREGATE_FUNCTION_OPERATORS.key?(operator.to_s.to_sym)
26
+
27
+ raise UnknownMetricFilterOperator,
28
+ "Unknown metric filter operator '#{operator}'. Valid operators are: " \
29
+ "#{AGGREGATE_FUNCTION_OPERATORS.keys.join(', ')}"
30
+ end
31
+ end
32
+
15
33
  def_delegators :@metric, :fact_model, :model
16
34
 
17
35
  def initialize(metric, dimension_identifiers: true, dimension_filter: {}, dimensions: [], metric_filter: {})
@@ -21,9 +39,10 @@ module ActiveReporting
21
39
  @dimension_identifiers = dimension_identifiers
22
40
  local_dimensions = ReportingDimension.build_from_dimensions(fact_model, Array(dimensions))
23
41
  @dimensions = (@metric.dimensions + local_dimensions).uniq
24
- @metric_filter = @metric.metric_filter.merge(metric_filter)
25
- @ordering = @metric.order_by_dimension
42
+ # The report's metric filter replaces the metric's, while the metric's dimension filters can't be overridden
43
+ @metric_filter = @metric.metric_filter.merge(Report.normalize_metric_filter(metric_filter))
26
44
  partition_dimension_filters dimension_filter
45
+ @order_by = build_order_by
27
46
  end
28
47
 
29
48
  # Builds and executes a query, returning the raw result
@@ -36,13 +55,14 @@ module ActiveReporting
36
55
  private ######################################################################
37
56
 
38
57
  def build_data
39
- @data = model.connection.exec_query(statement.to_sql).to_a
58
+ @data = model.connection.select_all(statement, 'ActiveReporting').to_a
40
59
  apply_dimension_callbacks
41
60
  @data
42
61
  end
43
62
 
44
63
  def partition_dimension_filters(user_dimension_filter)
45
64
  @dimension_filters = { ransack: {}, scope: {}, lambda: {} }
65
+ # The metric's dimension filters are part of its definition, so they take precedence over the report's
46
66
  user_dimension_filter.merge(@metric.dimension_filter).each do |key, value|
47
67
  dm = fact_model.find_dimension_filter(key.to_sym)
48
68
  @dimension_filters[dm.type][dm] = value
@@ -56,7 +76,7 @@ module ActiveReporting
56
76
  left_outer_joins: dimension_joins(ReportingDimension::JOIN_METHODS[:left_outer_joins]),
57
77
  group: group_by_statement,
58
78
  having: having_statement,
59
- order: order_by_statement
79
+ order: @order_by
60
80
  }
61
81
 
62
82
  statement = ([model] + parts.keys).inject do |chain, method|
@@ -65,13 +85,11 @@ module ActiveReporting
65
85
 
66
86
  statement = process_scope_dimension_filter(statement)
67
87
  statement = process_lambda_dimension_filter(statement)
68
- statement = process_ransack_dimension_filter(statement)
69
-
70
- statement
88
+ process_ransack_dimension_filter(statement)
71
89
  end
72
90
 
73
91
  def select_statement
74
- ss = ["#{select_aggregate} AS #{@metric.name}"]
92
+ ss = ["#{select_aggregate} AS #{model.connection.quote_column_name(@metric.name)}"]
75
93
  ss += @dimensions.map { |d| d.select_statement(with_identifier: @dimension_identifiers) }
76
94
  ss.flatten
77
95
  end
@@ -81,36 +99,52 @@ module ActiveReporting
81
99
  when :count
82
100
  'COUNT(*)'
83
101
  else
84
- "#{@metric.aggregate.to_s.upcase}(#{fact_model.measure})"
102
+ "#{@metric.aggregate.to_s.upcase}(#{measure_fragment})"
85
103
  end
86
104
  end
87
105
 
106
+ # The measure as a column qualified by the fact model's table, so it is never ambiguous with a column on
107
+ # a joined dimension. A measure given as `Arel.sql(...)` is used as is, allowing SQL expressions.
108
+ def measure_fragment
109
+ measure = @metric.measure || fact_model.measure
110
+ return measure if measure.is_a?(Arel::Nodes::SqlLiteral)
111
+
112
+ "#{model.quoted_table_name}.#{model.connection.quote_column_name(measure)}"
113
+ end
114
+
88
115
  def dimension_joins(join_method)
89
- @dimensions.select { |d| d.type == Dimension::TYPES[:standard] && d.join_method == join_method }.
90
- map { |d| d.name.to_sym }
116
+ @dimensions.select { |d| d.type == Dimension::TYPES[:standard] && d.join_method == join_method }
117
+ .map { |d| d.name.to_sym }
91
118
  end
92
119
 
93
120
  def group_by_statement
94
121
  @dimensions.map { |d| d.group_by_statement(with_identifier: @dimension_identifiers) }
95
122
  end
96
123
 
124
+ # Rails defines scopes methods taking any arguments, so it can't be known if a scope takes an argument.
97
125
  def process_scope_dimension_filter(chain)
98
- @dimension_filters[:scope].each do |dm, args|
99
- chain = if [true, 'true'].include?(args)
100
- chain.public_send(dm.name)
126
+ @dimension_filters[:scope].each do |df, value|
127
+ next if DIMENSION_FILTER_OFF_VALUES.include?(value)
128
+
129
+ chain = if DIMENSION_FILTER_ON_VALUES.include?(value)
130
+ chain.public_send(df.name)
101
131
  else
102
- chain.public_send(dm.name, args)
132
+ chain.public_send(df.name, value)
103
133
  end
104
134
  end
105
135
  chain
106
136
  end
107
137
 
138
+ # A lambda without parameters is a toggle: it is applied unless its value is `false`. A lambda with
139
+ # parameters is always passed the value, so it can take `true` and `false` as well.
108
140
  def process_lambda_dimension_filter(chain)
109
- @dimension_filters[:lambda].each do |df, args|
110
- chain = if [true, 'true'].include?(args)
111
- chain.scoping { model.instance_exec(&df.body) }
141
+ @dimension_filters[:lambda].each do |df, value|
142
+ chain = if df.body.arity.nonzero?
143
+ chain.scoping { model.instance_exec(value, &df.body) }
144
+ elsif DIMENSION_FILTER_OFF_VALUES.include?(value)
145
+ chain
112
146
  else
113
- chain.scoping { model.instance_exec(args, &df.body) }
147
+ chain.scoping { model.instance_exec(&df.body) }
114
148
  end
115
149
  end
116
150
  chain
@@ -121,8 +155,12 @@ module ActiveReporting
121
155
  @dimension_filters[:ransack].each do |dm, value|
122
156
  ransack_hash[dm.name] = value
123
157
  end
124
- chain = chain.ransack(ransack_hash).result if ransack_hash.present?
125
- chain
158
+ return chain if ransack_hash.empty?
159
+ unless Configuration.ransack_available
160
+ raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.'
161
+ end
162
+
163
+ chain.ransack(ransack_hash).result
126
164
  end
127
165
 
128
166
  def having_statement
@@ -131,12 +169,16 @@ module ActiveReporting
131
169
  end.join(' AND ')
132
170
  end
133
171
 
134
- def order_by_statement
135
- [].tap do |o|
136
- @ordering.each do |dimension_key, direction|
137
- dim = @dimensions.detect { |d| d.name.to_sym == dimension_key.to_sym }
138
- o << dim.order_by_statement(direction: direction) if dim
172
+ def build_order_by
173
+ @metric.order_by_dimension.map do |dimension_key, direction|
174
+ dim = @dimensions.detect { |d| d.name.to_sym == dimension_key.to_sym }
175
+ if dim.nil?
176
+ raise UnknownDimension,
177
+ "Cannot order by '#{dimension_key}'; it is not one of the report's dimensions " \
178
+ "(#{@dimensions.map(&:name).join(', ')})"
139
179
  end
180
+
181
+ dim.order_by_statement(direction: direction)
140
182
  end
141
183
  end
142
184
 
@@ -144,7 +186,8 @@ module ActiveReporting
144
186
  @dimensions.each do |dimension|
145
187
  callback = dimension.label_callback
146
188
  next unless callback
147
- key = "#{dimension.name}_#{dimension.label}"
189
+
190
+ key = dimension.label_name.to_s
148
191
  @data.each do |hash|
149
192
  hash[key] = callback.call(hash[key])
150
193
  end
@@ -4,13 +4,17 @@ require 'forwardable'
4
4
  module ActiveReporting
5
5
  class ReportingDimension
6
6
  extend Forwardable
7
- SUPPORTED_DBS = %w[PostgreSQL PostGIS Mysql2].freeze
7
+
8
8
  # Values for the Postgres `date_trunc` method.
9
- # See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
9
+ # See https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
10
10
  DATETIME_HIERARCHIES = %i[microseconds milliseconds second minute hour day week month quarter year decade
11
11
  century millennium date].freeze
12
+ # Column types a datetime drill can be used on. PostgreSQL reports `timestamp with time zone` columns as
13
+ # `timestamptz`, and `timestamp without time zone` columns as `timestamp` when its `datetime_type` is
14
+ # `:timestamptz`.
15
+ DATETIME_DRILL_COLUMN_TYPES = %i[datetime timestamp timestamptz date].freeze
12
16
  JOIN_METHODS = { joins: :joins, left_outer_joins: :left_outer_joins }.freeze
13
- attr_reader :join_method, :label
17
+ attr_reader :join_method, :label, :label_name
14
18
 
15
19
  def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?
16
20
 
@@ -19,15 +23,17 @@ module ActiveReporting
19
23
  dimension_name, options = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
20
24
  found_dimension = fact_model.dimensions[dimension_name.to_sym]
21
25
 
22
- raise(UnknownDimension, "Dimension '#{dimension_name}' not found on fact model '#{fact_model}'") if found_dimension.nil?
26
+ if found_dimension.nil?
27
+ raise(UnknownDimension,
28
+ "Dimension '#{dimension_name}' not found on fact model '#{fact_model}'")
29
+ end
23
30
 
24
- # Ambiguous behavior with string option for degenerate and standard dimension
25
- if !options.is_a?(Hash) && found_dimension.type == Dimension::TYPES[:degenerate]
26
- ActiveSupport::Deprecation.warn <<~EOS
27
- direct use of implict hierarchies is deprecated and will be removed in future versions. \
28
- Please use `:datetime_drill` option instead.
29
- EOS
30
- options = { datetime_drill: options }
31
+ # The `{ created_at: :month }` shorthand for datetime drills was removed in 0.7.0
32
+ if options && !options.is_a?(Hash) && found_dimension.type == Dimension::TYPES[:degenerate]
33
+ raise InvalidDimensionLabel,
34
+ "'#{dimension_name}' is not hierarchical. For a datetime drill, use " \
35
+ "`{ #{dimension_name}: { datetime_drill: :#{options} } }` " \
36
+ "instead of `{ #{dimension_name}: :#{options} }`"
31
37
  end
32
38
  new(found_dimension, **label_config(options))
33
39
  end
@@ -39,9 +45,7 @@ module ActiveReporting
39
45
  #
40
46
  # @param [Symbol|Hash] options
41
47
  def self.label_config(options)
42
- unless options.is_a?(Hash)
43
- return { label: options }
44
- end
48
+ return { label: options } unless options.is_a?(Hash)
45
49
 
46
50
  {
47
51
  label: options[:field],
@@ -75,7 +79,7 @@ module ActiveReporting
75
79
  # @return [Array]
76
80
  def select_statement(with_identifier: true)
77
81
  ss = ["#{label_fragment} AS #{label_fragment_alias}"]
78
- ss << "#{identifier_fragment} AS #{identifier_fragment_alias}" if with_identifier && type == Dimension::TYPES[:standard]
82
+ ss << "#{identifier_fragment} AS #{identifier_fragment_alias}" if with_identifier && identifiable?
79
83
  ss
80
84
  end
81
85
 
@@ -84,7 +88,7 @@ module ActiveReporting
84
88
  # @return [Array]
85
89
  def group_by_statement(with_identifier: true)
86
90
  group = [label_fragment]
87
- group << identifier_fragment if with_identifier && type == Dimension::TYPES[:standard]
91
+ group << identifier_fragment if with_identifier && identifiable?
88
92
  group
89
93
  end
90
94
 
@@ -93,36 +97,52 @@ module ActiveReporting
93
97
  # @return [String]
94
98
  def order_by_statement(direction:)
95
99
  direction = direction.to_s.upcase
96
- raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
100
+ unless %w[ASC DESC].include?(direction)
101
+ raise InvalidOrderDirection,
102
+ "Ordering direction for #{name} should be 'asc' or 'desc', got '#{direction.downcase}'"
103
+ end
104
+
97
105
  "#{label_fragment} #{direction}"
98
106
  end
99
107
 
108
+ # Tells if the dimension's identifier is included in queries when the report asks for identifiers.
109
+ #
110
+ # Only a standard dimension labeled by its default label has one row per record. Grouping a dimension
111
+ # drilled to a hierarchy level or datetime by its identifier as well would split each roll-up back into one
112
+ # row per record.
113
+ #
114
+ # @return [Boolean]
115
+ def identifiable?
116
+ type == Dimension::TYPES[:standard] && @datetime_drill.nil? && @label == dimension_fact_model.dimension_label
117
+ end
118
+
100
119
  # Looks up the dimension label callback for the label
101
120
  #
102
121
  # @return [Lambda, NilClass]
103
122
  def label_callback
104
- klass.fact_model.dimension_label_callbacks[@label]
123
+ klass.fact_model.dimension_label_callbacks[@label.to_sym]
105
124
  end
106
125
 
107
126
  private ####################################################################
108
127
 
109
128
  def determine_label_field(label_field)
110
- @label = if label_field.present? && validate_hierarchical_label(label_field)
111
- type == Dimension::TYPES[:degenerate] ? name : label_field.to_sym
112
- elsif type == Dimension::TYPES[:degenerate]
129
+ validate_hierarchical_label(label_field) if label_field.present?
130
+
131
+ @label = if type == Dimension::TYPES[:degenerate]
113
132
  name
133
+ elsif label_field.present?
134
+ label_field.to_sym
114
135
  else
115
136
  dimension_fact_model.dimension_label || Configuration.default_dimension_label
116
137
  end
117
138
  end
118
139
 
119
140
  def determine_label_name(label_name)
120
-
121
141
  if label_name
122
142
  @label_name = label_name
123
143
  else
124
144
  @label_name = name
125
- @label_name += "_#{@label}" if (type == Dimension::TYPES[:standard] && @label != :name)
145
+ @label_name += "_#{@label}" if type == Dimension::TYPES[:standard] && @label != :name
126
146
  @label_name += "_#{@datetime_drill}" if @datetime_drill
127
147
  end
128
148
  @label_name
@@ -130,6 +150,7 @@ module ActiveReporting
130
150
 
131
151
  def determine_datetime_drill(datetime_drill)
132
152
  return unless datetime_drill
153
+
133
154
  validate_supported_database_for_datetime_hierarchies
134
155
  validate_against_datetime_hierarchies(datetime_drill)
135
156
  validate_label_is_datetime
@@ -147,18 +168,19 @@ module ActiveReporting
147
168
  end
148
169
 
149
170
  def validate_hierarchical_label(hierarchical_label)
150
- validate_dimension_is_hierachical(hierarchical_label)
171
+ validate_dimension_is_hierarchical(hierarchical_label)
151
172
  validate_against_fact_model_properties(hierarchical_label)
152
- true
153
173
  end
154
174
 
155
- def validate_dimension_is_hierachical(hierarchical_label)
175
+ def validate_dimension_is_hierarchical(hierarchical_label)
156
176
  return if hierarchical?
177
+
157
178
  raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
158
179
  end
159
180
 
160
181
  def validate_supported_database_for_datetime_hierarchies
161
- return if SUPPORTED_DBS.include?(model.connection.adapter_name)
182
+ return if datetime_drill_adapter
183
+
162
184
  raise InvalidDimensionLabel,
163
185
  "Cannot utilize datetime grouping for #{name}; " \
164
186
  "database #{model.connection.adapter_name} is not supported"
@@ -166,67 +188,26 @@ module ActiveReporting
166
188
 
167
189
  def validate_against_datetime_hierarchies(hierarchical_label)
168
190
  return if DATETIME_HIERARCHIES.include?(hierarchical_label.to_sym)
191
+
169
192
  raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
170
193
  end
171
194
 
172
195
  def validate_label_is_datetime
173
- return if dimension_fact_model.model.column_for_attribute(@label).type == :datetime
174
- raise InvalidDimensionLabel, "'#{@label}' is not a datetime column"
196
+ return if DATETIME_DRILL_COLUMN_TYPES.include?(dimension_fact_model.model.column_for_attribute(@label).type)
197
+
198
+ raise InvalidDimensionLabel, "'#{@label}' is not a datetime or date column"
175
199
  end
176
200
 
177
201
  def validate_against_fact_model_properties(hierarchical_label)
178
202
  return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
179
- raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
180
- end
181
203
 
182
- def datetime_drill_label_fragment(column)
183
- if model.connection.adapter_name == "Mysql2"
184
- datetime_drill_mysql(column)
185
- else # Postgress
186
- datetime_drill_postgress(column)
187
- end
204
+ raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
188
205
  end
189
206
 
190
- def datetime_drill_postgress(column)
191
- case @datetime_drill.to_sym
192
- when :date
193
- "DATE('#{column}')"
194
- else
195
- "DATE_TRUNC('#{@datetime_drill}', #{column})"
196
- end
197
- end
207
+ def datetime_drill_adapter
208
+ return @datetime_drill_adapter if defined?(@datetime_drill_adapter)
198
209
 
199
- def datetime_drill_mysql(column)
200
- case @datetime_drill.to_sym
201
- when :microseconds
202
- "MICROSECOND(#{column})"
203
- when :milliseconds
204
- "MICROSECOND(#{column}) DIV 1000"
205
- when :second
206
- "SECOND(#{column})"
207
- when :minute
208
- "MINUTE(#{column})"
209
- when :hour
210
- "HOUR(#{column})"
211
- when :day
212
- "DAY(#{column})"
213
- when :week
214
- "WEEKDAY(#{column})"
215
- when :month
216
- "MONTH(#{column})"
217
- when :quarter
218
- "QUARTER(#{column})"
219
- when :year
220
- "YEAR(#{column})"
221
- when :decade
222
- "YEAR(#{column}) DIV 10"
223
- when :century
224
- "YEAR(#{column}) DIV 100"
225
- when :millennium
226
- "YEAR(#{column}) DIV 1000"
227
- when :date
228
- "DATE(#{column})"
229
- end
210
+ @datetime_drill_adapter = DatetimeDrill.adapter_for(model.connection)
230
211
  end
231
212
 
232
213
  def identifier_fragment
@@ -234,17 +215,17 @@ module ActiveReporting
234
215
  end
235
216
 
236
217
  def identifier_fragment_alias
237
- "#{model.connection.quote_column_name("#{name}_identifier")}"
218
+ model.connection.quote_column_name("#{name}_identifier").to_s
238
219
  end
239
220
 
240
221
  def label_fragment
241
222
  fragment = "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
242
- fragment = datetime_drill_label_fragment(fragment) if @datetime_drill
223
+ fragment = DatetimeDrill.fragment(datetime_drill_adapter, @datetime_drill, fragment) if @datetime_drill
243
224
  fragment
244
225
  end
245
226
 
246
227
  def label_fragment_alias
247
- "#{model.connection.quote_column_name(@label_name)}"
228
+ model.connection.quote_column_name(@label_name).to_s
248
229
  end
249
230
 
250
231
  def dimension_fact_model
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveReporting
4
- VERSION = '0.6.1'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -3,43 +3,63 @@
3
3
  require 'active_record'
4
4
  require 'active_reporting/active_record_adaptor'
5
5
  require 'active_reporting/configuration'
6
+ require 'active_reporting/datetime_drill'
6
7
  require 'active_reporting/dimension'
7
8
  require 'active_reporting/dimension_filter'
8
9
  require 'active_reporting/metric'
9
10
  require 'active_reporting/fact_model'
11
+ require 'active_reporting/fact_model_registry'
10
12
  require 'active_reporting/report'
11
13
  require 'active_reporting/reporting_dimension'
12
14
  require 'active_reporting/version'
15
+ require 'active_reporting/railtie' if defined?(Rails::Railtie)
13
16
 
14
- begin
15
- require 'ransack'
16
- ActiveReporting::Configuration.ransack_available = true
17
- rescue LoadError, StandardError
18
- ActiveReporting::Configuration.ransack_available = false
19
- end
20
-
21
- ActiveRecord::Base.extend(ActiveReporting::ActiveRecordAdaptor)
17
+ ActiveSupport.on_load(:active_record) { extend ActiveReporting::ActiveRecordAdaptor }
22
18
 
23
19
  module ActiveReporting
24
20
  def self.fetch_metric(name)
25
- klass = Configuration.metric_lookup_class
26
- unless defined?(klass.constantize)
21
+ klass_name = Configuration.metric_lookup_class
22
+ klass = klass_name.safe_constantize
23
+ if klass.nil?
27
24
  raise BadMetricLookupClass,
28
- "#{klass} not defined. Please define a class responsible for looking up a metric by name." \
29
- ' You may define your own class and set it with `ActiveReporting::Configuration.metric_lookup_class=`.'
25
+ "#{klass_name} not defined. Please define a class responsible for looking up a metric by name. " \
26
+ 'You may define your own class and set it with `ActiveReporting::Configuration.metric_lookup_class=`.'
30
27
  end
31
- unless klass.constantize.respond_to?(:lookup)
32
- raise BadMetricLookupClass, "#{klass} needs to define a class method called 'lookup'"
28
+ unless klass.respond_to?(:lookup)
29
+ raise BadMetricLookupClass, "#{klass_name} needs to define a class method called 'lookup'"
33
30
  end
34
- klass.constantize.lookup(name)
31
+
32
+ klass.lookup(name)
33
+ end
34
+
35
+ class BadMetricLookupClass < StandardError
36
+ end
37
+
38
+ class InvalidDimensionLabel < StandardError
39
+ end
40
+
41
+ # Subclasses RuntimeError, which was raised for invalid directions before this class existed
42
+ class InvalidOrderDirection < RuntimeError
43
+ end
44
+
45
+ class RansackNotAvailable < StandardError
35
46
  end
36
47
 
37
- BadMetricLookupClass = Class.new(StandardError)
38
- InvalidDimensionLabel = Class.new(StandardError)
39
- RansackNotAvailable = Class.new(StandardError)
40
- UnknownAggregate = Class.new(StandardError)
41
- UnknownDimension = Class.new(StandardError)
42
- UnknownDimensionFilter = Class.new(StandardError)
43
- UnknownMetric = Class.new(StandardError)
44
- UnknownJoinMethod = Class.new(StandardError)
48
+ class UnknownAggregate < StandardError
49
+ end
50
+
51
+ class UnknownDimension < StandardError
52
+ end
53
+
54
+ class UnknownDimensionFilter < StandardError
55
+ end
56
+
57
+ class UnknownMetric < StandardError
58
+ end
59
+
60
+ class UnknownMetricFilterOperator < StandardError
61
+ end
62
+
63
+ class UnknownJoinMethod < StandardError
64
+ end
45
65
  end