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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07ad5ad6e21234d6441a8e97908cf854406d30e2a77b741096d0c874c85d5c42
4
- data.tar.gz: 982a82ba6ab68d81dee8a1db785f25d993ac14b2572e698923437a4c1a37bd1e
3
+ metadata.gz: 63bda6ce4591fc9695b29db8e53aee19966e3630e9c8b059c686678043f20938
4
+ data.tar.gz: e780a77254b8c8b1cc23ecc732146d581def733fef8669fc3a0419e32fabf5df
5
5
  SHA512:
6
- metadata.gz: 0f7a37f152776e1d9fbc2440b71cb045464063f759c88cfff5d6dc0302ab8e9f6ea7aeed4c35375b5ad25861132dfb1ba4ebad57da6051ba75680beb309c3665
7
- data.tar.gz: 0c07b8989e32d26ca39105630ffe74b83207425449d412f344461c194926e0efed64da367449898295de62a3618910f7a249e94ee91efe0290b3332d1b820319
6
+ metadata.gz: 89718eab7d4398d77f9d19de7e7160e52feebfc295e03a7c05a113a87159e5e3640b599d681559c53e749bf68bdcee477effff6b4b605c4dcf317da6c09d2cdd
7
+ data.tar.gz: 4d999fe0374b28e826edbb6f6eca9fc9f8ab9ae7aff1056aa88757079e00847f8361862108ba65227f530b72fa918e849dd3bbfe1b337156a685dd4f26b706d8
@@ -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
- ### Implicit hierarchies with datetime columns (PostgreSQL support only)
226
+ ### Drill down / Roll up (Drill up) with datetime columns
223
227
 
224
- The fastest approach to group by certain date metrics is to create so-called "date dimensions". For
225
- those Postgres users that are restricted from organizing their data in this way, Postgres provides
226
- a way to group by `datetime` column data on the fly using the `date_trunc` function.
228
+ The fastest approach to group by certain date metrics is to create so-called "date dimensions" and add on columns for each desired hierarchy. For those users that are restricted from organizing their data in this way, ActiveRporting provides a `datetime_drill` option that can be passed with the dimension on the metric definition to drill datetime columns.
227
229
 
228
230
  To use, declare a datetime dimension on a fact model as normal:
229
231
 
@@ -233,7 +235,23 @@ class UserFactModel < ActiveReporting::FactModel
233
235
  end
234
236
  ```
235
237
 
236
- When creating a metric, ActiveReporting will recognize implicit hierarchies for this dimension. The hierarchies correspond to the [values](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC) supported by PostgreSQL. (See example under the metric section, below.)
238
+ When creating a metric, ActiveReporting will recognize the following datetime hierarchies: (See example under the metric section, below.)
239
+
240
+ - microseconds
241
+ - milliseconds
242
+ - second
243
+ - minute
244
+ - hour
245
+ - day
246
+ - week
247
+ - month
248
+ - quarter
249
+ - year
250
+ - decade
251
+ - century
252
+ - millennium
253
+
254
+ Under the hood Active Reporting uses specific database functions to manipulate datetime columns. Postgres provides a way to group by `datetime` column data on the fly using the [`date_trunc` function](https://www.postgresql.org/docs/8.1/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC). On Mysql this can be done using [Date and Time Functions](https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html).
237
255
 
238
256
  *NOTE*: PRs welcomed to support this functionality in other databases.
239
257
 
@@ -285,7 +303,17 @@ my_metric = ActiveReporting::Metric.new(
285
303
 
286
304
  `aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
287
305
 
