active_reporting 0.5.1 → 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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +35 -8
- data/lib/active_reporting/dimension.rb +0 -7
- data/lib/active_reporting/report.rb +2 -1
- data/lib/active_reporting/reporting_dimension.rb +106 -38
- data/lib/active_reporting/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
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/CHANGELOG.md
CHANGED
@@ -1,3 +1,13 @@
|
|
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
|
+
|
1
11
|
## 0.5.1 (2020-06-31)
|
2
12
|
|
3
13
|
### Features
|
data/README.md
CHANGED
@@ -49,6 +49,7 @@ 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
|
+
|
52
53
|
* A sales rep on a fact table of sales
|
53
54
|
* A state of an sale on a state machine
|
54
55
|
* The manufacture on a fact table of widgets
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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).
|
@@ -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]
|
@@ -144,8 +144,9 @@ module ActiveReporting
|
|
144
144
|
@dimensions.each do |dimension|
|
145
145
|
callback = dimension.label_callback
|
146
146
|
next unless callback
|
147
|
+
key = "#{dimension.name}_#{dimension.label}"
|
147
148
|
@data.each do |hash|
|
148
|
-
hash[
|
149
|
+
hash[key] = callback.call(hash[key])
|
149
150
|
end
|
150
151
|
end
|
151
152
|
end
|
@@ -4,24 +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
11
|
century millennium].freeze
|
12
12
|
JOIN_METHODS = { joins: :joins, left_outer_joins: :left_outer_joins }.freeze
|
13
|
-
attr_reader :join_method
|
13
|
+
attr_reader :join_method, :label
|
14
14
|
|
15
|
-
def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical
|
15
|
+
def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?
|
16
16
|
|
17
17
|
def self.build_from_dimensions(fact_model, dimensions)
|
18
18
|
Array(dimensions).map do |dim|
|
19
|
-
dimension_name,
|
19
|
+
dimension_name, options = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
|
20
20
|
found_dimension = fact_model.dimensions[dimension_name.to_sym]
|
21
21
|
|
22
|
-
raise(UnknownDimension, "Dimension '#{
|
23
|
-
|
24
|
-
|
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))
|
25
33
|
end
|
26
34
|
end
|
27
35
|
|
@@ -29,24 +37,28 @@ module ActiveReporting
|
|
29
37
|
# the field on that dimension. With a hash you can
|
30
38
|
# customize the name of the label
|
31
39
|
#
|
32
|
-
# @param [Symbol|Hash]
|
33
|
-
def self.label_config(
|
34
|
-
|
40
|
+
# @param [Symbol|Hash] options
|
41
|
+
def self.label_config(options)
|
42
|
+
unless options.is_a?(Hash)
|
43
|
+
return { label: options }
|
44
|
+
end
|
35
45
|
|
36
46
|
{
|
37
|
-
label:
|
38
|
-
label_name:
|
39
|
-
join_method:
|
47
|
+
label: options[:field],
|
48
|
+
label_name: options[:name],
|
49
|
+
join_method: options[:join_method],
|
50
|
+
datetime_drill: options[:datetime_drill]
|
40
51
|
}
|
41
52
|
end
|
42
53
|
|
43
54
|
# @param dimension [ActiveReporting::Dimension]
|
44
55
|
# @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
|
45
56
|
# @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
|
46
|
-
def initialize(dimension, label: nil, label_name: nil, join_method: nil)
|
57
|
+
def initialize(dimension, label: nil, label_name: nil, join_method: nil, datetime_drill: nil)
|
47
58
|
@dimension = dimension
|
48
59
|
|
49
60
|
determine_label_field(label)
|
61
|
+
determine_datetime_drill(datetime_drill)
|
50
62
|
determine_label_name(label_name)
|
51
63
|
determine_join_method(join_method)
|
52
64
|
end
|
@@ -62,10 +74,8 @@ module ActiveReporting
|
|
62
74
|
#
|
63
75
|
# @return [Array]
|
64
76
|
def select_statement(with_identifier: true)
|
65
|
-
|
66
|
-
|
67
|
-
ss = ["#{label_fragment} AS #{model.connection.quote_column_name(@label_name)}"]
|
68
|
-
ss << "#{identifier_fragment} AS #{model.connection.quote_column_name("#{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]
|
69
79
|
ss
|
70
80
|
end
|
71
81
|
|
@@ -73,10 +83,8 @@ module ActiveReporting
|
|
73
83
|
#
|
74
84
|
# @return [Array]
|
75
85
|
def group_by_statement(with_identifier: true)
|
76
|
-
return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
|
77
|
-
|
78
86
|
group = [label_fragment]
|
79
|
-
group << identifier_fragment if with_identifier
|
87
|
+
group << identifier_fragment if with_identifier && type == Dimension::TYPES[:standard]
|
80
88
|
group
|
81
89
|
end
|
82
90
|
|
@@ -86,7 +94,6 @@ module ActiveReporting
|
|
86
94
|
def order_by_statement(direction:)
|
87
95
|
direction = direction.to_s.upcase
|
88
96
|
raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
|
89
|
-
return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
|
90
97
|
"#{label_fragment} #{direction}"
|
91
98
|
end
|
92
99
|
|
@@ -101,14 +108,32 @@ module ActiveReporting
|
|
101
108
|
|
102
109
|
def determine_label_field(label_field)
|
103
110
|
@label = if label_field.present? && validate_hierarchical_label(label_field)
|
104
|
-
label_field.to_sym
|
111
|
+
type == Dimension::TYPES[:degenerate] ? name : label_field.to_sym
|
112
|
+
elsif type == Dimension::TYPES[:degenerate]
|
113
|
+
name
|
105
114
|
else
|
106
115
|
dimension_fact_model.dimension_label || Configuration.default_dimension_label
|
107
116
|
end
|
108
117
|
end
|
109
118
|
|
110
119
|
def determine_label_name(label_name)
|
111
|
-
|
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
|
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
|
112
137
|
end
|
113
138
|
|
114
139
|
def determine_join_method(join_method)
|
@@ -122,13 +147,8 @@ module ActiveReporting
|
|
122
147
|
end
|
123
148
|
|
124
149
|
def validate_hierarchical_label(hierarchical_label)
|
125
|
-
|
126
|
-
|
127
|
-
validate_against_datetime_hierarchies(hierarchical_label)
|
128
|
-
else
|
129
|
-
validate_dimension_is_hierachical(hierarchical_label)
|
130
|
-
validate_against_fact_model_properties(hierarchical_label)
|
131
|
-
end
|
150
|
+
validate_dimension_is_hierachical(hierarchical_label)
|
151
|
+
validate_against_fact_model_properties(hierarchical_label)
|
132
152
|
true
|
133
153
|
end
|
134
154
|
|
@@ -149,27 +169,75 @@ module ActiveReporting
|
|
149
169
|
raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
|
150
170
|
end
|
151
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
|
+
|
152
177
|
def validate_against_fact_model_properties(hierarchical_label)
|
153
178
|
return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
|
154
179
|
raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
|
155
180
|
end
|
156
181
|
|
157
|
-
def
|
158
|
-
|
159
|
-
|
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
|
160
188
|
end
|
161
189
|
|
162
|
-
def
|
163
|
-
|
164
|
-
|
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
|
165
223
|
end
|
166
224
|
|
167
225
|
def identifier_fragment
|
168
226
|
"#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
|
169
227
|
end
|
170
228
|
|
229
|
+
def identifier_fragment_alias
|
230
|
+
"#{model.connection.quote_column_name("#{name}_identifier")}"
|
231
|
+
end
|
232
|
+
|
171
233
|
def label_fragment
|
172
|
-
"#{klass.quoted_table_name}.#{model.connection.quote_column_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)}"
|
173
241
|
end
|
174
242
|
|
175
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
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -136,7 +136,7 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
-
description:
|
139
|
+
description:
|
140
140
|
email:
|
141
141
|
- t27duck@gmail.com
|
142
142
|
executables: []
|
@@ -170,7 +170,7 @@ homepage: https://github.com/t27duck/active_reporting
|
|
170
170
|
licenses:
|
171
171
|
- MIT
|
172
172
|
metadata: {}
|
173
|
-
post_install_message:
|
173
|
+
post_install_message:
|
174
174
|
rdoc_options: []
|
175
175
|
require_paths:
|
176
176
|
- lib
|
@@ -186,7 +186,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
186
186
|
version: '0'
|
187
187
|
requirements: []
|
188
188
|
rubygems_version: 3.0.3
|
189
|
-
signing_key:
|
189
|
+
signing_key:
|
190
190
|
specification_version: 4
|
191
191
|
summary: Add relational OLAP-like functionality for ActiveRecord
|
192
192
|
test_files: []
|