active_reporting 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 7a5444ba19be4d20842e16f8e800e45837e133bd
4
- data.tar.gz: 24b59685e4cd70770a13a8a2925c85d860e782a3
2
+ SHA256:
3
+ metadata.gz: 63bda6ce4591fc9695b29db8e53aee19966e3630e9c8b059c686678043f20938
4
+ data.tar.gz: e780a77254b8c8b1cc23ecc732146d581def733fef8669fc3a0419e32fabf5df
5
5
  SHA512:
6
- metadata.gz: 1df566a6122ec806f231a116ffe33cf729374fe529ddf18a2763cb2462946414c60bada55a06e7fc036ef3c4fc0002f021d06b1147cb011c7810ce1b517cd82e
7
- data.tar.gz: 0ec9387af8a5918f3685732b4331377302e373ae56843305991d2c8abc24928c4a19ea79ed7d3bd6b5e979204c2a15cde3806df62a282a8714ac0d6a439fb4e6
6
+ metadata.gz: 89718eab7d4398d77f9d19de7e7160e52feebfc295e03a7c05a113a87159e5e3640b599d681559c53e749bf68bdcee477effff6b4b605c4dcf317da6c09d2cdd
7
+ data.tar.gz: 4d999fe0374b28e826edbb6f6eca9fc9f8ab9ae7aff1056aa88757079e00847f8361862108ba65227f530b72fa918e849dd3bbfe1b337156a685dd4f26b706d8
data/.gitignore CHANGED
@@ -6,5 +6,7 @@
6
6
  /doc/
7
7
  /pkg/
8
8
  /spec/reports/
9
+ .byebug_history
10
+ .ruby-version
9
11
  /tmp/
10
12
  .DS_STORE
@@ -4,6 +4,9 @@ AllCops:
4
4
  - Gemfile
5
5
  TargetRubyVersion: 2.3
6
6
 
7
+ Metrics/ClassLength:
8
+ Max: 150
9
+
7
10
  Metrics/LineLength:
8
11
  Max: 120
9
12
 
@@ -1,19 +1,20 @@
1
1
  sudo: false
2
2
  language: ruby
3
+ services:
4
+ - mysql
5
+ addons:
6
+ postgresql: "9.6"
3
7
  rvm:
4
- - 2.3.7
5
- - 2.4.4
6
- - 2.5.1
8
+ - 2.5.8
9
+ - 2.6.6
10
+ - 2.7.1
7
11
  env:
12
+ - RAILS=6-0 DB=sqlite
13
+ - RAILS=6-0 DB=pg
14
+ - RAILS=6-0 DB=mysql
8
15
  - RAILS=5-2 DB=sqlite
9
16
  - RAILS=5-2 DB=pg
10
17
  - RAILS=5-2 DB=mysql
11
- - RAILS=5-1 DB=sqlite
12
- - RAILS=5-1 DB=pg
13
- - RAILS=5-1 DB=mysql
14
- - RAILS=4-2 DB=sqlite
15
- - RAILS=4-2 DB=pg
16
- - RAILS=4-2 DB=mysql
17
18
  before_script:
18
19
  - psql -c 'create database active_reporting_test;' -U postgres
19
20
  - mysql -e 'create database active_reporting_test collate utf8_general_ci;'