288
- `dimensions` - An array of dimensions used for the metric. When given just a symbol, the default dimension label will be used for the dimension. You may specify a hierarchy level by using a hash. (Examples: `[:sales_rep, {sale_date: :month}]`). In hierarchies you can customize the label in this way: `[{sale_date: { field: :month, name: :a_custom_name_for_month }}]`. If you use a hash instead of a Symbol to define a hierarchy the `field` item must be a valid field in your table. The `name` can be whatever label you want. You can 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 }}]`
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[dimension.name.to_s] = callback.call(hash[dimension.name.to_s])
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?, :datetime?
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, label = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
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 '#{dim}' not found on fact model '#{fact_model}'") if found_dimension.nil?
23
-
24
- new(found_dimension, **label_config(label))
22
+ raise(UnknownDimension, "Dimension '#{dimension_name}' not found on fact model '#{fact_model}'") if found_dimension.nil?
23
+
24
+ # Ambiguous behavior with string option for degenerate and standard dimension
25
+ if !options.is_a?(Hash) && found_dimension.type == Dimension::TYPES[:degenerate]
26
+ ActiveSupport::Deprecation.warn <<~EOS
27
+ direct use of implict hierarchies is deprecated and will be removed in future versions. \
28
+ Please use `:datetime_drill` option instead.
29
+ EOS
30
+ options = { datetime_drill: options }
31
+ end
32
+ new(found_dimension, **label_config(options))
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] label
33
- def self.label_config(label)
34
- return { label: label } unless label.is_a?(Hash)
40
+ # @param [Symbol|Hash] options
41
+ def self.label_config(options)
42
+ unless options.is_a?(Hash)
43
+ return { label: options }
44
+ end
35
45
 
36
46
  {
37
- label: label[:field],
38
- label_name: label[:name],
39
- join_method: label[: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
- return [degenerate_select_fragment] if type == Dimension::TYPES[:degenerate]
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
- @label_name = label_name ? "#{name}_#{label_name}" : name
120
+
121
+ if label_name
122
+ @label_name = label_name
123
+ else
124
+ @label_name = name
125
+ @label_name += "_#{@label}" if (type == Dimension::TYPES[:standard] && @label != :name)
126
+ @label_name += "_#{@datetime_drill}" if @datetime_drill
127
+ end
128
+ @label_name
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
- if datetime?
126
- validate_supported_database_for_datetime_hierarchies
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 degenerate_fragment
158
- return "#{name}_#{@label}" if datetime?
159
- "#{model.quoted_table_name}.#{name}"
182
+ def datetime_drill_label_fragment(column)
183
+ if model.connection.adapter_name == "Mysql2"
184
+ datetime_drill_mysql(column)
185
+ else # Postgress
186
+ datetime_drill_postgress(column)
187
+ end
160
188
  end
161
189
 
162
- def degenerate_select_fragment
163
- return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
164
- "#{model.quoted_table_name}.#{name}"
190
+ def datetime_drill_postgress(column)
191
+ "DATE_TRUNC('#{@datetime_drill}', #{column})"
192
+ end
193
+
194
+ def datetime_drill_mysql(column)
195
+ case @datetime_drill.to_sym
196
+ when :microseconds
197
+ "MICROSECOND(#{column})"
198
+ when :milliseconds
199
+ "MICROSECOND(#{column}) DIV 1000"
200
+ when :second
201
+ "SECOND(#{column})"
202
+ when :minute
203
+ "MINUTE(#{column})"
204
+ when :hour
205
+ "HOUR(#{column})"
206
+ when :day
207
+ "DAY(#{column})"
208
+ when :week
209
+ "WEEKDAY(#{column})"
210
+ when :month
211
+ "MONTH(#{column})"
212
+ when :quarter
213
+ "QUARTER(#{column})"
214
+ when :year
215
+ "YEAR(#{column})"
216
+ when :decade
217
+ "YEAR(#{column}) DIV 10"
218
+ when :century
219
+ "YEAR(#{column}) DIV 100"
220
+ when :millennium
221
+ "YEAR(#{column}) DIV 1000"
222
+ end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveReporting
4
- VERSION = '0.5.1'
4
+ VERSION = '0.6.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_reporting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
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-07-31 00:00:00.000000000 Z
11
+ date: 2020-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -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: []