active_reporting 0.6.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -30
- data/README.md +141 -45
- data/lib/active_reporting/active_record_adaptor.rb +2 -10
- data/lib/active_reporting/configuration.rb +63 -45
- data/lib/active_reporting/datetime_drill.rb +88 -0
- data/lib/active_reporting/dimension.rb +10 -6
- data/lib/active_reporting/fact_model.rb +15 -10
- data/lib/active_reporting/fact_model_registry.rb +51 -0
- data/lib/active_reporting/metric.rb +12 -7
- data/lib/active_reporting/railtie.rb +9 -0
- data/lib/active_reporting/report.rb +75 -32
- data/lib/active_reporting/reporting_dimension.rb +60 -79
- data/lib/active_reporting/version.rb +1 -1
- data/lib/active_reporting.rb +43 -23
- metadata +20 -122
- data/.codeclimate.yml +0 -15
- data/.gitignore +0 -12
- data/.rubocop.yml +0 -29
- data/.travis.yml +0 -20
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -15
- data/Rakefile +0 -12
- data/active_reporting.gemspec +0 -37
- data/bin/console +0 -15
- data/bin/setup +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2739361482925da496d90c15b8aa28c386587f7efaa9d37b918d1847eaf2c45
|
|
4
|
+
data.tar.gz: 501c16713ac1d1d10eccf5679a820c920b7459321c07fbf2168c1091e12a1f01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d8e32d39baba7d583ce6a9734e1040252927ef1eee97db85ed6b28e47484f99c31fcea3e897a1f6be55f79da2eb5795227d36a742a11c627ca07a31f0247a5a
|
|
7
|
+
data.tar.gz: 94ebd8032809fbede8e295375cf76f073c183807b417868567670f40effb4df7d5e130cd94ae79a8cd80f50c4c35b3a1c24c1878ef2e53704e1c4016dce7f027
|
data/CHANGELOG.md
CHANGED
|
@@ -1,108 +1,164 @@
|
|
|
1
|
+
## 1.0.0 (2026-10-09)
|
|
2
|
+
|
|
3
|
+
### Breaking Changes
|
|
4
|
+
|
|
5
|
+
- Only support supported versions of Ruby and Rails.
|
|
6
|
+
- Require ActiveRecord and ActiveSupport 8.0 or newer (previously any version was allowed)
|
|
7
|
+
- `Configuration.metric_lookup_class=` no longer singularizes the given name. `metric_lookup_class = StoredMetrics` now resolves to `::StoredMetrics` (previously `::StoredMetric`). If you relied on the old behavior (e.g. `metric_lookup_class = :metrics` to mean `::Metric`), pass the singular name instead.
|
|
8
|
+
- Removed the `{ created_at: :month }` shorthand for datetime drills on degenerate dimensions, deprecated in 0.6.0. Use `{ created_at: { datetime_drill: :month } }` instead; the old form now raises `InvalidDimensionLabel` with the replacement syntax. This also removes the spurious deprecation warning that was printed for every plain degenerate dimension (e.g. `dimensions: [:kind]`)
|
|
9
|
+
- Ransack is no longer required when the gem is loaded; it is loaded the first time a ransack feature is used
|
|
10
|
+
- Fact models generated for models without a `[ModelName]FactModel` class are no longer assigned to a constant (previously `PostFactModel` was defined the first time `Post.fact_model` was called). Define the fact model class if you need to reference it
|
|
11
|
+
- On MySQL, `datetime_drill` now truncates datetimes like PostgreSQL's `date_trunc` and returns `DATETIME` values, instead of extracting a part as an integer. For example, a `month` drill now returns `2026-09-01 00:00:00` instead of `9`, so the same month in different years is no longer grouped together. `week` now returns the Monday the week starts on (previously the day of the week) and `day` returns the date at midnight (previously the day of the month)
|
|
12
|
+
- Measures are now quoted and qualified with the fact model's table (e.g. `SUM("sales"."total")`). This fixes ambiguous column errors when a joined dimension table has a column with the same name as the measure, and stops a measure from injecting SQL. A measure that was a SQL expression (e.g. `measure: 'price * quantity'`) must now be wrapped in `Arel.sql`: `measure: Arel.sql('price * quantity')`
|
|
13
|
+
- A standard dimension that uses a hierarchy level other than its default label (`field:`) or a `datetime_drill` no longer includes its identifier in the query, so its rows actually roll up. Previously the report also grouped by the dimension table's primary key, returning one row per record (e.g. one row per day labeled with its month) unless `dimension_identifiers: false` was passed. These rows no longer have a `<dimension>_identifier` key
|
|
14
|
+
- A scope dimension filter given `false` or `'false'` is now left out of the query instead of being called with `false` as its argument (previously this raised `ArgumentError` for scopes without parameters). A scope that takes a boolean can no longer be passed `false`; use a lambda dimension filter instead. See "Dimension filter values" in the README
|
|
15
|
+
- A lambda dimension filter with parameters is now always passed its value, including `true` (previously `true` called it without an argument, raising `ArgumentError`). A lambda without parameters is now applied for any value except `false` or `'false'`, which leave it out (previously `false` raised `ArgumentError`)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
- Fix `datetime_drill: :date` generating invalid SQL on PostgreSQL by quoting the column as a string literal
|
|
20
|
+
- `ActiveReporting.fetch_metric` now raises `BadMetricLookupClass` instead of `NameError` when the configured `metric_lookup_class` is not defined
|
|
21
|
+
- Dimension label callbacks are now applied when the dimension uses a custom `name:`, uses the default `:name` label, or is a degenerate dimension (previously they were silently skipped in all of these cases)
|
|
22
|
+
- Auto-generated fact models for namespaced models (e.g. `Admin::Post`) are now created in the model's namespace (`Admin::PostFactModel`) instead of raising `NameError`
|
|
23
|
+
- A `NameError` raised while loading a defined fact model is no longer swallowed and replaced with an empty auto-generated fact model
|
|
24
|
+
- Metric names are now quoted in the generated SQL, so metrics named after reserved words (e.g. `:order`) no longer produce invalid queries
|
|
25
|
+
- An invalid `order_by_dimension` direction now raises `ActiveReporting::InvalidOrderDirection` (a `RuntimeError` subclass, so existing `rescue RuntimeError` code still works) with a message naming the dimension
|
|
26
|
+
- Using a ransack dimension filter without ransack installed now raises `RansackNotAvailable` instead of `NoMethodError`
|
|
27
|
+
- `metric_filter` operators may now be strings (e.g. `{ 'gt' => 3 }` from request params), and an unknown operator raises `ActiveReporting::UnknownMetricFilterOperator` when the metric or report is built (previously both generated invalid SQL that failed when the report ran)
|
|
28
|
+
- Ordering by a dimension that isn't one of the report's dimensions now raises `UnknownDimension` when the report is created (previously the ordering was silently dropped). An invalid ordering direction also now raises `InvalidOrderDirection` when the report is created instead of when it runs
|
|
29
|
+
- Dimension filters that fall back to ransack are no longer stored on the fact model when a report uses them. Running reports no longer modifies fact models, which are shared between threads, and unknown filter names from user input no longer accumulate in memory
|
|
30
|
+
- `Configuration.ransack_fallback = false` no longer raises `RansackNotAvailable` when ransack is not installed
|
|
31
|
+
- Fact models are looked up again after Rails reloads code. Previously a generated fact model kept pointing to the model class from before the reload, and a model that isn't reloaded (e.g. from an engine) kept returning the old fact model
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
- `datetime_drill` now works on SQLite. All databases now truncate datetimes the same way PostgreSQL's `date_trunc` does
|
|
36
|
+
- `datetime_drill` now works with the Trilogy MySQL adapter (and any other adapter built on Rails' MySQL, PostgreSQL, or SQLite adapters)
|
|
37
|
+
- `datetime_drill` now works on `date` columns and on PostgreSQL `timestamp with time zone` (`timestamptz`) columns. Previously only columns Rails reports as `datetime` were accepted, so these raised `InvalidDimensionLabel`
|
|
38
|
+
|
|
39
|
+
### Misc
|
|
40
|
+
|
|
41
|
+
- `Dimension#hierarchical?` and `Dimension#association` now cache `false`/`nil` results instead of recomputing them on every call
|
|
42
|
+
- Add gem metadata (changelog, source, and bug tracker links) and require MFA for releases
|
|
43
|
+
- Requiring the gem no longer forces `ActiveRecord::Base` to load; models are extended via `ActiveSupport.on_load(:active_record)`
|
|
44
|
+
- Report queries now run through `select_all`, so they use the ActiveRecord query cache and appear as `ActiveReporting` in SQL logs and instrumentation
|
|
45
|
+
- Datetime drill SQL moved to `ActiveReporting::DatetimeDrill`; `ReportingDimension::SUPPORTED_DBS` was removed
|
|
46
|
+
- Fix typos and inaccurate descriptions in documentation comments, error messages, and the README
|
|
47
|
+
- Document why `has_many` dimensions give wrong results and what to use instead
|
|
48
|
+
- `Configuration` uses `mattr_reader` with defaults instead of hand-written getters
|
|
49
|
+
- Development: `bin/setup` installs the adapter for `DB` and creates the test database, database adapters are optional Gemfile groups, and CI runs RuboCop (now including the tests) and codespell
|
|
50
|
+
|
|
51
|
+
## 0.6.2 (2024-03-18)
|
|
52
|
+
|
|
53
|
+
### Features
|
|
54
|
+
|
|
55
|
+
- `Metric` can now take an optional `measure` argument to override the measure that would normally be used.
|
|
56
|
+
|
|
1
57
|
## 0.6.1 (2020-08-29)
|
|
2
58
|
|
|
3
59
|
### Misc
|
|
4
60
|
|
|
5
|
-
|
|
61
|
+
- Add `date` as an option for datetime drills - _germanotm_
|
|
6
62
|
|
|
7
63
|
## 0.6.0 (2020-08-21)
|
|
8
64
|
|
|
9
65
|
### Features
|
|
10
66
|
|
|
11
|
-
|
|
12
|
-
|
|
67
|
+
- Support to implicit hierarchical on datetime columns in MySQL (#33) - _germanotm_
|
|
68
|
+
- Added `{ datetime_drill: :month }` option for reporting dimensions to explicitly - _germanotm_
|
|
13
69
|
|
|
14
|
-
This
|
|
70
|
+
This deprecates the use of key-value only use for report dimension options (ie, `dimensions: [{ dim: single_option }]`).
|
|
15
71
|
Instead, use `dimensions: [{ dim: { option: value} }]` See the README for all reporting dimension options.
|
|
16
72
|
|
|
17
73
|
## 0.5.1 (2020-06-31)
|
|
18
74
|
|
|
19
75
|
### Features
|
|
20
76
|
|
|
21
|
-
|
|
77
|
+
- Allow dimensions defined in a `Metric` to use LEFT OUTER JOINs via a new `:join_method` option (#32) - _germanotm_
|
|
22
78
|
|
|
23
79
|
### Misc
|
|
24
80
|
|
|
25
|
-
|
|
26
|
-
|
|
81
|
+
- Fixed warning about initialized variables
|
|
82
|
+
- Fixed Ruby 2.7 warning
|
|
27
83
|
|
|
28
84
|
## 0.5.0 (2020-06-30)
|
|
29
85
|
|
|
30
86
|
### Bug Fixes
|
|
31
87
|
|
|
32
|
-
|
|
88
|
+
- Fix Missing quotation marks in column names causing SQL errors on MYSQL (#30) - _germanotm_
|
|
33
89
|
|
|
34
90
|
### Misc
|
|
35
91
|
|
|
36
|
-
|
|
92
|
+
- Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
|
|
37
93
|
|
|
38
94
|
## 0.4.2 (2019-11-01)
|
|
39
95
|
|
|
40
96
|
### Misc
|
|
41
97
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
98
|
+
- Test against Rails 6.0 final
|
|
99
|
+
- Fixed deprecated call to `to_hash` - _joshforbes_
|
|
100
|
+
- Corrected readme entry for `dimensions` option for `ActiveReporting::Metric` - _joshforbes_
|
|
45
101
|
|
|
46
102
|
## 0.4.1 (2019-05-28)
|
|
47
103
|
|
|
48
104
|
### Features
|
|
49
105
|
|
|
50
|
-
|
|
106
|
+
- Hierarchical dimensions may now have custom keys in result (#16) - _andresgutgon_
|
|
51
107
|
|
|
52
108
|
### Misc
|
|
53
109
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
110
|
+
- Test against Raisl 6.0RC
|
|
111
|
+
- Loosen AR requirements. The gem will install for any AR version, but only ones listed in the README are supported
|
|
112
|
+
- Test against active Rubies
|
|
57
113
|
|
|
58
114
|
## 0.4.0 (2018-05-02)
|
|
59
115
|
|
|
60
116
|
### Breaking Changes
|
|
61
117
|
|
|
62
|
-
|
|
118
|
+
- Gemspec now requires Ruby 2.3 and later to install
|
|
63
119
|
|
|
64
120
|
### Features
|
|
65
121
|
|
|
66
|
-
|
|
122
|
+
- Dimension off of `datetime` columns by date parts in PostgreSQL (See README for details) (#10) - _niborg_
|
|
67
123
|
|
|
68
124
|
## 0.3.0 (2018-04-12)
|
|
69
125
|
|
|
70
126
|
### Bug Fixes
|
|
71
127
|
|
|
72
|
-
|
|
73
|
-
|
|
128
|
+
- Specify rescue from LoadError for ransack (#9) - _niborg_
|
|
129
|
+
- Fix ransack fallback logic (#8) - _germanotm_
|
|
74
130
|
|
|
75
131
|
### Misc
|
|
76
132
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
- Test against Rails 5.2
|
|
134
|
+
- Test against Ruby 2.5
|
|
135
|
+
- Drop support for Rails 5.0 (EOL-ed)
|
|
136
|
+
- Drop support for Ruby 2.2 (EOL-ed)
|
|
81
137
|
|
|
82
138
|
## 0.2.0 (2017-06-17)
|
|
83
139
|
|
|
84
140
|
### Breaking Changes
|
|
85
141
|
|
|
86
|
-
|
|
142
|
+
- `FactModel.use_model` renamed to `FactModel.model=`
|
|
87
143
|
|
|
88
144
|
### Bug Fixes
|
|
89
145
|
|
|
90
|
-
|
|
146
|
+
- `metric` lives on fact model and not metric (#3) - _wheeyls_
|
|
91
147
|
|
|
92
148
|
### Misc
|
|
93
149
|
|
|
94
|
-
|
|
150
|
+
- Readme corrections and updates (#2) - _wheeyls_
|
|
95
151
|
|
|
96
152
|
## 0.1.1 (2017-04-22)
|
|
97
153
|
|
|
98
154
|
### Bug Fixes
|
|
99
155
|
|
|
100
|
-
|
|
156
|
+
- Fix MySQL querying
|
|
101
157
|
|
|
102
158
|
### Misc
|
|
103
159
|
|
|
104
|
-
|
|
160
|
+
- Properly test against multiple dbs and versions of ActiveRecord in CI
|
|
105
161
|
|
|
106
162
|
## 0.1.0 (2017-04-16)
|
|
107
163
|
|
|
108
|
-
|
|
164
|
+
- Initial release
|
data/README.md
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
[](https://codeclimate.com/github/t27duck/active_reporting)
|
|
1
|
+

|
|
4
2
|
|
|
5
3
|
# ActiveReporting
|
|
6
4
|
|
|
@@ -8,9 +6,9 @@ ActiveReporting implements various terminology used in Relational Online Analyti
|
|
|
8
6
|
|
|
9
7
|
ActiveReporting officially supports MySQL, PostgreSQL, and SQLite.
|
|
10
8
|
|
|
11
|
-
ActiveReporting officially supports Ruby
|
|
9
|
+
ActiveReporting officially supports [Ruby versions under official support](https://endoflife.date/ruby).
|
|
12
10
|
|
|
13
|
-
ActiveReporting officially supports Rails
|
|
11
|
+
ActiveReporting officially supports [Rails versions under official support](https://endoflife.date/rails).
|
|
14
12
|
|
|
15
13
|
## Installation
|
|
16
14
|
|
|
@@ -30,13 +28,13 @@ Or install it yourself as:
|
|
|
30
28
|
|
|
31
29
|
## What is "Reporting"?
|
|
32
30
|
|
|
33
|
-
Reporting is the collection and presentation data so that it can be analyzed. Our databases only store one thing: data. Data is great for computers but mostly worthless to humans. What
|
|
31
|
+
Reporting is the collection and presentation of data so that it can be analyzed. Our databases only store one thing: data. Data is great for computers but mostly worthless to humans. What ActiveReporting does is turn that _data_ into _information_ to help humans make decisions.
|
|
34
32
|
|
|
35
33
|
## Terminology
|
|
36
34
|
|
|
37
35
|
ROLAP uses a set of terms to describe how a report is generated. ActiveReporting implements them in the closest way possible in Ruby-land.
|
|
38
36
|
|
|
39
|
-
### Fact table (
|
|
37
|
+
### Fact table (sometimes called fact model)
|
|
40
38
|
|
|
41
39
|
A fact table is the primary table where information is derived from in a report. It commonly contains fact columns (usually numeric values) and dimension columns (foreign keys to other tables or values that can be grouped together).
|
|
42
40
|
|
|
@@ -50,9 +48,9 @@ A dimension is a point of data used to "slice and dice" data from a fact model.
|
|
|
50
48
|
|
|
51
49
|
Examples:
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
- A sales rep on a fact table of sales
|
|
52
|
+
- A state of a sale on a state machine
|
|
53
|
+
- The manufacturer on a fact table of widgets
|
|
56
54
|
|
|
57
55
|
SQL Equivalent: JOIN, GROUP BY
|
|
58
56
|
|
|
@@ -64,8 +62,8 @@ A hierarchy for a dimension is related attributes that live on a dimension table
|
|
|
64
62
|
|
|
65
63
|
Examples:
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
- Dates: Date, Month, Year, Quarter
|
|
66
|
+
- Mobile Phone: Model, Manufacturer, OS, Wireless Technology
|
|
69
67
|
|
|
70
68
|
### Dimension Member (also known as dimension labels)
|
|
71
69
|
|
|
@@ -73,7 +71,7 @@ This is information related to a dimension. When the dimension lives on the fact
|
|
|
73
71
|
|
|
74
72
|
Examples:
|
|
75
73
|
|
|
76
|
-
|
|
74
|
+
- 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.
|
|
77
75
|
|
|
78
76
|
### Dimension Filter (or just "filter")
|
|
79
77
|
|
|
@@ -89,8 +87,8 @@ A measure is a column in a fact table (usually a numeric value) used in aggregat
|
|
|
89
87
|
|
|
90
88
|
Examples:
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
- Total amount in a sale
|
|
91
|
+
- Number of units used in a transaction
|
|
94
92
|
|
|
95
93
|
SQL Equivalent: Column in the fact table used in an aggregation function
|
|
96
94
|
|
|
@@ -98,7 +96,7 @@ Rails: ActiveRecord attribute
|
|
|
98
96
|
|
|
99
97
|
### Metric
|
|
100
98
|
|
|
101
|
-
A metric is a measured value and the subject of the report. It is the result of
|
|
99
|
+
A metric is a measured value and the subject of the report. It is the result of _the_ question you want answered.
|
|
102
100
|
|
|
103
101
|
SQL Equivalent: A query result
|
|
104
102
|
|
|
@@ -108,7 +106,7 @@ Rails: The result of an ActiveRecord query
|
|
|
108
106
|
|
|
109
107
|
Star schema is a way of structuring your relational data. It is one of the most common forms of organization for relational data warehousing. The layout of a star schema consists of a fact table referencing one or more dimension tables. When laid out in an entity relationship diagram, it resembles a star.
|
|
110
108
|
|
|
111
|
-
[
|
|
109
|
+

|
|
112
110
|
|
|
113
111
|
More information: https://en.wikipedia.org/wiki/Star_schema
|
|
114
112
|
|
|
@@ -116,7 +114,7 @@ More information: https://en.wikipedia.org/wiki/Star_schema
|
|
|
116
114
|
|
|
117
115
|
Snowflake schema is a super class of star schema. A fact table still resides in the middle of the diagram, but dimension tables are normalized out into multiple tables resulting in the resemblance of a snowflake.
|
|
118
116
|
|
|
119
|
-
[
|
|
117
|
+

|
|
120
118
|
|
|
121
119
|
More information: https://en.wikipedia.org/wiki/Snowflake_schema
|
|
122
120
|
|
|
@@ -142,16 +140,15 @@ ActiveReporting::Configuration.setting = value
|
|
|
142
140
|
|
|
143
141
|
`default_measure` - If a fact model does not specify a measure to use for aggregates, this value will be used. (Default: `:value`)
|
|
144
142
|
|
|
145
|
-
`ransack_fallback` - If the ransack gem is
|
|
146
|
-
|
|
147
|
-
`metric_lookup_class` - The name of a constant used to lookup prebuilt `Reporting::Metric` objects by name. The constant should define a class method called `#lookup` which can take a string or symbol of the metric name. (Default: `::Metric`)
|
|
143
|
+
`ransack_fallback` - If the ransack gem is available, allow all unknown dimension filters to be delegated to ransack. (Default: `false`)
|
|
148
144
|
|
|
145
|
+
`metric_lookup_class` - The name of a constant used to lookup prebuilt `ActiveReporting::Metric` objects by name. The constant should define a class method called `#lookup` which can take a string or symbol of the metric name. Accepts a class, string, or symbol; the name is used as-is (`:stored_metrics` becomes `::StoredMetrics`). (Default: `::Metric`)
|
|
149
146
|
|
|
150
147
|
## ActiveReporting::FactModel
|
|
151
148
|
|
|
152
149
|
In ActiveReporting, a fact model stores configuration information on how it can be used in reports. We use the term fact model instead of fact table because this class "models how the fact table interacts with dimensions and other reporting features".
|
|
153
150
|
|
|
154
|
-
You can put these classes anywhere you want in your app, though I recommend putting them in `app/fact_models`
|
|
151
|
+
You can put these classes anywhere you want in your app, though I recommend putting them in `app/fact_models` so Rails autoloads and reloads them with your models
|
|
155
152
|
|
|
156
153
|
### Linking a fact model to an ActiveRecord model
|
|
157
154
|
|
|
@@ -165,7 +162,7 @@ class TicketFactModel < ActiveReporting::FactModel
|
|
|
165
162
|
end
|
|
166
163
|
```
|
|
167
164
|
|
|
168
|
-
Alternatively, you may manually specify the model
|
|
165
|
+
Alternatively, you may manually specify the model with `self.model=`
|
|
169
166
|
|
|
170
167
|
```ruby
|
|
171
168
|
class TicketFactModel < ActiveReporting::FactModel
|
|
@@ -177,9 +174,13 @@ class TicketFactModel < ActiveReporting::FactModel
|
|
|
177
174
|
end
|
|
178
175
|
```
|
|
179
176
|
|
|
177
|
+
If a model doesn't have a fact model, ActiveReporting generates one with the default configuration the first time it's needed (for example, when the model is used as a dimension). Generated fact models aren't assigned to a constant, so define a `[ModelName]FactModel` class if you need to refer to one.
|
|
178
|
+
|
|
179
|
+
In Rails, fact models are looked up again after code is reloaded in development, so changes to models and fact models take effect without restarting the server.
|
|
180
|
+
|
|
180
181
|
### Configuring a fact model's measure
|
|
181
182
|
|
|
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.
|
|
183
|
+
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. The column is quoted and qualified with the fact model's table. To aggregate a SQL expression instead of a column, wrap it in `Arel.sql`, e.g. `self.measure = Arel.sql('base_price + taxes')`. Only use `Arel.sql` with trusted input.
|
|
183
184
|
|
|
184
185
|
```ruby
|
|
185
186
|
class SaleFactModel < ActiveReporting::FactModel
|
|
@@ -191,7 +192,7 @@ end
|
|
|
191
192
|
|
|
192
193
|
### Declaring dimensions on a fact model
|
|
193
194
|
|
|
194
|
-
You must declare what a fact model is dimensional by. A valid dimension is a column on the fact model's ActiveRecord model or a `belongs_to`/`has_one through`
|
|
195
|
+
You must declare what a fact model is dimensional by. A valid dimension is a column on the fact model's ActiveRecord model or a `belongs_to`/`has_one` relationship (including `has_one :through`). Avoid `has_many` relationships; see [below](#avoid-has_many-dimensions).
|
|
195
196
|
|
|
196
197
|
```ruby
|
|
197
198
|
class TicketFactModel < ActiveReporting::FactModel
|
|
@@ -201,6 +202,53 @@ class TicketFactModel < ActiveReporting::FactModel
|
|
|
201
202
|
end
|
|
202
203
|
```
|
|
203
204
|
|
|
205
|
+
### Avoid `has_many` dimensions
|
|
206
|
+
|
|
207
|
+
A dimension that is a relationship joins its table to the fact table. With `belongs_to` and `has_one`, each fact row joins to at most one row, so aggregates are correct. With `has_many` (or `has_many :through` or `has_and_belongs_to_many`), each fact row is repeated once for every related record before it is aggregated. ActiveReporting allows these dimensions and raises no error, but the results are wrong:
|
|
208
|
+
|
|
209
|
+
- `count` counts related records instead of facts
|
|
210
|
+
- `sum` and `avg` include a fact's measure once for every related record (`max` and `min` are unaffected)
|
|
211
|
+
- A fact related to records in more than one group is counted in each group, so the rows add up to more than the total
|
|
212
|
+
|
|
213
|
+
For example, counting tickets by the type of author who commented on them:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
class Ticket < ActiveRecord::Base
|
|
217
|
+
has_many :comments
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class CommentFactModel < ActiveReporting::FactModel
|
|
221
|
+
dimension_hierarchy %i[author_type]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
class TicketFactModel < ActiveReporting::FactModel
|
|
225
|
+
dimension :comments # has_many relationship
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
ActiveReporting::Metric.new(:ticket_count, fact_model: TicketFactModel, dimensions: [{ comments: { field: :author_type } }])
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
A ticket with three comments from staff is counted three times under `staff`, and a ticket with comments from both staff and customers is counted under both. The query is effectively `SELECT COUNT(*) ... FROM tickets INNER JOIN comments ... GROUP BY comments.author_type`.
|
|
232
|
+
|
|
233
|
+
Instead:
|
|
234
|
+
|
|
235
|
+
- **Use the "many" side as the fact model.** Report on comments, with the ticket as a `belongs_to` dimension. A fact model's rows should be the most detailed level the report needs.
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class CommentFactModel < ActiveReporting::FactModel
|
|
239
|
+
dimension :ticket # belongs_to relationship
|
|
240
|
+
dimension :author_type # Column on the comments table
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- **Filter instead of grouping.** To count tickets that have a matching related record, use a dimension filter with a subquery. A subquery doesn't repeat fact rows the way a join does.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class TicketFactModel < ActiveReporting::FactModel
|
|
248
|
+
dimension_filter :commented_on_by, ->(type) { where(id: Comment.where(author_type: type).select(:ticket_id)) }
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
204
252
|
### When a fact model is used as a dimension
|
|
205
253
|
|
|
206
254
|
When another fact model uses a relationship as a dimension, that ActiveRecord model's fact model class can hold configuration information for how to act when used as a dimension.
|
|
@@ -215,7 +263,7 @@ end
|
|
|
215
263
|
|
|
216
264
|
### Dimension Hierarchies
|
|
217
265
|
|
|
218
|
-
For dimensions that can have a hierarchy (such as a mobile phone), you can declare
|
|
266
|
+
For dimensions that can have a hierarchy (such as a mobile phone), you can declare what columns make it up. This will allow reports to dimension against a fact model and be able to use different labels to group by.
|
|
219
267
|
|
|
220
268
|
```ruby
|
|
221
269
|
class PhoneFactModel < ActiveReporting::FactModel
|
|
@@ -225,7 +273,7 @@ end
|
|
|
225
273
|
|
|
226
274
|
### Drill down / Roll up (Drill up) with datetime columns
|
|
227
275
|
|
|
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,
|
|
276
|
+
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, ActiveReporting provides a `datetime_drill` option that can be passed with the dimension on the metric definition to drill datetime columns.
|
|
229
277
|
|
|
230
278
|
To use, declare a datetime dimension on a fact model as normal:
|
|
231
279
|
|
|
@@ -252,9 +300,15 @@ When creating a metric, ActiveReporting will recognize the following datetime hi
|
|
|
252
300
|
- millennium
|
|
253
301
|
- date
|
|
254
302
|
|
|
255
|
-
|
|
303
|
+
A datetime drill truncates the datetime to the given level, like PostgreSQL's [`date_trunc` function](https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC). For example, a `month` drill on `2026-09-15 10:30:00` returns `2026-09-01 00:00:00`, so the same month in different years is grouped separately, and a `week` drill returns the Monday the week starts on. The `date` drill returns the date portion of the datetime.
|
|
304
|
+
|
|
305
|
+
Datetime drills may be used on `datetime`, `timestamp`, `timestamptz` (PostgreSQL's `timestamp with time zone`), and `date` columns. A drill on a `date` column is treated as a datetime at midnight.
|
|
306
|
+
|
|
307
|
+
This works the same on every supported database:
|
|
256
308
|
|
|
257
|
-
|
|
309
|
+
- **PostgreSQL** (including PostGIS) uses `date_trunc` and returns timestamps.
|
|
310
|
+
- **MySQL** (both the `mysql2` and `trilogy` adapters) builds the truncated value with [date and time functions](https://dev.mysql.com/doc/refman/8.4/en/date-and-time-functions.html) and returns `DATETIME` values.
|
|
311
|
+
- **SQLite** emulates `date_trunc` and returns the truncated datetime as `YYYY-MM-DD HH:MM:SS` text.
|
|
258
312
|
|
|
259
313
|
## Configuring Dimension Filters
|
|
260
314
|
|
|
@@ -274,7 +328,34 @@ The first example exposes the `Ticket.open` scope to the fact model allowing it
|
|
|
274
328
|
|
|
275
329
|
The second example defines a lambda to be invoked like a Rails scope. It joins against the `category` relationship on `Ticket` and filters by the category's name.
|
|
276
330
|
|
|
277
|
-
The third example defines a filter called "subject_cont" and will delegate it to ransack when called.
|
|
331
|
+
The third example defines a filter called "subject_cont" and will delegate it to ransack when called. Ransack is loaded the first time it's needed; add it to your Gemfile to use it. Ransack 4 and newer also require the model to allowlist searchable attributes with a `ransackable_attributes` class method (here, `Ticket.ransackable_attributes` must include `subject`).
|
|
332
|
+
|
|
333
|
+
### Dimension filter values
|
|
334
|
+
|
|
335
|
+
A report passes each dimension filter's value to the filter. How the value is used depends on the kind of filter, which matters most when values come from request params as strings:
|
|
336
|
+
|
|
337
|
+
| Filter | `true` / `'true'` | `false` / `'false'` | Any other value |
|
|
338
|
+
|---|---|---|---|
|
|
339
|
+
| Lambda without parameters, e.g. `-> { where(open: true) }` | Applied | Left out | Applied |
|
|
340
|
+
| Lambda with a parameter, e.g. `->(x) { where(name: x) }` | Passed as the argument | Passed as the argument | Passed as the argument |
|
|
341
|
+
| Scope | Called without an argument | Left out | Passed as the argument |
|
|
342
|
+
|
|
343
|
+
A scope is called without an argument for `true` because Rails defines every scope as a method that accepts any arguments, so ActiveReporting can't tell whether a scope takes one. As a result, a scope can't be passed `true` or `false`. For a filter that takes a boolean, use a lambda with a parameter instead:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
class TicketFactModel < ActiveReporting::FactModel
|
|
347
|
+
# `{ open: true }` applies `Ticket.open`, `{ open: false }` leaves it out
|
|
348
|
+
dimension_filter :open
|
|
349
|
+
|
|
350
|
+
# `{ resolved: true }` and `{ resolved: false }` both reach the lambda. Values from params
|
|
351
|
+
# are strings, so cast them yourself, e.g. with ActiveModel::Type::Boolean
|
|
352
|
+
dimension_filter :resolved, ->(resolved) { where(resolved: ActiveModel::Type::Boolean.new.cast(resolved)) }
|
|
353
|
+
end
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Ransack filters are always passed their value as is, and ransack decides what it means.
|
|
357
|
+
|
|
358
|
+
Only `false` and `'false'` leave a filter out. Other "falsy" strings such as `'0'` are passed to a scope as an argument, since a scope may take a number.
|
|
278
359
|
|
|
279
360
|
Only dimension filters defined in the fact model may be used. Whitelisting available filters allows for more control over what the user may filter by. Giving the user full control to call any scope or method from the ActiveRecord model could lead to unexpected results, poor performing queries, or possible security concerns.
|
|
280
361
|
|
|
@@ -306,23 +387,25 @@ my_metric = ActiveReporting::Metric.new(
|
|
|
306
387
|
|
|
307
388
|
`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
389
|
|
|
309
|
-
You may pass a hash instead of a symbol to customize the dimension options (example: { dimension_name: { option1: value, option2: value}}). The
|
|
390
|
+
You may pass a hash instead of a symbol to customize the dimension options (example: { dimension_name: { option1: value, option2: value}}). The available options are:
|
|
310
391
|
|
|
311
|
-
- `field` - Specify the hierarchy level that should be used instead the default dimension label. Ex: `[:sales_rep, {mobile_phone: { field :
|
|
392
|
+
- `field` - Specify the hierarchy level that should be used instead of the default dimension label. Ex: `[:sales_rep, {mobile_phone: { field: :manufacturer }}]`. 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
393
|
|
|
313
|
-
- `name` - You may
|
|
394
|
+
- `name` - You may customize 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
395
|
|
|
315
396
|
- `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
397
|
|
|
317
398
|
- `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.
|
|
318
399
|
|
|
319
|
-
`dimension_filter` - A hash
|
|
400
|
+
`dimension_filter` - A hash where the keys are dimension filter names and the values are the values passed into the filter.
|
|
320
401
|
|
|
321
402
|
`metric_filter` - An additional HAVING clause to be tacked on to the end of the query. This allows for the further filtering of the end results based on the value of the aggregate. (Examples: `{gt: 3}`, `{eq: 5}`, `{lte: 7}`)
|
|
322
403
|
|
|
323
|
-
`
|
|
404
|
+
`measure` - Override the measure on the `ActiveReporting::FactModel` used.
|
|
405
|
+
|
|
406
|
+
`order_by_dimension` - Allows you to set the ordering of the results based on a dimension label. The dimension must be one of the report's dimensions, either from the metric or added by the report, or `UnknownDimension` is raised. (Examples: `{author: :desc}`, `{sales_rep: :asc}`)
|
|
324
407
|
|
|
325
|
-
|
|
408
|
+
You can take advantage of datetime drills on `datetime` columns, as mentioned above:
|
|
326
409
|
|
|
327
410
|
```ruby
|
|
328
411
|
class UserFactModel < ActiveReporting::FactModel
|
|
@@ -338,7 +421,7 @@ my_metric = ActiveReporting::Metric.new(
|
|
|
338
421
|
|
|
339
422
|
## ActiveReporting::Report
|
|
340
423
|
|
|
341
|
-
A `Report` takes an `ActiveReporting::Metric` and ties everything together. It is responsible for building and executing the query to generate a result. The result is
|
|
424
|
+
A `Report` takes an `ActiveReporting::Metric` and ties everything together. It is responsible for building and executing the query to generate a result. The result is a simple array of hashes.
|
|
342
425
|
|
|
343
426
|
```ruby
|
|
344
427
|
metric = ActiveReporting::Metric.new(
|
|
@@ -355,13 +438,13 @@ report.run
|
|
|
355
438
|
|
|
356
439
|
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`.
|
|
357
440
|
|
|
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`)
|
|
441
|
+
`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. Identifiers are only included for dimensions labeled by their default label; a dimension using another hierarchy level (`field`) or a `datetime_drill` groups many records into one row, so it has no identifier. (Default `true`)
|
|
359
442
|
|
|
360
|
-
`dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
|
|
443
|
+
`dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters. The `Metric`'s dimension filters are part of its definition, so when both set the same filter, the `Metric`'s value is used. This makes it safe to pass user input here without it widening what the metric measures.
|
|
361
444
|
|
|
362
445
|
`dimensions` - An array of additional dimensions which are merged with the `Metric`'s dimensions.
|
|
363
446
|
|
|
364
|
-
`metric_filter` - Sets the HAVING clause of the final query and is merged with the `Metric`'s metric filter.
|
|
447
|
+
`metric_filter` - Sets the HAVING clause of the final query and is merged with the `Metric`'s metric filter. Unlike dimension filters, when both set the same operator, the `Report`'s value replaces the `Metric`'s.
|
|
365
448
|
|
|
366
449
|
```ruby
|
|
367
450
|
metric = ActiveReporting::Metric.new(
|
|
@@ -376,7 +459,7 @@ report.run
|
|
|
376
459
|
=> [{sale_count: 17, sales_rep: 'Mary Sue'}]
|
|
377
460
|
```
|
|
378
461
|
|
|
379
|
-
It may be more DRY to store ready-made metrics in a database table or stored in memory to use as the
|
|
462
|
+
It may be more DRY to store ready-made metrics in a database table or stored in memory to use as the basis for various reports. You can pass a string or symbol into a `Report` instead of a `Metric` to look up a 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`.
|
|
380
463
|
|
|
381
464
|
```ruby
|
|
382
465
|
class StoredMetrics
|
|
@@ -392,18 +475,31 @@ report = ActiveReporting::Report.new(:a_stored_metric, ...)
|
|
|
392
475
|
|
|
393
476
|
## Development
|
|
394
477
|
|
|
395
|
-
After checking out the repo, run `bin/setup` to install dependencies
|
|
478
|
+
After checking out the repo, run `bin/setup` to install dependencies, then `bundle exec rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
479
|
+
|
|
480
|
+
Before opening a pull request, run `bundle exec rubocop` and, if you have [codespell](https://github.com/codespell-project/codespell) installed, `codespell`. CI runs both.
|
|
396
481
|
|
|
397
482
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
398
483
|
|
|
399
484
|
## Testing
|
|
400
485
|
|
|
401
|
-
|
|
402
|
-
|
|
486
|
+
The tests run against an in-memory SQLite database by default. To test against another database, set `DB` to `pg`, `mysql` (the `mysql2` adapter), or `trilogy`, and run `bin/setup` once for it:
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
DB=pg bin/setup
|
|
490
|
+
DB=pg bundle exec rake test
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
`bin/setup` installs the database's adapter gem (each one is in an optional Gemfile group, so you only need the client libraries for databases you test against) and creates the `active_reporting_test` database. It needs the database server to be running. Connection settings come from these environment variables:
|
|
494
|
+
|
|
495
|
+
- PostgreSQL: `POSTGRES_HOST`, `POSTGRES_USER`, `POSTGRES_PASSWORD` (plus the standard `PG*` variables, such as `PGPORT`)
|
|
496
|
+
- MySQL: `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`
|
|
497
|
+
|
|
498
|
+
Set `RAILS` (e.g. `RAILS=8.0`) when installing and running to test against a specific Rails version.
|
|
403
499
|
|
|
404
500
|
## Contributing
|
|
405
501
|
|
|
406
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
502
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/t27duck/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.
|
|
407
503
|
|
|
408
504
|
## License
|
|
409
505
|
|
|
@@ -5,19 +5,11 @@ module ActiveReporting
|
|
|
5
5
|
module ActiveRecordAdaptor
|
|
6
6
|
# Returns the ActiveReporting::FactModel related to the model.
|
|
7
7
|
#
|
|
8
|
-
# If
|
|
9
|
-
# inherits from ActiveReporting::Factmodel named [MyModel]FactModel
|
|
8
|
+
# If a [MyModel]FactModel class is not defined, a fact model is generated for the model.
|
|
10
9
|
#
|
|
11
10
|
# @return [ActiveReporting::FactModel]
|
|
12
11
|
def fact_model
|
|
13
|
-
|
|
14
|
-
@fact_model ||= begin
|
|
15
|
-
const_name.constantize
|
|
16
|
-
rescue NameError
|
|
17
|
-
const = Object.const_set(const_name, Class.new(ActiveReporting::FactModel))
|
|
18
|
-
const.model = self
|
|
19
|
-
const
|
|
20
|
-
end
|
|
12
|
+
FactModelRegistry.fetch(self)
|
|
21
13
|
end
|
|
22
14
|
end
|
|
23
15
|
end
|