active_reporting 0.2.0 → 0.5.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
- SHA1:
3
- metadata.gz: a42a126d95b11825c31edc43efd23fb483238266
4
- data.tar.gz: 1679cf35f25aeee3f40117ffce4d4b4fdf8be499
2
+ SHA256:
3
+ metadata.gz: 1bbb1cca5da777e98db50e52e0dd97a44e20cd9844c8d4d88cbb739485a7f22f
4
+ data.tar.gz: f3d5fcc3f6e174e6eb6ca582c57add3b844cc0deb632e6f844ee9990db31c93f
5
5
  SHA512:
6
- metadata.gz: 2eb0fba1d05b5f482716db8cd47cf165546c35533e061a46d55abc8708359b3ba3f0812db60d68b85cf1375554f25037a6e83ff231db1e988d4b6014e2198431
7
- data.tar.gz: d2d2be2376f03a9590142bd732369426510c7f0916bc432f42e82c14e4dcff406a726a8de53ae2a4d7774b8f39e93c3485c54236eafd244dcf8a022a3494f609
6
+ metadata.gz: b57b0e82ebb5d1d645383d3aa88551b43c999c1443d0ecac6544d83295d6d1745f2956696499a5ce3ff249f8d91251e298a77e3921a68722d6842ffb7e90e705
7
+ data.tar.gz: 3fcec68b2de1a822ecef745f7b75ca6aaa7ae9587d7a25ce5ae5234a26e1d083bab01da4ccd70bafd47a9382810fb0932cb287ca7b18d20916789f4e0809b045
data/.gitignore CHANGED
@@ -6,5 +6,7 @@
6
6
  /doc/
7
7
  /pkg/
8
8
  /spec/reports/
9
+ .byebug_history
10
+ .ruby-version
9
11
  /tmp/
10
12
  .DS_STORE
@@ -1,7 +1,11 @@
1
1
  AllCops:
2
2
  Exclude:
3
3
  - test/**/*
4
- TargetRubyVersion: 2.2
4
+ - Gemfile
5
+ TargetRubyVersion: 2.3
6
+
7
+ Metrics/ClassLength:
8
+ Max: 150
5
9
 
6
10
  Metrics/LineLength:
7
11
  Max: 120
@@ -18,7 +22,7 @@ Style/ClassAndModuleChildren:
18
22
  Style/Documentation:
19
23
  Enabled: false
20
24
 
21
- Style/PredicateName:
25
+ Style/Name:
22
26
  Enabled: false
23
27
 
24
28
  Style/RaiseArgs:
@@ -1,19 +1,20 @@
1
1
  sudo: false
2
2
  language: ruby
3
+ services:
4
+ - mysql
5
+ addons:
6
+ postgresql: "9.6"
3
7
  rvm:
4
- - 2.2.7
5
- - 2.3.4
6
- - 2.4.1
8
+ - 2.5.8
9
+ - 2.6.6
10
+ - 2.7.1
7
11
  env:
8
- - RAILS=5-1 DB=sqlite
9
- - RAILS=5-1 DB=pg
10
- - RAILS=5-1 DB=mysql
11
- - RAILS=5-0 DB=sqlite
12
- - RAILS=5-0 DB=pg
13
- - RAILS=5-0 DB=mysql
14
- - RAILS=4-2 DB=sqlite
15
- - RAILS=4-2 DB=pg
16
- - RAILS=4-2 DB=mysql
12
+ - RAILS=6-0 DB=sqlite
13
+ - RAILS=6-0 DB=pg
14
+ - RAILS=6-0 DB=mysql
15
+ - RAILS=5-2 DB=sqlite
16
+ - RAILS=5-2 DB=pg
17
+ - RAILS=5-2 DB=mysql
17
18
  before_script:
18
19
  - psql -c 'create database active_reporting_test;' -U postgres
19
20
  - mysql -e 'create database active_reporting_test collate utf8_general_ci;'
