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 +5 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +10 -9
- data/CHANGELOG.md +52 -1
- data/Gemfile +3 -11
- data/README.md +50 -23
- data/active_reporting.gemspec +3 -3
- data/lib/active_reporting.rb +1 -0
- data/lib/active_reporting/dimension.rb +0 -7
- data/lib/active_reporting/fact_model.rb +2 -0
- data/lib/active_reporting/report.rb +8 -5
- data/lib/active_reporting/reporting_dimension.rb +139 -33
- data/lib/active_reporting/version.rb +1 -1
- metadata +11 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 63bda6ce4591fc9695b29db8e53aee19966e3630e9c8b059c686678043f20938
|
|
4
|
+
data.tar.gz: e780a77254b8c8b1cc23ecc732146d581def733fef8669fc3a0419e32fabf5df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89718eab7d4398d77f9d19de7e7160e52feebfc295e03a7c05a113a87159e5e3640b599d681559c53e749bf68bdcee477effff6b4b605c4dcf317da6c09d2cdd
|
|
7
|
+
data.tar.gz: 4d999fe0374b28e826edbb6f6eca9fc9f8ab9ae7aff1056aa88757079e00847f8361862108ba65227f530b72fa918e849dd3bbfe1b337156a685dd4f26b706d8
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
|
@@ -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.
|
|
5
|
-
- 2.
|
|
6
|
-
- 2.
|
|
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;'
|
data/CHANGELOG.md
CHANGED
|
@@ -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
|
-
|
|
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 '
|
|
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.
|
|
11
|
+
ActiveReporting officially supports Ruby 2.5 and later. Other versions may work, but are not supported.
|
|
12
12
|
|
|
13
|
-
ActiveReporting officially supports Rails
|
|
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
|
-
|
|
53
|
-
* A
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
-
:
|
|
277
|
-
fact_model:
|
|
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.
|
|
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
|
-
:
|
|
317
|
-
fact_model:
|
|
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
|
-
=> [{
|
|
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
|
|
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
|
-
:
|
|
340
|
-
fact_model:
|
|
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
|
-
=> [{
|
|
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).
|
data/active_reporting.gemspec
CHANGED
|
@@ -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'
|
|
28
|
-
spec.add_dependency 'activesupport'
|
|
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'
|
|
34
|
+
spec.add_development_dependency 'rake'
|
|
35
35
|
spec.add_development_dependency 'ransack'
|
|
36
36
|
spec.add_development_dependency 'sqlite3'
|
|
37
37
|
end
|
data/lib/active_reporting.rb
CHANGED
|
@@ -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).
|
|
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
|
|
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[
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
79
|
-
@label = if
|
|
80
|
-
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
def determine_label_name(label_name)
|
|
120
|
+
|
|
121
|
+
if label_name
|
|
122
|
+
@label_name = label_name
|
|
90
123
|
else
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
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
|
+
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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
|