active_reporting 0.4.0 → 0.6.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.
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