@@ -1,3 +1,57 @@
1
+ ## 0.5.0 (2020-06-30)
2
+
3
+ ### Bug Fixes
4
+
5
+ * Fix Missing quotation marks in column names causing SLQ errors on MYSQL - *germanotm*
6
+
7
+ ### Misc
8
+
9
+ * Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
10
+
11
+ ## 0.4.2 (2019-11-01)
12
+
13
+ ### Misc
14
+
15
+ * Test against Rails 6.0 final
16
+ * Fixed deprecated call to `to_hash` - *joshforbes*
17
+ * Corrected readme entry for `dimesions` option for `ActiveReporting::Metric` - *joshforbes*
18
+
19
+ ## 0.4.1 (2019-05-28)
20
+
21
+ ### Features
22
+
23
+ * Hierarchical dimensions may now have custom keys in result (#16) - *andresgutgon*
24
+
25
+ ### Misc
26
+
27
+ * Test against Raisl 6.0RC
28
+ * Loosen AR requirements. The gem will install for any AR version, but only ones listed in the README are supported
29
+ * Test against active Rubies
30
+
31
+ ## 0.4.0 (2018-05-02)
32
+
33
+ ### Breaking Changes
34
+
35
+ * Gemspec now requires Ruby 2.3 and later to install
36
+
37
+ ### Features
38
+
39
+ * Dimension off of `datetime` columns by date parts in PostgreSQL (See README for details) (#10) - *niborg*
40
+
41
+ ## 0.3.0 (2018-04-12)
42
+
43
+ ### Bug Fixes
44
+
45
+ * Specify rescue from LoadError for ransack (#9) - *niborg*
46
+ * Fix ransack fallback logic (#8) - *germanotm*
47
+
48
+ ### Misc
49
+
50
+ * Test against Rails 5.2
51
+ * Test against Ruby 2.5
52
+ * Drop support for Rails 5.0 (EOL-ed)
53
+ * Drop support for Ruby 2.2 (EOL-ed)
54
+
1
55
  ## 0.2.0 (2017-06-17)
2
56
 
3
57
  ### Breaking Changes
@@ -6,11 +60,11 @@
6
60
 
7
61
  ### Bug Fixes
8
62
 
9
- * `metric` lives on fact model and not metric (#3) - *Michael Wheeler (wheeyls)*
63
+ * `metric` lives on fact model and not metric (#3) - *wheeyls*
10
64
 
11
65
  ### Misc
12
66
 
13
- * Readme corrections and updates (#2) - *Michael Wheeler (wheeyls)*
67
+ * Readme corrections and updates (#2) - *wheeyls*
14
68
 
15
69
  ## 0.1.1 (2017-04-22)
16
70
 
data/Gemfile CHANGED
@@ -4,13 +4,12 @@ gemspec
4
4
 
5
5
  gem 'simplecov', require: false
6
6
 
7
- rails = ENV['RAILS'] || '5-1'
7
+ rails = ENV['RAILS'] || '5-2'
8
+ db = ENV['DB'] || 'sqlite'
8
9
 
9
10
  case rails
10
- when '4-2'
11
- gem 'activerecord', '~> 4.2.0'
12
- when '5-0'
13
- gem 'activerecord', '~> 5.0.0'
14
- else
15
- gem 'activerecord', '~> 5.1.0'
11
+ when '5-2'
12
+ gem 'activerecord', '~> 5.2.0'
13
+ when '6-0'
14
+ gem 'activerecord', '6.0.0'
16
15
  end
data/README.md CHANGED
@@ -8,6 +8,10 @@ ActiveReporting implements various terminology used in Relational Online Analyti
8
8
 
9
9
  ActiveReporting officially supports MySQL, PostgreSQL, and SQLite.
10
10
 
11
+ ActiveReporting officially supports Ruby 2.5 and later. Other versions may work, but are not supported.
12
+
13
+ ActiveReporting officially supports Rails 5.2, and 6.0. Other versions may work, but are not supported.
14
+
11
15
  ## Installation
12
16
 
13
17
  Add this line to your application's Gemfile:
@@ -45,8 +49,8 @@ Rails: ActiveRecord model
45
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.
46
50
 
47
51
  Examples:
48
- * A sales rep on a fact table of orders
49
- * A state of an order on a state machine
52
+ * A sales rep on a fact table of sales
53
+ * A state of an sale on a state machine
50
54
  * The manufacture on a fact table of widgets
51
55
 
52
56
  SQL Equivalent: JOIN, GROUP BY
@@ -174,7 +178,7 @@ end
174
178
  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.
175
179
 
176
180
  ```ruby
177
- class OrderFactModel < ActiveReporting::FactModel
181
+ class SaleFactModel < ActiveReporting::FactModel
178
182
  self.measure = :total
179
183
  end
180
184
  ```
@@ -215,6 +219,24 @@ class PhoneFactModel < ActiveReporting::FactModel
215
219
  end
216
220
  ```
217
221
 
222
+ ### Implicit hierarchies with datetime columns (PostgreSQL support only)
223
+
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.
227
+
228
+ To use, declare a datetime dimension on a fact model as normal:
229
+
230
+ ```ruby
231
+ class UserFactModel < ActiveReporting::FactModel
232
+ dimension :created_at
233
+ end
234
+ ```
235
+
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.)
237
+
238
+ *NOTE*: PRs welcomed to support this functionality in other databases.
239
+
218
240
  ## Configuring Dimension Filters
219
241
 
220
242
  A dimension filter provides filtering for a report. In SQL-land, this is the `WHERE` clause.
@@ -251,8 +273,8 @@ A `Metric` is the basic building block used to describe a question you want to a
251
273
 
252
274
  ```ruby
253
275
  my_metric = ActiveReporting::Metric.new(
254
- :order_total,
255
- fact_model: OrderFactModel,
276
+ :sale_total,
277
+ fact_model: SaleFactModel,
256
278
  aggregate: :sum
257
279
  )
258
280
  ```
@@ -263,7 +285,7 @@ my_metric = ActiveReporting::Metric.new(
263
285
 
264
286
  `aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
265
287
 
266
- `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, {order_date: :month}]`)
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.
267
289
 
268
290
  `dimension_filter` - A hash were the keys are dimension filter names and the values are the values passed into the filter.
269
291
 
@@ -271,26 +293,40 @@ my_metric = ActiveReporting::Metric.new(
271
293
 
272
294
  `order_by_dimension` - Allows you to set the ordering of the results based on a dimension label. (Examples: `{author: :desc}`, `{sales_ref: :asc}`)
273
295
 
296
+ For those using Postgres, you can take advantage of implicit hierarchies in `datetime` columns, as mentioned above:
297
+
298
+ ```ruby
299
+ class UserFactModel < ActiveReporting::FactModel
300
+ dimension :created_at
301
+ end
302
+
303
+ my_metric = ActiveReporting::Metric.new(
304
+ :my_total,
305
+ fact_model: UserFactModel,
306
+ dimensions: [{ created_at: :quarter } ]
307
+ )
308
+ ```
309
+
274
310
  ## ActiveReporting::Report
275
311
 
276
312
  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 an simple array of hashing.
277
313
 
278
314
  ```ruby
279
315
  metric = ActiveReporting::Metric.new(
280
- :order_count,
281
- fact_model: OrderFactModel,
282
- dimension: [:sales_rep],
316
+ :sale_count,
317
+ fact_model: SaleFactModel,
318
+ dimensions: [:sales_rep],
283
319
  dimension_filter: {months_ago: 1}
284
320
  )
285
321
 
286
322
  report = ActiveReporting::Report.new(metric)
287
323
  report.run
288
- => [{order_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{order_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
324
+ => [{sale_count: 12, sales_rep: 'Fred Jones', sales_rep_identifier: 123},{sale_count: 17, sales_rep: 'Mary Sue', sales_rep_identifier: 123}]
289
325
  ```
290
326
 
291
327
  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`.
292
328
 
293
- `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 orders dimensioned by sales rep, the rep's IDs from the `sales_reps` table will be included. (Default `true`)
329
+ `dimension_identifiers` - When true, the result will include the database identifier columns of the dimensions. For example, when running a report for the total number of sales dimensioned by sales rep, the rep's IDs from the `sales_reps` table will be included. (Default `true`)
294
330
 
295
331
  `dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
296
332
 
@@ -300,15 +336,15 @@ A `Report` may also take additional arguments to merge with the `Metric`'s infor
300
336
 
301
337
  ```ruby
302
338
  metric = ActiveReporting::Metric.new(
303
- :order_count,
304
- fact_model: OrderFactModel,
305
- dimension: [:sales_rep],
339
+ :sale_count,
340
+ fact_model: SaleFactModel,
341
+ dimensions: [:sales_rep],
306
342
  dimension_filter: {months_ago: 1}
307
343
  )
308
344
 
309
- report = ActiveReporting.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
345
+ report = ActiveReporting::Report.new(metric, dimension_filter: {from_region: 'North'}, dimension_identifiers: false)
310
346
  report.run
311
- => [{order_count: 17, sales_rep: 'Mary Sue'}]
347
+ => [{sale_count: 17, sales_rep: 'Mary Sue'}]
312
348
  ```
313
349
 
314
350
  It may be more DRY to store ready-made metrics in a database table or stored in memory to use as the bases for various reports. You can pass a string or symbol into a `Report` instead of a `Metric` to look up an pre-made metric. This is done by passing the symbol or string into the `lookup` class method on the constant defined in `ActiveReporting::Configuration.metric_lookup_class`.
@@ -331,6 +367,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
331
367
 
332
368
  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).
333
369
 
370
+ ## Testing
371
+
372
+ You can run the test suite using `rake test`. To test against a particular database, you'll need to set the
373
+ appropriate `DB` environment variable, e.g. `DB=pg rake test`.
374
+
334
375
  ## Contributing
335
376
 
336
377
  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.
@@ -339,4 +380,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
339
380
  ## License
340
381
 
341
382
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
342
-
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
 
@@ -1,5 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'active_reporting/version'
5
6
 
@@ -21,16 +22,16 @@ Gem::Specification.new do |spec|
21
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
23
  spec.require_paths = ['lib']
23
24
 
24
- spec.required_ruby_version = '>= 2.2'
25
+ spec.required_ruby_version = '>= 2.3'
25
26
 
26
- spec.add_dependency 'activerecord', '>= 4.2.0'
27
- spec.add_dependency 'activesupport', '>= 4.2.0'
27
+ spec.add_dependency 'activerecord'
28
+ spec.add_dependency 'activesupport'
28
29
 
29
30
  spec.add_development_dependency 'bundler'
30
- spec.add_development_dependency 'rake', '~> 10.0'
31
31
  spec.add_development_dependency 'minitest', '~> 5.0'
32
+ spec.add_development_dependency 'mysql2'
33
+ spec.add_development_dependency 'pg'
34
+ spec.add_development_dependency 'rake'
32
35
  spec.add_development_dependency 'ransack'
33
36
  spec.add_development_dependency 'sqlite3'
34
- spec.add_development_dependency 'pg'
35
- spec.add_development_dependency 'mysql2'
36
37
  end
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'active_reporting'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'active_reporting/active_record_adaptor'
3
5
  require 'active_reporting/configuration'
@@ -12,7 +14,7 @@ require 'active_reporting/version'
12
14
  begin
13
15
  require 'ransack'
14
16
  ActiveReporting::Configuration.ransack_available = true
15
- rescue
17
+ rescue LoadError, StandardError
16
18
  ActiveReporting::Configuration.ransack_available = false
17
19
  end
18
20
 
@@ -23,8 +25,8 @@ module ActiveReporting
23
25
  klass = Configuration.metric_lookup_class
24
26
  unless defined?(klass.constantize)
25
27
  raise BadMetricLookupClass,
26
- "#{klass} not defined. Please define a class responsible for looking up a metric by name." +
27
- " You may define your own class and set it with `ActiveReporting::Configuration.metric_lookup_class=`."
28
+ "#{klass} not defined. Please define a class responsible for looking up a metric by name." \
29
+ ' You may define your own class and set it with `ActiveReporting::Configuration.metric_lookup_class=`.'
28
30
  end
29
31
  unless klass.constantize.respond_to?(:lookup)
30
32
  raise BadMetricLookupClass, "#{klass} needs to define a class method called 'lookup'"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  # This is included into every class that inherits from ActiveRecord::Base
3
5
  module ActiveRecordAdaptor
@@ -13,7 +15,7 @@ module ActiveReporting
13
15
  const_name.constantize
14
16
  rescue NameError
15
17
  const = Object.const_set(const_name, Class.new(ActiveReporting::FactModel))
16
- const.model= self
18
+ const.model = self
17
19
  const
18
20
  end
19
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  module Configuration
3
5
  class << self
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  class Dimension
5
+ TYPES = { degenerate: :degenerate, standard: :standard }.freeze
3
6
  attr_reader :name
4
7
 
5
8
  # @param model [ActiveRecord::Base]
@@ -19,14 +22,21 @@ module ActiveReporting
19
22
  # @return [Symbol]
20
23
  def type
21
24
  @type ||= if model.column_names.include?(@name)
22
- :degenerate
25
+ TYPES[:degenerate]
23
26
  elsif association
24
- :standard
27
+ TYPES[:standard]
25
28
  else
26
29
  raise UnknownDimension, "Dimension '#{@name}' not found on fact model '#{@fact_model}'"
27
30
  end
28
31
  end
29
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
+
30
40
  # Tells if the dimension is hierarchical
31
41
  #
32
42
  # @return [Boolean]
@@ -53,7 +63,7 @@ module ActiveReporting
53
63
  #
54
64
  # @return [ActiveRecord::Reflection]
55
65
  def association
56
- @association_info ||= model.reflect_on_association(@name)
66
+ @association ||= model.reflect_on_association(@name)
57
67
  end
58
68
  end
59
69
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  class DimensionFilter
3
- attr_reader :type, :body
5
+ attr_reader :name, :type, :body
4
6
 
5
7
  # Factory for creating a new DimensionFilter
6
8
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  class FactModel
3
5
  class << self
@@ -109,7 +111,7 @@ module ActiveReporting
109
111
  # @param body [Lambda]
110
112
  def self.dimension_label_callback(column, body)
111
113
  @dimension_label_callbacks ||= {}
112
- raise ArgumentError, "Dimension label callback body must be a callable object" unless body.respond_to?(:call)
114
+ raise ArgumentError, 'Dimension label callback body must be a callable object' unless body.respond_to?(:call)
113
115
  @dimension_label_callbacks[column.to_sym] = body
114
116
  end
115
117
 
@@ -143,7 +145,9 @@ module ActiveReporting
143
145
  # Invoke this method to make all dimension filters fallback to use ransack
144
146
  # if they are not defined as scopes on the model
145
147
  def self.use_ransack_for_unknown_dimension_filters
146
- raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.' unless ransack_available
148
+ unless Configuration.ransack_available
149
+ raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.'
150
+ end
147
151
  @ransack_fallback = true
148
152
  end
149
153
 
@@ -163,7 +167,7 @@ module ActiveReporting
163
167
  @dimension_filters ||= {}
164
168
  dm = @dimension_filters[name.to_sym]
165
169
  return dm if dm.present?
166
- return @dimension_filters[name.to_sym] = DimensionFilter.build(self, name, :ransack) if ransack_fallback
170
+ return @dimension_filters[name.to_sym] = DimensionFilter.build(name, :ransack) if ransack_fallback
167
171
  raise UnknownDimensionFilter, "Dimension filter '#{name}' not found on fact model '#{self.name}'"
168
172
  end
169
173
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  module ActiveReporting
3
- AGGREGATES = %i(count sum max min avg).freeze
5
+ AGGREGATES = %i[count sum max min avg].freeze
4
6
 
5
7
  class Metric
6
8
  extend Forwardable
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  module ActiveReporting
3
5
  class Report
@@ -34,7 +36,7 @@ module ActiveReporting
34
36
  private ######################################################################
35
37
 
36
38
  def build_data
37
- @data = model.connection.exec_query(statement.to_sql).to_hash
39
+ @data = model.connection.exec_query(statement.to_sql).to_a
38
40
  apply_dimension_callbacks
39
41
  @data
40
42
  end
@@ -1,23 +1,49 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  module ActiveReporting
3
5
  class ReportingDimension
4
6
  extend Forwardable
5
- def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?
7
+ SUPPORTED_DBS = %w[PostgreSQL PostGIS].freeze
8
+ # Values for the Postgres `date_trunc` method.
9
+ # See https://www.postgresql.org/docs/10/static/functions-datetime.html#FUNCTIONS-DATETIME-TRUNC
10
+ DATETIME_HIERARCHIES = %i[microseconds milliseconds second minute hour day week month quarter year decade
11
+ century millennium].freeze
12
+ def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?, :datetime?
6
13
 
7
14
  def self.build_from_dimensions(fact_model, dimensions)
8
15
  Array(dimensions).map do |dim|
9
16
  dimension_name, label = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
10
17
  found_dimension = fact_model.dimensions[dimension_name.to_sym]
11
- if found_dimension.nil?
12
- raise UnknownDimension, "Dimension '#{dim}' not found on fact model '#{fact_model}'"
13
- end
14
- new(found_dimension, label: label)
18
+
19
+ raise(UnknownDimension, "Dimension '#{dim}' not found on fact model '#{fact_model}'") if found_dimension.nil?
20
+
21
+ new(found_dimension, label_config(label))
15
22
  end
16
23
  end
17
24
 
18
- def initialize(dimension, label: nil)
25
+ # If you pass a symbol it means you just indicate
26
+ # the field on that dimension. With a hash you can
27
+ # customize the name of the label
28
+ #
29
+ # @param [Symbol|Hash] label
30
+ def self.label_config(label)
31
+ return { label: label } unless label.is_a?(Hash)
32
+
33
+ {
34
+ label: label[:field],
35
+ label_name: label[:name]
36
+ }
37
+ end
38
+
39
+ # @param dimension [ActiveReporting::Dimension]
40
+ # @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
41
+ # @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
42
+ def initialize(dimension, label: nil, label_name: nil)
19
43
  @dimension = dimension
20
- determine_label(label)
44
+
45
+ determine_label_field(label)
46
+ determine_label_name(label_name)
21
47
  end
22
48
 
23
49
  # The foreign key to use in queries
@@ -31,10 +57,10 @@ module ActiveReporting
31
57
  #
32
58
  # @return [Array]
33
59
  def select_statement(with_identifier: true)
34
- return [degenerate_fragment] if type == :degenerate
60
+ return [degenerate_select_fragment] if type == Dimension::TYPES[:degenerate]
35
61
 
36
- ss = ["#{label_fragment} AS #{name}"]
37
- ss << "#{identifier_fragment} AS #{name}_identifier" if with_identifier
62
+ ss = ["#{label_fragment} AS #{model.connection.quote_column_name(@label_name)}"]
63
+ ss << "#{identifier_fragment} AS #{model.connection.quote_column_name("#{name}_identifier")}" if with_identifier
38
64
  ss
39
65
  end
40
66
 
@@ -42,7 +68,7 @@ module ActiveReporting
42
68
  #
43
69
  # @return [Array]
44
70
  def group_by_statement(with_identifier: true)
45
- return [degenerate_fragment] if type == :degenerate
71
+ return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
46
72
 
47
73
  group = [label_fragment]
48
74
  group << identifier_fragment if with_identifier
@@ -54,8 +80,8 @@ module ActiveReporting
54
80
  # @return [String]
55
81
  def order_by_statement(direction:)
56
82
  direction = direction.to_s.upcase
57
- raise "Ording direction should be 'asc' or 'desc'" unless %w(ASC DESC).include?(direction)
58
- return "#{degenerate_fragment} #{direction}" if type == :degenerate
83
+ raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
84
+ return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
59
85
  "#{label_fragment} #{direction}"
60
86
  end
61
87
 
@@ -68,31 +94,67 @@ module ActiveReporting
68
94
 
69
95
  private ####################################################################
70
96
 
71
- def determine_label(label)
72
- @label = label.to_sym if label.present? && validate_hierarchical_label(label)
73
- @label ||= dimension_fact_model.dimension_label || Configuration.default_dimension_label
97
+ def determine_label_field(label_field)
98
+ @label = if label_field.present? && validate_hierarchical_label(label_field)
99
+ label_field.to_sym
100
+ else
101
+ dimension_fact_model.dimension_label || Configuration.default_dimension_label
102
+ end
103
+ end
104
+
105
+ def determine_label_name(label_name)
106
+ @label_name = label_name ? "#{name}_#{label_name}" : name
74
107
  end
75
108
 
76
109
  def validate_hierarchical_label(hierarchical_label)
77
- if !hierarchical?
78
- raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
79
- end
80
- unless dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
81
- raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
110
+ if datetime?
111
+ validate_supported_database_for_datetime_hierarchies
112
+ validate_against_datetime_hierarchies(hierarchical_label)
113
+ else
114
+ validate_dimension_is_hierachical(hierarchical_label)
115
+ validate_against_fact_model_properties(hierarchical_label)
82
116
  end
83
117
  true
84
118
  end
85
119
 
120
+ def validate_dimension_is_hierachical(hierarchical_label)
121
+ return if hierarchical?
122
+ raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
123
+ end
124
+
125
+ def validate_supported_database_for_datetime_hierarchies
126
+ return if SUPPORTED_DBS.include?(model.connection.adapter_name)
127
+ raise InvalidDimensionLabel,
128
+ "Cannot utilize datetime grouping for #{name}; " \
129
+ "database #{model.connection.adapter_name} is not supported"
130
+ end
131
+
132
+ def validate_against_datetime_hierarchies(hierarchical_label)
133
+ return if DATETIME_HIERARCHIES.include?(hierarchical_label.to_sym)
134
+ raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
135
+ end
136
+
137
+ def validate_against_fact_model_properties(hierarchical_label)
138
+ return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
139
+ raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
140
+ end
141
+
86
142
  def degenerate_fragment
143
+ return "#{name}_#{@label}" if datetime?
144
+ "#{model.quoted_table_name}.#{name}"
145
+ end
146
+
147
+ def degenerate_select_fragment
148
+ return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
87
149
  "#{model.quoted_table_name}.#{name}"
88
150
  end
89
151
 
90
152
  def identifier_fragment
91
- "#{klass.quoted_table_name}.#{klass.primary_key}"
153
+ "#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
92
154
  end
93
155
 
94
156
  def label_fragment
95
- "#{klass.quoted_table_name}.#{@label}"
157
+ "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
96
158
  end
97
159
 
98
160
  def dimension_fact_model
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
- VERSION = '0.2.0'.freeze
4
+ VERSION = '0.5.0'
3
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.2.0
4
+ version: 0.5.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: 2017-06-17 00:00:00.000000000 Z
11
+ date: 2020-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 4.2.0
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 4.2.0
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 4.2.0
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 4.2.0
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,35 +53,35 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: rake
56
+ name: minitest
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: '5.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: '5.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: minitest
70
+ name: mysql2
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - "~>"
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '5.0'
75
+ version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - "~>"
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '5.0'
82
+ version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: ransack
84
+ name: pg
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -95,7 +95,7 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: sqlite3
98
+ name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: pg
112
+ name: ransack
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
@@ -123,7 +123,7 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: mysql2
126
+ name: sqlite3
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
@@ -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
@@ -178,16 +178,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
178
178
  requirements:
179
179
  - - ">="
180
180
  - !ruby/object:Gem::Version
181
- version: '2.2'
181
+ version: '2.3'
182
182
  required_rubygems_version: !ruby/object:Gem::Requirement
183
183
  requirements:
184
184
  - - ">="
185
185
  - !ruby/object:Gem::Version
186
186
  version: '0'
187
187
  requirements: []
188
- rubyforge_project:
189
- rubygems_version: 2.6.11
190
- signing_key:
188
+ rubygems_version: 3.0.3
189
+ signing_key:
191
190
  specification_version: 4
192
191
  summary: Add relational OLAP-like functionality for ActiveRecord
193
192
  test_files: []