active_reporting 0.4.1 → 0.6.1

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
2
  SHA256:
3
- metadata.gz: 4470ea5ed19154c0f73c4205dfb251f327a076563eeee479a8ebf1410cfd6c81
4
- data.tar.gz: '038914792fde14a53347f7952336a68e3d2520a171dbb2a620342ba85dffc144'
3
+ metadata.gz: bd8ff0998a757ff875a17baad2fcb148ace4239da0781e1b7fbdd4c6367cef2c
4
+ data.tar.gz: 316ee68af897e1b866b79b28d3f0889f116982a48b94a123cde5170ac851b4e1
5
5
  SHA512:
6
- metadata.gz: 0dafa3b0a2d8a9731f14c51e1505bdf79b79cb52fb9b9ecabe3c8006340f946b60638090b775a830a8952f7dce1c32f75608eb488894e08d899e0cc6b5b0d5e5
7
- data.tar.gz: a4a2e65604d42d290582c50954aa66f991350bf5efbfd47887ae4534d4ed7811dc70538371cd693650124af7f1ef0843ebd4c62a84111527d35c268231a79a11
6
+ metadata.gz: 73f92eed3389912530e9ff393e761bbc6e86a84e1c6beff4428de03f295a2fe91f2b230d25d8a3d0b783761b099dca43b4bc62ea05538a469c925eae5c9044d2
7
+ data.tar.gz: b1217306b02aa8e40b78cea0d0d31dca84621c4b676e79a638bf8644891e301a3d850e2f645d9c0b745724c4a03c0837936cbe1776e9794a89e4e73580589256
@@ -1,11 +1,13 @@
1
1
  sudo: false
2
2
  language: ruby
3
+ services:
4
+ - mysql
3
5
  addons:
4
6
  postgresql: "9.6"
5
7
  rvm:
6
- - 2.4.6
7
- - 2.5.5
8
- - 2.6.3
8
+ - 2.5.8
9
+ - 2.6.6
10
+ - 2.7.1
9
11
  env:
10
12
  - RAILS=6-0 DB=sqlite
11
13
  - RAILS=6-0 DB=pg
@@ -13,20 +15,6 @@ env:
13
15
  - RAILS=5-2 DB=sqlite
14
16
  - RAILS=5-2 DB=pg
15
17
  - RAILS=5-2 DB=mysql
16
- - RAILS=5-1 DB=sqlite
17
- - RAILS=5-1 DB=pg
18
- - RAILS=5-1 DB=mysql
19
- - RAILS=4-2 DB=sqlite
20
- - RAILS=4-2 DB=pg
21
- - RAILS=4-2 DB=mysql
22
- matrix:
23
- exclude:
24
- - rvm: 2.4.6
25
- env: RAILS=6-0 DB=mysql
26
- - rvm: 2.4.6
27
- env: RAILS=6-0 DB=pg
28
- - rvm: 2.4.6
29
- env: RAILS=6-0 DB=sqlite
30
18
  before_script:
31
19
  - psql -c 'create database active_reporting_test;' -U postgres
32
20
  - mysql -e 'create database active_reporting_test collate utf8_general_ci;'