@@ -1,3 +1,54 @@
1
+ ## 0.6.0 (2020-08-21)
2
+
3
+ ### Features
4
+
5
+ * Support to implicit hierarchical on datetime columns in MySQL (#33) - *germanotm*
6
+ * Added `{ datetime_drill: :month }` option for reporting dimentions to explicitly - *germanotm*
7
+
8
+ This depricates the use of key-value only use for report dimension options (ie, `dimensions: [{ dim: single_option }]`).
9
+ Instead, use `dimensions: [{ dim: { option: value} }]` See the README for all reporting dimension options.
10
+
11
+ ## 0.5.1 (2020-06-31)
12
+
13
+ ### Features
14
+
15
+ * Allow dimensions defined in a `Metric` to use LEFT OUTER JOINs via a new `:join_method` option (#32) - *germanotm*
16
+
17
+ ### Misc
18
+
19
+ * Fixed warning about initialized variables
20
+ * Fixed Ruby 2.7 warning
21
+
22
+ ## 0.5.0 (2020-06-30)
23
+
24
+ ### Bug Fixes
25
+
26
+ * Fix Missing quotation marks in column names causing SQL errors on MYSQL (#30) - *germanotm*
27
+
28
+ ### Misc
29
+
30
+ * Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
31
+
32
+ ## 0.4.2 (2019-11-01)
33
+
34
+ ### Misc
35
+
36
+ * Test against Rails 6.0 final
37
+ * Fixed deprecated call to `to_hash` - *joshforbes*
38
+ * Corrected readme entry for `dimesions` option for `ActiveReporting::Metric` - *joshforbes*
39
+
40
+ ## 0.4.1 (2019-05-28)
41
+
42
+ ### Features
43
+
44
+ * Hierarchical dimensions may now have custom keys in result (#16) - *andresgutgon*
45
+
46
+ ### Misc
47
+
48
+ * Test against Raisl 6.0RC
49
+ * Loosen AR requirements. The gem will install for any AR version, but only ones listed in the README are supported
50
+ * Test against active Rubies
51
+
1
52
  ## 0.4.0 (2018-05-02)
2
53
 
3
54
  ### Breaking Changes
@@ -15,7 +66,7 @@
15
66
  * Specify rescue from LoadError for ransack (#9) - *niborg*
16
67
  * Fix ransack fallback logic (#8) - *germanotm*
17
68
 
18
- ## Misc
69
+ ### Misc
19
70
 
20
71
  * Test against Rails 5.2
21
72
  * Test against Ruby 2.5
data/Gemfile CHANGED
@@ -8,16 +8,8 @@ rails = ENV['RAILS'] || '5-2'
8
8
  db = ENV['DB'] || 'sqlite'
9
9
 
10
10
  case rails
11
- when '4-2'
12
- gem 'activerecord', '~> 4.2.0'
13
- if ENV['DB'] == 'pg'
14
- gem 'pg', '~> 0.18'
15
- end
16
- if ENV['DB'] == 'mysql'
17
- gem 'mysql2', '~> 0.3.18'
18
- end
19
- when '5-1'
20
- gem 'activerecord', '~> 5.1.0'
21
- else
11
+ when '5-2'
22
12
  gem 'activerecord', '~> 5.2.0'
13
+ when '6-0'
14
+ gem 'activerecord', '~> 6.0.0'
23
15
  end
data/README.md CHANGED
@@ -8,9 +8,9 @@ ActiveReporting implements various terminology used in Relational Online Analyti
8
8
 
9
9
  ActiveReporting officially supports MySQL, PostgreSQL, and SQLite.
10
10
 
11
- ActiveReporting officially supports Ruby 2.3, 2.4, and 2.5.
11
+ ActiveReporting officially supports Ruby 2.5 and later. Other versions may work, but are not supported.
12
12
 
13
- ActiveReporting officially supports Rails 4.2, 5.1, and 5.2.
13
+ ActiveReporting officially supports Rails 5.2, and 6.0. Other versions may work, but are not supported.
14
14
 
15
15
  ## Installation
16
16
 
@@ -49,8 +49,9 @@ Rails: ActiveRecord model
49
49
  A dimension is a point of data used to "slice and dice" data from a fact model. It's either a column that lives on the fact table or a foreign key to another table.
50
50
 
51
51
  Examples:
52
- * A sales rep on a fact table of orders
53
- * A state of an order on a state machine
52
+
53
+ * A sales rep on a fact table of sales
54
+ * A state of an sale on a state machine
54
55
  * The manufacture on a fact table of widgets
55
56
 
56
57
  SQL Equivalent: JOIN, GROUP BY
@@ -62,6 +63,7 @@ Rails: ActiveRecord relation or attribute
62
63
  A hierarchy for a dimension is related attributes that live on a dimension table used to drill down and drill up through a dimension.
63
64
 
64
65
  Examples:
66
+
65
67
  * Dates: Date, Month, Year, Quarter
66
68
  * Mobile Phone: Model, Manufacture, OS, Wireless Technology
67
69
 
@@ -70,6 +72,7 @@ Examples:
70
72
  This is information related to a dimension. When the dimension lives on the fact table, the label is the column used. When the dimension is a related table, the label is a column representing the hierarchy level.
71
73
 
72
74
  Examples:
75
+
73
76
  * When dimensioning blog posts by category, the dimension is the category_id which leads to the categories table. The label would be the category name.
74
77
 
75
78
  ### Dimension Filter (or just "filter")
@@ -85,6 +88,7 @@ Rails: `where()`, scopes, etc.
85
88
  A measure is a column in a fact table (usually a numeric value) used in aggregations such as sum, maximum, average, etc.
86
89
 
87
90
  Examples:
91
+
88
92
  * Total amount in a sale
89
93
  * Number of units used in a transaction
90
94
 
@@ -178,7 +182,7 @@ end
178
182
  ActiveReporting assumes the column of a fact model used for summing, averaging, etc. is called `value`. This may be changed on a fact model using `measure=`. You may pass in a string or symbol of the column you wish to use for aggregations.
179
183
 
180
184
  ```ruby
181
- class OrderFactModel < ActiveReporting::FactModel
185
+ class SaleFactModel < ActiveReporting::FactModel
182
186
  self.measure = :total
183
187
  end
184
188
  ```
@@ -219,11 +223,9 @@ class PhoneFactModel < ActiveReporting::FactModel
219
223
  end
220
224
  ```
221
225
 
222
- ### Implicit hierarchies with datetime columns (PostgreSQL support only)
226
+ ### Drill down / Roll up (Drill up) with datetime columns
223
227
 
224
- The fastest approach to group by certain date metrics is to create so-called "date dimensions". For
225
- those Postgres users that are restricted from organizing their data in this way, Postgres provides
226
- a way to group by `datetime` column data on the fly using the `date_trunc` function.
228
+ The fastest approach to group by certain date metrics is to create so-called "date dimensions" and add on columns for each desired hierarchy. For those users that are restricted from organizing their data in this way, ActiveRporting provides a `datetime_drill` option that can be passed with the dimension on the metric definition to drill datetime columns.
227
229
 
228
230
  To use, declare a datetime dimension on a fact model as normal:
229
231
 
@@ -233,7 +235,23 @@ class UserFactModel < ActiveReporting::FactModel
233
235
  end
234
236
  ```
235
237
 
236
- When creating a metric, ActiveReporting will recognize implicit hierarchies for this dimension. The hierarchies correspond to the [values](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC) supported by PostgreSQL. (See example under the metric section, below.)
238
+ When creating a metric, ActiveReporting will recognize the following datetime hierarchies: (See example under the metric section, below.)
239
+
240
+ - microseconds
241
+ - milliseconds
242
+ - second
243
+ - minute
244
+ - hour
245
+ - day
246
+ - week
247
+ - month
248
+ - quarter
249
+ - year
250
+ - decade
251
+ - century
252
+ - millennium
253
+
254
+ Under the hood Active Reporting uses specific database functions to manipulate datetime columns. Postgres provides a way to group by `datetime` column data on the fly using the [`date_trunc` function](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC). On Mysql this can be done using [Date and Time Functions](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html).
237
255
 
238
256
  *NOTE*: PRs welcomed to support this functionality in other databases.
239
257
 
@@ -273,8 +291,8 @@ A `Metric` is the basic building block used to describe a question you want to a
273
291
 
274
292
  ```ruby
275
293
  my_metric = ActiveReporting::Metric.new(
276
- :order_total,
277
- fact_model: OrderFactModel,
294
+ :sale_total,
295
+ fact_model: SaleFactModel,
278
296
  aggregate: :sum
279
297
  )
280
298
  ```
@@ -285,7 +303,17 @@ my_metric = ActiveReporting::Metric.new(
285
303
 
286
304
  `aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
287
305
 
288
- `dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension. You may specify a hierarchy level by using a hash. (Examples: `[:sales_rep, {order_date: :month}]`)
306
+ `dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension.
307
+
308
+ You may pass a hash instead of a symbol to customize the dimension options (example: { dimension_name: { option1: value, option2: value}}). The avaliable options are:
309
+
310
+ - `field` - Specify the hierarchy level that should be used instead the default dimension label. Ex: `[:sales_rep, {mobile_phone: { field :manufacture }}]`. If you use a hash instead of a Symbol to define a hierarchy the `field` item must be a valid field in your table.
311
+
312
+ - `name` - You may costumize the label alias, by default the dimension name will be used. The `name` can be whatever label you want. Ex :`[{sale_date: { field: :month, name: :a_custom_name_for_month }}]`.
313
+
314
+ - `join_method` - You may choose the join_method with the dimension. The default value for join_method is :joins which does a standard "INNER JOIN", but you can pass a :left_outer_joins to use "LEFT OUTER JOIN" instead. Ex: `[{sales_rep: { join_method: :left_outer_joins }}]`
315
+
316
+ - `datetime_drill` - To drill up and down over datetime column you may pass a `datetime_drill`. Ex: `[:sales_rep, { order: { field: :created_at, datetime_drill: :month }}]`. This option will perform an implicit drill over datetime columns and not a date dimension relationship.
289
317
 
290
318
  `dimension_filter` - A hash were the keys are dimension filter names and the values are the values passed into the filter.
291
319
 
@@ -303,7 +331,7 @@ end
303
331
  my_metric = ActiveReporting::Metric.new(
304
332
  :my_total,
305
333
  fact_model: UserFactModel,
306
- dimensions: [{ created_at: :quarter } ]
334
+ dimensions: [{ created_at: { datetime_drill: :quarter }} ]
307
335
  )
308
336
  ```
309
337
 
@@ -313,20 +341,20 @@ A `Report` takes an `ActiveReporting::Metric` and ties everything together. It i
313
341
 
314
342
  ```ruby
315
343
  metric = ActiveReporting::Metric.new(
316
- :order_count,
317
- fact_model: OrderFactModel,
344
+ :sale_count,
345
+ fact_model: SaleFactModel,
318
346
  dimensions: [:sales_rep],
319
347
  dimension_filter: {months_ago: 1}
320
348
  )
321
349
 
322
350
  report = ActiveReporting::Report.new(metric)
323
351
  report.run
324
- => [{order_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{order_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
352
+ => [{sale_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{sale_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
325
353
  ```
326
354
 
327
355
  A `Report` may also take additional arguments to merge with the `Metric`'s information. This can be user input for additional filters, or to expand on a base `Metric`.
328
356
 
329
- `dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of orders dimensioned by sales rep, the rep's IDs from the `sales_reps` table will be included. (Default `true`)
357
+ `dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of sales dimensioned by sales rep, the rep's IDs from the `sales_reps` table will be included. (Default `true`)
330
358
 
331
359
  `dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
332
360
 
@@ -336,15 +364,15 @@ A `Report` may also take additional arguments to merge with the `Metric`'s infor
336
364
 
337
365
  ```ruby
338
366
  metric = ActiveReporting::Metric.new(
339
- :order_count,
340
- fact_model: OrderFactModel,
367
+ :sale_count,
368
+ fact_model: SaleFactModel,
341
369
  dimensions: [:sales_rep],
342
370
  dimension_filter: {months_ago: 1}
343
371
  )
344
372
 
345
- report = ActiveReporting.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
373
+ report = ActiveReporting::Report.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
346
374
  report.run
347
- => [{order_count: 17, sales_rep: 'Mary Sue'}]
375
+ => [{sale_count: 17, sales_rep: 'Mary Sue'}]
348
376
  ```
349
377
 
350
378
  It may be more DRY to store ready-made metrics in a database table or stored in memory to use as the bases for various reports. You can pass a string or symbol into a `Report` instead of a `Metric` to look up an pre-made metric. This is done by passing the symbol or string into the `lookup` class method on the constant defined in `ActiveReporting::Configuration.metric_lookup_class`.
@@ -376,7 +404,6 @@ appropriate `DB` environment variable, e.g. `DB=pg rake test`.
376
404
 
377
405
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_reporting. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
378
406
 
379
-
380
407
  ## License
381
408
 
382
409
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -24,14 +24,14 @@ Gem::Specification.new do |spec|
24
24
 
25
25
  spec.required_ruby_version = '>= 2.3'
26
26
 
27
- spec.add_dependency 'activerecord', '>= 4.2.0'
28
- spec.add_dependency 'activesupport', '>= 4.2.0'
27
+ spec.add_dependency 'activerecord'
28
+ spec.add_dependency 'activesupport'
29
29
 
30
30
  spec.add_development_dependency 'bundler'
31
31
  spec.add_development_dependency 'minitest', '~> 5.0'
32
32
  spec.add_development_dependency 'mysql2'
33
33
  spec.add_development_dependency 'pg'
34
- spec.add_development_dependency 'rake', '~> 10.0'
34
+ spec.add_development_dependency 'rake'
35
35
  spec.add_development_dependency 'ransack'
36
36
  spec.add_development_dependency 'sqlite3'
37
37
  end
@@ -41,4 +41,5 @@ module ActiveReporting
41
41
  UnknownDimension = Class.new(StandardError)
42
42
  UnknownDimensionFilter = Class.new(StandardError)
43
43
  UnknownMetric = Class.new(StandardError)
44
+ UnknownJoinMethod = Class.new(StandardError)
44
45
  end
@@ -30,13 +30,6 @@ module ActiveReporting
30
30
  end
31
31
  end
32
32
 
33
- # Whether the dimension is a datetime column
34
- #
35
- # @return [Boolean]
36
- def datetime?
37
- @datetime ||= type == TYPES[:degenerate] && model.column_for_attribute(@name).type == :datetime
38
- end
39
-
40
33
  # Tells if the dimension is hierarchical
41
34
  #
42
35
  # @return [Boolean]
@@ -85,6 +85,7 @@ module ActiveReporting
85
85
  #
86
86
  # @return [Symbol]
87
87
  def self.dimension_label
88
+ @dimension_label ||= nil
88
89
  @dimension_label || Configuration.default_dimension_label
89
90
  end
90
91
 
@@ -155,6 +156,7 @@ module ActiveReporting
155
156
  #
156
157
  # @return [Boolean]
157
158
  def self.ransack_fallback
159
+ @ransack_fallback ||= false
158
160
  @ransack_fallback || Configuration.ransack_fallback
159
161
  end
160
162
  private_class_method :ransack_fallback
@@ -36,7 +36,7 @@ module ActiveReporting
36
36
  private ######################################################################
37
37
 
38
38
  def build_data
39
- @data = model.connection.exec_query(statement.to_sql).to_hash
39
+ @data = model.connection.exec_query(statement.to_sql).to_a
40
40
  apply_dimension_callbacks
41
41
  @data
42
42
  end
@@ -52,7 +52,8 @@ module ActiveReporting
52
52
  def statement
53
53
  parts = {
54
54
  select: select_statement,
55
- joins: dimension_joins,
55
+ joins: dimension_joins(ReportingDimension::JOIN_METHODS[:joins]),
56
+ left_outer_joins: dimension_joins(ReportingDimension::JOIN_METHODS[:left_outer_joins]),
56
57
  group: group_by_statement,
57
58
  having: having_statement,
58
59
  order: order_by_statement
@@ -84,8 +85,9 @@ module ActiveReporting
84
85
  end
85
86
  end
86
87
 
87
- def dimension_joins
88
- @dimensions.select { |d| d.type == :standard }.map { |d| d.name.to_sym }
88
+ 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 }
89
91
  end
90
92
 
91
93
  def group_by_statement
@@ -142,8 +144,9 @@ module ActiveReporting
142
144
  @dimensions.each do |dimension|
143
145
  callback = dimension.label_callback
144
146
  next unless callback
147
+ key = "#{dimension.name}_#{dimension.label}"
145
148
  @data.each do |hash|
146
- hash[dimension.name.to_s] = callback.call(hash[dimension.name.to_s])
149
+ hash[key] = callback.call(hash[key])
147
150
  end
148
151
  end
149
152
  end
@@ -4,27 +4,63 @@ require 'forwardable'
4
4
  module ActiveReporting
5
5
  class ReportingDimension
6
6
  extend Forwardable
7
- SUPPORTED_DBS = %w[PostgreSQL PostGIS].freeze
7
+ SUPPORTED_DBS = %w[PostgreSQL PostGIS Mysql2].freeze
8
8
  # Values for the Postgres `date_trunc` method.
9
9
  # See https://www.postgresql.org/docs/10/static/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].freeze
12
- def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?, :datetime?
12
+ JOIN_METHODS = { joins: :joins, left_outer_joins: :left_outer_joins }.freeze
13
+ attr_reader :join_method, :label
14
+
15
+ def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?
13
16
 
14
17
  def self.build_from_dimensions(fact_model, dimensions)
15
18
  Array(dimensions).map do |dim|
16
- dimension_name, label = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
19
+ dimension_name, options = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
17
20
  found_dimension = fact_model.dimensions[dimension_name.to_sym]
18
- raise UnknownDimension, "Dimension '#{dim}' not found on fact model '#{fact_model}'" if found_dimension.nil?
19
- new(found_dimension, label: label)
21
+
22
+ raise(UnknownDimension, "Dimension '#{dimension_name}' not found on fact model '#{fact_model}'") if found_dimension.nil?
23
+
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
+ end
32
+ new(found_dimension, **label_config(options))
33
+ end
34
+ end
35
+
36
+ # If you pass a symbol it means you just indicate
37
+ # the field on that dimension. With a hash you can
38
+ # customize the name of the label
39
+ #
40
+ # @param [Symbol|Hash] options
41
+ def self.label_config(options)
42
+ unless options.is_a?(Hash)
43
+ return { label: options }
20
44
  end
45
+
46
+ {
47
+ label: options[:field],
48
+ label_name: options[:name],
49
+ join_method: options[:join_method],
50
+ datetime_drill: options[:datetime_drill]
51
+ }
21
52
  end
22
53
 
23
54
  # @param dimension [ActiveReporting::Dimension]
24
- # @option label [Symbol] Hierarchical dimension to be used as a label
25
- def initialize(dimension, label: nil)
55
+ # @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
56
+ # @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
57
+ def initialize(dimension, label: nil, label_name: nil, join_method: nil, datetime_drill: nil)
26
58
  @dimension = dimension
27
- determine_label(label)
59
+
60
+ determine_label_field(label)
61
+ determine_datetime_drill(datetime_drill)
62
+ determine_label_name(label_name)
63
+ determine_join_method(join_method)
28
64
  end
29
65
 
30
66
  # The foreign key to use in queries
@@ -38,10 +74,8 @@ module ActiveReporting
38
74
  #
39
75
  # @return [Array]
40
76
  def select_statement(with_identifier: true)
41
- return [degenerate_select_fragment] if type == Dimension::TYPES[:degenerate]
42
-
43
- ss = ["#{label_fragment} AS #{name}"]
44
- ss << "#{identifier_fragment} AS #{name}_identifier" if with_identifier
77
+ ss = ["#{label_fragment} AS #{label_fragment_alias}"]
78
+ ss << "#{identifier_fragment} AS #{identifier_fragment_alias}" if with_identifier && type == Dimension::TYPES[:standard]
45
79
  ss
46
80
  end
47
81
 
@@ -49,10 +83,8 @@ module ActiveReporting
49
83
  #
50
84
  # @return [Array]
51
85
  def group_by_statement(with_identifier: true)
52
- return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
53
-
54
86
  group = [label_fragment]
55
- group << identifier_fragment if with_identifier
87
+ group << identifier_fragment if with_identifier && type == Dimension::TYPES[:standard]
56
88
  group
57
89
  end
58
90
 
@@ -62,7 +94,6 @@ module ActiveReporting
62
94
  def order_by_statement(direction:)
63
95
  direction = direction.to_s.upcase
64
96
  raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
65
- return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
66
97
  "#{label_fragment} #{direction}"
67
98
  end
68
99
 
@@ -75,22 +106,49 @@ module ActiveReporting
75
106
 
76
107
  private ####################################################################
77
108
 
78
- def determine_label(label)
79
- @label = if label.present? && validate_hierarchical_label(label)
80
- label.to_sym
109
+ 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]
113
+ name
81
114
  else
82
115
  dimension_fact_model.dimension_label || Configuration.default_dimension_label
83
116
  end
84
117
  end
85
118
 
86
- def validate_hierarchical_label(hierarchical_label)
87
- if datetime?
88
- validate_supported_database_for_datetime_hierarchies
89
- validate_against_datetime_hierarchies(hierarchical_label)
119
+ def determine_label_name(label_name)
120
+
121
+ if label_name
122
+ @label_name = label_name
90
123
  else
91
- validate_dimension_is_hierachical(hierarchical_label)
92
- validate_against_fact_model_properties(hierarchical_label)
124
+ @label_name = name
125
+ @label_name += "_#{@label}" if (type == Dimension::TYPES[:standard] && @label != :name)
126
+ @label_name += "_#{@datetime_drill}" if @datetime_drill
93
127
  end
128
+ @label_name
129
+ end
130
+
131
+ def determine_datetime_drill(datetime_drill)
132
+ return unless datetime_drill
133
+ validate_supported_database_for_datetime_hierarchies
134
+ validate_against_datetime_hierarchies(datetime_drill)
135
+ validate_label_is_datetime
136
+ @datetime_drill = datetime_drill
137
+ end
138
+
139
+ def determine_join_method(join_method)
140
+ if join_method.blank?
141
+ @join_method = ReportingDimension::JOIN_METHODS[:joins]
142
+ elsif ReportingDimension::JOIN_METHODS.include?(join_method)
143
+ @join_method = join_method
144
+ else
145
+ raise UnknownJoinMethod, "Method '#{join_method}' not included in '#{ReportingDimension::JOIN_METHODS.values}'"
146
+ end
147
+ end
148
+
149
+ def validate_hierarchical_label(hierarchical_label)
150
+ validate_dimension_is_hierachical(hierarchical_label)
151
+ validate_against_fact_model_properties(hierarchical_label)
94
152
  true
95
153
  end
96
154
 
@@ -111,27 +169,75 @@ module ActiveReporting
111
169
  raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
112
170
  end
113
171
 
172
+ 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"
175
+ end
176
+
114
177
  def validate_against_fact_model_properties(hierarchical_label)
115
178
  return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
116
179
  raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
117
180
  end
118
181
 
119
- def degenerate_fragment
120
- return "#{name}_#{@label}" if datetime?
121
- "#{model.quoted_table_name}.#{name}"
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
122
188
  end
123
189
 
124
- def degenerate_select_fragment
125
- return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
126
- "#{model.quoted_table_name}.#{name}"
190
+ def datetime_drill_postgress(column)
191
+ "DATE_TRUNC('#{@datetime_drill}', #{column})"
192
+ end
193
+
194
+ def datetime_drill_mysql(column)
195
+ case @datetime_drill.to_sym
196
+ when :microseconds
197
+ "MICROSECOND(#{column})"
198
+ when :milliseconds
199
+ "MICROSECOND(#{column}) DIV 1000"
200
+ when :second
201
+ "SECOND(#{column})"
202
+ when :minute
203
+ "MINUTE(#{column})"
204
+ when :hour
205
+ "HOUR(#{column})"
206
+ when :day
207
+ "DAY(#{column})"
208
+ when :week
209
+ "WEEKDAY(#{column})"
210
+ when :month
211
+ "MONTH(#{column})"
212
+ when :quarter
213
+ "QUARTER(#{column})"
214
+ when :year
215
+ "YEAR(#{column})"
216
+ when :decade
217
+ "YEAR(#{column}) DIV 10"
218
+ when :century
219
+ "YEAR(#{column}) DIV 100"
220
+ when :millennium
221
+ "YEAR(#{column}) DIV 1000"
222
+ end
127
223
  end
128
224
 
129
225
  def identifier_fragment
130
- "#{klass.quoted_table_name}.#{klass.primary_key}"
226
+ "#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
227
+ end
228
+
229
+ def identifier_fragment_alias
230
+ "#{model.connection.quote_column_name("#{name}_identifier")}"
131
231
  end
132
232
 
133
233
  def label_fragment
134
- "#{klass.quoted_table_name}.#{@label}"
234
+ fragment = "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
235
+ fragment = datetime_drill_label_fragment(fragment) if @datetime_drill
236
+ fragment
237
+ end
238
+
239
+ def label_fragment_alias
240
+ "#{model.connection.quote_column_name(@label_name)}"
135
241
  end
136
242
 
137
243
  def dimension_fact_model
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveReporting
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_reporting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Drake
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-03 00:00:00.000000000 Z
11
+ date: 2020-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.2.0
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.2.0
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.2.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.2.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -98,16 +98,16 @@ dependencies:
98
98
  name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '10.0'
103
+ version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '10.0'
110
+ version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: ransack
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -185,8 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
187
  requirements: []
188
- rubyforge_project:
189
- rubygems_version: 2.6.14
188
+ rubygems_version: 3.0.3
190
189
  signing_key:
191
190
  specification_version: 4
192
191
  summary: Add relational OLAP-like functionality for ActiveRecord