active_reporting 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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: []