@@ -1,3 +1,48 @@
1
+ ## 0.6.1 (2020-08-29)
2
+
3
+ ### Misc
4
+
5
+ * Add `date` as an option for datetime drills - *germanotm*
6
+
7
+ ## 0.6.0 (2020-08-21)
8
+
9
+ ### Features
10
+
11
+ * Support to implicit hierarchical on datetime columns in MySQL (#33) - *germanotm*
12
+ * Added `{ datetime_drill: :month }` option for reporting dimentions to explicitly - *germanotm*
13
+
14
+ This depricates the use of key-value only use for report dimension options (ie, `dimensions: [{ dim: single_option }]`).
15
+ Instead, use `dimensions: [{ dim: { option: value} }]` See the README for all reporting dimension options.
16
+
17
+ ## 0.5.1 (2020-06-31)
18
+
19
+ ### Features
20
+
21
+ * Allow dimensions defined in a `Metric` to use LEFT OUTER JOINs via a new `:join_method` option (#32) - *germanotm*
22
+
23
+ ### Misc
24
+
25
+ * Fixed warning about initialized variables
26
+ * Fixed Ruby 2.7 warning
27
+
28
+ ## 0.5.0 (2020-06-30)
29
+
30
+ ### Bug Fixes
31
+
32
+ * Fix Missing quotation marks in column names causing SQL errors on MYSQL (#30) - *germanotm*
33
+
34
+ ### Misc
35
+
36
+ * Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
37
+
38
+ ## 0.4.2 (2019-11-01)
39
+
40
+ ### Misc
41
+
42
+ * Test against Rails 6.0 final
43
+ * Fixed deprecated call to `to_hash` - *joshforbes*
44
+ * Corrected readme entry for `dimesions` option for `ActiveReporting::Metric` - *joshforbes*
45
+
1
46
  ## 0.4.1 (2019-05-28)
2
47
 
3
48
  ### Features
data/Gemfile CHANGED
@@ -8,24 +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
- when '5-1'
14
- gem 'activerecord', '~> 5.1.0'
15
11
  when '5-2'
16
12
  gem 'activerecord', '~> 5.2.0'
17
13
  when '6-0'
18
- gem 'activerecord', '6.0.0.rc1'
19
- end
20
-
21
- case rails
22
- when '4-2'
23
- case ENV['DB']
24
- when 'pg'
25
- gem 'pg', '~> 0.18'
26
- when 'mysql'
27
- gem 'mysql2', '~> 0.3.18'
28
- when 'sqlite'
29
- gem 'sqlite3', '~> 1.3.0'
30
- end
14
+ gem 'activerecord', '~> 6.0.0'
31
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.4 and later. Other versions may work, but are not supported.
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. Other versions may work, but are not supported.
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,24 @@ 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
+ - date
254
+
255
+ 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
256
 
238
257
  *NOTE*: PRs welcomed to support this functionality in other databases.
239
258
 
@@ -273,8 +292,8 @@ A `Metric` is the basic building block used to describe a question you want to a
273
292
 
274
293
  ```ruby
275
294
  my_metric = ActiveReporting::Metric.new(
276
- :order_total,
277
- fact_model: OrderFactModel,
295
+ :sale_total,
296
+ fact_model: SaleFactModel,
278
297
  aggregate: :sum
279
298
  )
280
299
  ```
@@ -285,7 +304,17 @@ my_metric = ActiveReporting::Metric.new(
285
304
 
286
305
  `aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
287
306
 
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}]`). In hierarchies you can customize the label in this way: `[{order_date: { label: :month, name: :a_custom_name_for_month }}]`. If you use a hash instead of a Symbol to define a hierarchy the `label` item must be a valid field in your table. The `name` can be whatever you want.
307
+ `dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension.
308
+
309
+ 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:
310
+
311
+ - `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.
312
+
313
+ - `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 }}]`.
314
+
315
+ - `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 }}]`
316
+
317
+ - `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
318
 
290
319
  `dimension_filter` - A hash were the keys are dimension filter names and the values are the values passed into the filter.
291
320
 
@@ -303,7 +332,7 @@ end
303
332
  my_metric = ActiveReporting::Metric.new(
304
333
  :my_total,
305
334
  fact_model: UserFactModel,
306
- dimensions: [{ created_at: :quarter } ]
335
+ dimensions: [{ created_at: { datetime_drill: :quarter }} ]
307
336
  )
308
337
  ```
309
338
 
@@ -313,20 +342,20 @@ A `Report` takes an `ActiveReporting::Metric` and ties everything together. It i
313
342
 
314
343
  ```ruby
315
344
  metric = ActiveReporting::Metric.new(
316
- :order_count,
317
- fact_model: OrderFactModel,
345
+ :sale_count,
346
+ fact_model: SaleFactModel,
318
347
  dimensions: [:sales_rep],
319
348
  dimension_filter: {months_ago: 1}
320
349
  )
321
350
 
322
351
  report = ActiveReporting::Report.new(metric)
323
352
  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}]
353
+ => [{sale_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{sale_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
325
354
  ```
326
355
 
327
356
  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
357
 
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`)
358
+ `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
359
 
331
360
  `dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
332
361
 
@@ -336,15 +365,15 @@ A `Report` may also take additional arguments to merge with the `Metric`'s infor
336
365
 
337
366
  ```ruby
338
367
  metric = ActiveReporting::Metric.new(
339
- :order_count,
340
- fact_model: OrderFactModel,
368
+ :sale_count,
369
+ fact_model: SaleFactModel,
341
370
  dimensions: [:sales_rep],
342
371
  dimension_filter: {months_ago: 1}
343
372
  )
344
373
 
345
374
  report = ActiveReporting::Report.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
346
375
  report.run
347
- => [{order_count: 17, sales_rep: 'Mary Sue'}]
376
+ => [{sale_count: 17, sales_rep: 'Mary Sue'}]
348
377
  ```
349
378
 
350
379
  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 +405,6 @@ appropriate `DB` environment variable, e.g. `DB=pg rake test`.
376
405
 
377
406
  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
407
 
379
-
380
408
  ## License
381
409
 
382
410
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
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,21 +4,32 @@ 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
- century millennium].freeze
12
- def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?, :datetime?
11
+ century millennium date].freeze
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
21
 
19
- raise(UnknownDimension, "Dimension '#{dim}' not found on fact model '#{fact_model}'") if found_dimension.nil?
20
-
21
- new(found_dimension, label_config(label))
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))
22
33
  end
23
34
  end
24
35
 
@@ -26,24 +37,30 @@ module ActiveReporting
26
37
  # the field on that dimension. With a hash you can
27
38
  # customize the name of the label
28
39
  #
29
- # @param [Symbol|Hash] label
30
- def self.label_config(label)
31
- return { label: label } unless label.is_a?(Hash)
40
+ # @param [Symbol|Hash] options
41
+ def self.label_config(options)
42
+ unless options.is_a?(Hash)
43
+ return { label: options }
44
+ end
32
45
 
33
46
  {
34
- label: label[:field],
35
- label_name: label[:name]
47
+ label: options[:field],
48
+ label_name: options[:name],
49
+ join_method: options[:join_method],
50
+ datetime_drill: options[:datetime_drill]
36
51
  }
37
52
  end
38
53
 
39
54
  # @param dimension [ActiveReporting::Dimension]
40
55
  # @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
41
56
  # @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
42
- def initialize(dimension, label: nil, label_name: nil)
57
+ def initialize(dimension, label: nil, label_name: nil, join_method: nil, datetime_drill: nil)
43
58
  @dimension = dimension
44
59
 
45
60
  determine_label_field(label)
61
+ determine_datetime_drill(datetime_drill)
46
62
  determine_label_name(label_name)
63
+ determine_join_method(join_method)
47
64
  end
48
65
 
49
66
  # The foreign key to use in queries
@@ -57,10 +74,8 @@ module ActiveReporting
57
74
  #
58
75
  # @return [Array]
59
76
  def select_statement(with_identifier: true)
60
- return [degenerate_select_fragment] if type == Dimension::TYPES[:degenerate]
61
-
62
- ss = ["#{label_fragment} AS #{@label_name}"]
63
- 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]
64
79
  ss
65
80
  end
66
81
 
@@ -68,10 +83,8 @@ module ActiveReporting
68
83
  #
69
84
  # @return [Array]
70
85
  def group_by_statement(with_identifier: true)
71
- return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
72
-
73
86
  group = [label_fragment]
74
- group << identifier_fragment if with_identifier
87
+ group << identifier_fragment if with_identifier && type == Dimension::TYPES[:standard]
75
88
  group
76
89
  end
77
90
 
@@ -81,7 +94,6 @@ module ActiveReporting
81
94
  def order_by_statement(direction:)
82
95
  direction = direction.to_s.upcase
83
96
  raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
84
- return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
85
97
  "#{label_fragment} #{direction}"
86
98
  end
87
99
 
@@ -96,24 +108,47 @@ module ActiveReporting
96
108
 
97
109
  def determine_label_field(label_field)
98
110
  @label = if label_field.present? && validate_hierarchical_label(label_field)
99
- label_field.to_sym
111
+ type == Dimension::TYPES[:degenerate] ? name : label_field.to_sym
112
+ elsif type == Dimension::TYPES[:degenerate]
113
+ name
100
114
  else
101
115
  dimension_fact_model.dimension_label || Configuration.default_dimension_label
102
116
  end
103
117
  end
104
118
 
105
119
  def determine_label_name(label_name)
106
- @label_name = label_name ? "#{name}_#{label_name}" : name
120
+
121
+ if label_name
122
+ @label_name = label_name
123
+ else
124
+ @label_name = name
125
+ @label_name += "_#{@label}" if (type == Dimension::TYPES[:standard] && @label != :name)
126
+ @label_name += "_#{@datetime_drill}" if @datetime_drill
127
+ end
128
+ @label_name
107
129
  end
108
130
 
109
- def validate_hierarchical_label(hierarchical_label)
110
- if datetime?
111
- validate_supported_database_for_datetime_hierarchies
112
- validate_against_datetime_hierarchies(hierarchical_label)
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
113
144
  else
114
- validate_dimension_is_hierachical(hierarchical_label)
115
- validate_against_fact_model_properties(hierarchical_label)
145
+ raise UnknownJoinMethod, "Method '#{join_method}' not included in '#{ReportingDimension::JOIN_METHODS.values}'"
116
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)
117
152
  true
118
153
  end
119
154
 
@@ -134,27 +169,82 @@ module ActiveReporting
134
169
  raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
135
170
  end
136
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
+
137
177
  def validate_against_fact_model_properties(hierarchical_label)
138
178
  return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
139
179
  raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
140
180
  end
141
181
 
142
- def degenerate_fragment
143
- return "#{name}_#{@label}" if datetime?
144
- "#{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
145
188
  end
146
189
 
147
- def degenerate_select_fragment
148
- return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
149
- "#{model.quoted_table_name}.#{name}"
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
198
+
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
150
230
  end
151
231
 
152
232
  def identifier_fragment
153
- "#{klass.quoted_table_name}.#{klass.primary_key}"
233
+ "#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
234
+ end
235
+
236
+ def identifier_fragment_alias
237
+ "#{model.connection.quote_column_name("#{name}_identifier")}"
154
238
  end
155
239
 
156
240
  def label_fragment
157
- "#{klass.quoted_table_name}.#{@label}"
241
+ fragment = "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
242
+ fragment = datetime_drill_label_fragment(fragment) if @datetime_drill
243
+ fragment
244
+ end
245
+
246
+ def label_fragment_alias
247
+ "#{model.connection.quote_column_name(@label_name)}"
158
248
  end
159
249
 
160
250
  def dimension_fact_model
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveReporting
4
- VERSION = '0.4.1'
4
+ VERSION = '0.6.1'
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.1
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Drake
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-05-28 00:00:00.000000000 Z
11
+ date: 2020-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -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,7 +185,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
187
  requirements: []
188
- rubygems_version: 3.0.1
188
+ rubygems_version: 3.0.3
189
189
  signing_key:
190
190
  specification_version: 4
191
191
  summary: Add relational OLAP-like functionality for ActiveRecord