active_reporting 0.3.0 → 0.5.1

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
- SHA1:
3
- metadata.gz: b787d85e5e44d1efda8c727cc6b289674bbebb35
4
- data.tar.gz: 567f54e1ea4471e679f827c8819c220a1d7dca28
2
+ SHA256:
3
+ metadata.gz: 07ad5ad6e21234d6441a8e97908cf854406d30e2a77b741096d0c874c85d5c42
4
+ data.tar.gz: 982a82ba6ab68d81dee8a1db785f25d993ac14b2572e698923437a4c1a37bd1e
5
5
  SHA512:
6
- metadata.gz: a89a8bb2e06495ff3fb14f9f5b6356840c09298dcda898433d057b658481e73aedf362ec2b4ccba3b07f7a3878f69e4fdfdfa55521fa67c10c43232caaa0cc98
7
- data.tar.gz: adfb3ea1b3de2e96ea2fb10dd178cea6a7f68d5a9761e938b9dd9f0eef8c43b54af3cb583f7bca0f6717b7fab6f769d714087dea281830bef95b1a98cd800130
6
+ metadata.gz: 0f7a37f152776e1d9fbc2440b71cb045464063f759c88cfff5d6dc0302ab8e9f6ea7aeed4c35375b5ad25861132dfb1ba4ebad57da6051ba75680beb309c3665
7
+ data.tar.gz: 0c07b8989e32d26ca39105630ffe74b83207425449d412f344461c194926e0efed64da367449898295de62a3618910f7a249e94ee91efe0290b3332d1b820319
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,18 +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.3.7
5
- - 2.4.4
6
- - 2.5.1
8
+ - 2.5.8
9
+ - 2.6.6
10
+ - 2.7.1
7
11
  env:
12
+ - RAILS=6-0 DB=sqlite
13
+ - RAILS=6-0 DB=pg
14
+ - RAILS=6-0 DB=mysql
8
15
  - RAILS=5-2 DB=sqlite
9
16
  - RAILS=5-2 DB=pg
10
- - RAILS=5-1 DB=sqlite
11
- - RAILS=5-1 DB=pg
12
- - RAILS=5-1 DB=mysql
13
- - RAILS=4-2 DB=sqlite
14
- - RAILS=4-2 DB=pg
15
- - RAILS=4-2 DB=mysql
17
+ - RAILS=5-2 DB=mysql
16
18
  before_script:
17
19
  - psql -c 'create database active_reporting_test;' -U postgres
18
20
  - mysql -e 'create database active_reporting_test collate utf8_general_ci;'
@@ -1,11 +1,62 @@
1
+ ## 0.5.1 (2020-06-31)
2
+
3
+ ### Features
4
+
5
+ * Allow dimensions defined in a `Metric` to use LEFT OUTER JOINs via a new `:join_method` option (#32) - *germanotm*
6
+
7
+ ### Misc
8
+
9
+ * Fixed warning about initialized variables
10
+ * Fixed Ruby 2.7 warning
11
+
12
+ ## 0.5.0 (2020-06-30)
13
+
14
+ ### Bug Fixes
15
+
16
+ * Fix Missing quotation marks in column names causing SQL errors on MYSQL (#30) - *germanotm*
17
+
18
+ ### Misc
19
+
20
+ * Update matrix to only supported Rubies and Rails versions. Rails 5.2+ and Ruby 2.5+ are officially supported now.
21
+
22
+ ## 0.4.2 (2019-11-01)
23
+
24
+ ### Misc
25
+
26
+ * Test against Rails 6.0 final
27
+ * Fixed deprecated call to `to_hash` - *joshforbes*
28
+ * Corrected readme entry for `dimesions` option for `ActiveReporting::Metric` - *joshforbes*
29
+
30
+ ## 0.4.1 (2019-05-28)
31
+
32
+ ### Features
33
+
34
+ * Hierarchical dimensions may now have custom keys in result (#16) - *andresgutgon*
35
+
36
+ ### Misc
37
+
38
+ * Test against Raisl 6.0RC
39
+ * Loosen AR requirements. The gem will install for any AR version, but only ones listed in the README are supported
40
+ * Test against active Rubies
41
+
42
+ ## 0.4.0 (2018-05-02)
43
+
44
+ ### Breaking Changes
45
+
46
+ * Gemspec now requires Ruby 2.3 and later to install
47
+
48
+ ### Features
49
+
50
+ * Dimension off of `datetime` columns by date parts in PostgreSQL (See README for details) (#10) - *niborg*
51
+
1
52
  ## 0.3.0 (2018-04-12)
2
53
 
3
54
  ### Bug Fixes
4
55
 
5
- * Specify rescue from LoadError for ransack (#9) *niborg*
6
- * Fix ransack fallback logic *germanotm*
56
+ * Specify rescue from LoadError for ransack (#9) - *niborg*
57
+ * Fix ransack fallback logic (#8) - *germanotm*
7
58
 
8
- ## Misc
59
+ ### Misc
9
60
 
10
61
  * Test against Rails 5.2
11
62
  * Test against Ruby 2.5
@@ -20,11 +71,11 @@
20
71
 
21
72
  ### Bug Fixes
22
73
 
23
- * `metric` lives on fact model and not metric (#3) - *Michael Wheeler (wheeyls)*
74
+ * `metric` lives on fact model and not metric (#3) - *wheeyls*
24
75
 
25
76
  ### Misc
26
77
 
27
- * Readme corrections and updates (#2) - *Michael Wheeler (wheeyls)*
78
+ * Readme corrections and updates (#2) - *wheeyls*
28
79
 
29
80
  ## 0.1.1 (2017-04-22)
30
81
 
data/Gemfile CHANGED
@@ -8,16 +8,8 @@ rails = ENV['RAILS'] || '5-2'
8
8
  db = ENV['DB'] || 'sqlite'
9
9
 
10
10
  case rails
11
- when '4-2'
12
- gem 'activerecord', '~> 4.2.0'
13
- if ENV['DB'] == 'pg'
14
- gem 'pg', '~> 0.18'
15
- end
16
- if ENV['DB'] == 'mysql'
17
- gem 'mysql2', '~> 0.3.18'
18
- end
19
- when '5-1'
20
- gem 'activerecord', '~> 5.1.0'
21
- else
11
+ when '5-2'
22
12
  gem 'activerecord', '~> 5.2.0'
13
+ when '6-0'
14
+ gem 'activerecord', '~> 6.0.0'
23
15
  end
data/README.md CHANGED
@@ -8,9 +8,9 @@ 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.3, 2.4, and 2.5.
11
+ ActiveReporting officially supports Ruby 2.5 and later. Other versions may work, but are not supported.
12
12
 
13
- ActiveReporting officially supports Rails 4.2, 5.1, and 5.2.
13
+ ActiveReporting officially supports Rails 5.2, and 6.0. Other versions may work, but are not supported.
14
14
 
15
15
  ## Installation
16
16
 
@@ -49,8 +49,8 @@ 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
- * A sales rep on a fact table of orders
53
- * 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
54
54
  * The manufacture on a fact table of widgets
55
55
 
56
56
  SQL Equivalent: JOIN, GROUP BY
@@ -178,7 +178,7 @@ end
178
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.
179
179
 
180
180
  ```ruby
181
- class OrderFactModel < ActiveReporting::FactModel
181
+ class SaleFactModel < ActiveReporting::FactModel
182
182
  self.measure = :total
183
183
  end
184
184
  ```
@@ -219,6 +219,24 @@ class PhoneFactModel < ActiveReporting::FactModel
219
219
  end
220
220
  ```
221
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
+
222
240
  ## Configuring Dimension Filters
223
241
 
224
242
  A dimension filter provides filtering for a report. In SQL-land, this is the `WHERE` clause.
@@ -255,8 +273,8 @@ A `Metric` is the basic building block used to describe a question you want to a
255
273
 
256
274
  ```ruby
257
275
  my_metric = ActiveReporting::Metric.new(
258
- :order_total,
259
- fact_model: OrderFactModel,
276
+ :sale_total,
277
+ fact_model: SaleFactModel,
260
278
  aggregate: :sum
261
279
  )
262
280
  ```
@@ -267,7 +285,7 @@ my_metric = ActiveReporting::Metric.new(
267
285
 
268
286
  `aggregate` - The SQL aggregate used to calculate the metric. Supported aggregates include count, max, min, avg, and sum. (Default: `:count`)
269
287
 
270
- `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. 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 }}]`
271
289
 
272
290
  `dimension_filter` - A hash were the keys are dimension filter names and the values are the values passed into the filter.
273
291
 
@@ -275,26 +293,40 @@ my_metric = ActiveReporting::Metric.new(
275
293
 
276
294
  `order_by_dimension` - Allows you to set the ordering of the results based on a dimension label. (Examples: `{author: :desc}`, `{sales_ref: :asc}`)
277
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
+
278
310
  ## ActiveReporting::Report
279
311
 
280
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.
281
313
 
282
314
  ```ruby
283
315
  metric = ActiveReporting::Metric.new(
284
- :order_count,
285
- fact_model: OrderFactModel,
286
- dimension: [:sales_rep],
316
+ :sale_count,
317
+ fact_model: SaleFactModel,
318
+ dimensions: [:sales_rep],
287
319
  dimension_filter: {months_ago: 1}
288
320
  )
289
321
 
290
322
  report = ActiveReporting::Report.new(metric)
291
323
  report.run
292
- => [{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}]
293
325
  ```
294
326
 
295
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`.
296
328
 
297
- `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`)
298
330
 
299
331
  `dimension_filter` - A hash that will be merged with the `Metric`'s dimension filters.
300
332
 
@@ -304,15 +336,15 @@ A `Report` may also take additional arguments to merge with the `Metric`'s infor
304
336
 
305
337
  ```ruby
306
338
  metric = ActiveReporting::Metric.new(
307
- :order_count,
308
- fact_model: OrderFactModel,
309
- dimension: [:sales_rep],
339
+ :sale_count,
340
+ fact_model: SaleFactModel,
341
+ dimensions: [:sales_rep],
310
342
  dimension_filter: {months_ago: 1}
311
343
  )
312
344
 
313
- 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)
314
346
  report.run
315
- => [{order_count: 17, sales_rep: 'Mary Sue'}]
347
+ => [{sale_count: 17, sales_rep: 'Mary Sue'}]
316
348
  ```
317
349
 
318
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`.
@@ -335,6 +367,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
335
367
 
336
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).
337
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
+
338
375
  ## Contributing
339
376
 
340
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.
@@ -343,4 +380,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
343
380
  ## License
344
381
 
345
382
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
346
-
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'
@@ -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'"
@@ -39,4 +41,5 @@ module ActiveReporting
39
41
  UnknownDimension = Class.new(StandardError)
40
42
  UnknownDimensionFilter = Class.new(StandardError)
41
43
  UnknownMetric = Class.new(StandardError)
44
+ UnknownJoinMethod = Class.new(StandardError)
42
45
  end
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  class DimensionFilter
3
5
  attr_reader :name, :type, :body
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
4
  class FactModel
3
5
  class << self
@@ -83,6 +85,7 @@ module ActiveReporting
83
85
  #
84
86
  # @return [Symbol]
85
87
  def self.dimension_label
88
+ @dimension_label ||= nil
86
89
  @dimension_label || Configuration.default_dimension_label
87
90
  end
88
91
 
@@ -109,7 +112,7 @@ module ActiveReporting
109
112
  # @param body [Lambda]
110
113
  def self.dimension_label_callback(column, body)
111
114
  @dimension_label_callbacks ||= {}
112
- raise ArgumentError, "Dimension label callback body must be a callable object" unless body.respond_to?(:call)
115
+ raise ArgumentError, 'Dimension label callback body must be a callable object' unless body.respond_to?(:call)
113
116
  @dimension_label_callbacks[column.to_sym] = body
114
117
  end
115
118
 
@@ -143,7 +146,9 @@ module ActiveReporting
143
146
  # Invoke this method to make all dimension filters fallback to use ransack
144
147
  # if they are not defined as scopes on the model
145
148
  def self.use_ransack_for_unknown_dimension_filters
146
- raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.' unless Configuration.ransack_available
149
+ unless Configuration.ransack_available
150
+ raise RansackNotAvailable, 'Ransack not available. Please include it in your Gemfile.'
151
+ end
147
152
  @ransack_fallback = true
148
153
  end
149
154
 
@@ -151,6 +156,7 @@ module ActiveReporting
151
156
  #
152
157
  # @return [Boolean]
153
158
  def self.ransack_fallback
159
+ @ransack_fallback ||= false
154
160
  @ransack_fallback || Configuration.ransack_fallback
155
161
  end
156
162
  private_class_method :ransack_fallback
@@ -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
@@ -50,7 +52,8 @@ module ActiveReporting
50
52
  def statement
51
53
  parts = {
52
54
  select: select_statement,
53
- joins: dimension_joins,
55
+ joins: dimension_joins(ReportingDimension::JOIN_METHODS[:joins]),
56
+ left_outer_joins: dimension_joins(ReportingDimension::JOIN_METHODS[:left_outer_joins]),
54
57
  group: group_by_statement,
55
58
  having: having_statement,
56
59
  order: order_by_statement
@@ -82,8 +85,9 @@ module ActiveReporting
82
85
  end
83
86
  end
84
87
 
85
- def dimension_joins
86
- @dimensions.select { |d| d.type == :standard }.map { |d| d.name.to_sym }
88
+ def dimension_joins(join_method)
89
+ @dimensions.select { |d| d.type == Dimension::TYPES[:standard] && d.join_method == join_method }.
90
+ map { |d| d.name.to_sym }
87
91
  end
88
92
 
89
93
  def group_by_statement
@@ -1,23 +1,54 @@
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
+ JOIN_METHODS = { joins: :joins, left_outer_joins: :left_outer_joins }.freeze
13
+ attr_reader :join_method
14
+
15
+ def_delegators :@dimension, :name, :type, :klass, :association, :model, :hierarchical?, :datetime?
6
16
 
7
17
  def self.build_from_dimensions(fact_model, dimensions)
8
18
  Array(dimensions).map do |dim|
9
19
  dimension_name, label = dim.is_a?(Hash) ? Array(dim).flatten : [dim, nil]
10
20
  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)
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))
15
25
  end
16
26
  end
17
27
 
18
- def initialize(dimension, label: nil)
28
+ # If you pass a symbol it means you just indicate
29
+ # the field on that dimension. With a hash you can
30
+ # customize the name of the label
31
+ #
32
+ # @param [Symbol|Hash] label
33
+ def self.label_config(label)
34
+ return { label: label } unless label.is_a?(Hash)
35
+
36
+ {
37
+ label: label[:field],
38
+ label_name: label[:name],
39
+ join_method: label[:join_method]
40
+ }
41
+ end
42
+
43
+ # @param dimension [ActiveReporting::Dimension]
44
+ # @option label [Maybe<Symbol>] Hierarchical dimension to be used as a label
45
+ # @option label_name [Maybe<Symbol|String>] Hierarchical dimension custom name
46
+ def initialize(dimension, label: nil, label_name: nil, join_method: nil)
19
47
  @dimension = dimension
20
- determine_label(label)
48
+
49
+ determine_label_field(label)
50
+ determine_label_name(label_name)
51
+ determine_join_method(join_method)
21
52
  end
22
53
 
23
54
  # The foreign key to use in queries
@@ -31,10 +62,10 @@ module ActiveReporting
31
62
  #
32
63
  # @return [Array]
33
64
  def select_statement(with_identifier: true)
34
- return [degenerate_fragment] if type == :degenerate
65
+ return [degenerate_select_fragment] if type == Dimension::TYPES[:degenerate]
35
66
 
36
- ss = ["#{label_fragment} AS #{name}"]
37
- ss << "#{identifier_fragment} AS #{name}_identifier" if with_identifier
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
38
69
  ss
39
70
  end
40
71
 
@@ -42,7 +73,7 @@ module ActiveReporting
42
73
  #
43
74
  # @return [Array]
44
75
  def group_by_statement(with_identifier: true)
45
- return [degenerate_fragment] if type == :degenerate
76
+ return [degenerate_fragment] if type == Dimension::TYPES[:degenerate]
46
77
 
47
78
  group = [label_fragment]
48
79
  group << identifier_fragment if with_identifier
@@ -54,8 +85,8 @@ module ActiveReporting
54
85
  # @return [String]
55
86
  def order_by_statement(direction:)
56
87
  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
88
+ raise "Ording direction should be 'asc' or 'desc'" unless %w[ASC DESC].include?(direction)
89
+ return "#{degenerate_fragment} #{direction}" if type == Dimension::TYPES[:degenerate]
59
90
  "#{label_fragment} #{direction}"
60
91
  end
61
92
 
@@ -68,31 +99,77 @@ module ActiveReporting
68
99
 
69
100
  private ####################################################################
70
101
 
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
102
+ def determine_label_field(label_field)
103
+ @label = if label_field.present? && validate_hierarchical_label(label_field)
104
+ label_field.to_sym
105
+ else
106
+ dimension_fact_model.dimension_label || Configuration.default_dimension_label
107
+ end
74
108
  end
75
109
 
76
- def validate_hierarchical_label(hierarchical_label)
77
- if !hierarchical?
78
- raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
110
+ def determine_label_name(label_name)
111
+ @label_name = label_name ? "#{name}_#{label_name}" : name
112
+ end
113
+
114
+ def determine_join_method(join_method)
115
+ if join_method.blank?
116
+ @join_method = ReportingDimension::JOIN_METHODS[:joins]
117
+ elsif ReportingDimension::JOIN_METHODS.include?(join_method)
118
+ @join_method = join_method
119
+ else
120
+ raise UnknownJoinMethod, "Method '#{join_method}' not included in '#{ReportingDimension::JOIN_METHODS.values}'"
79
121
  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}"
122
+ end
123
+
124
+ 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)
82
131
  end
83
132
  true
84
133
  end
85
134
 
135
+ def validate_dimension_is_hierachical(hierarchical_label)
136
+ return if hierarchical?
137
+ raise InvalidDimensionLabel, "#{name} must be hierarchical to use label #{hierarchical_label}"
138
+ end
139
+
140
+ def validate_supported_database_for_datetime_hierarchies
141
+ return if SUPPORTED_DBS.include?(model.connection.adapter_name)
142
+ raise InvalidDimensionLabel,
143
+ "Cannot utilize datetime grouping for #{name}; " \
144
+ "database #{model.connection.adapter_name} is not supported"
145
+ end
146
+
147
+ def validate_against_datetime_hierarchies(hierarchical_label)
148
+ return if DATETIME_HIERARCHIES.include?(hierarchical_label.to_sym)
149
+ raise InvalidDimensionLabel, "#{hierarchical_label} is not a valid datetime grouping label in #{name}"
150
+ end
151
+
152
+ def validate_against_fact_model_properties(hierarchical_label)
153
+ return if dimension_fact_model.hierarchical_levels.include?(hierarchical_label.to_sym)
154
+ raise InvalidDimensionLabel, "#{hierarchical_label} is not a hierarchical label in #{name}"
155
+ end
156
+
86
157
  def degenerate_fragment
158
+ return "#{name}_#{@label}" if datetime?
159
+ "#{model.quoted_table_name}.#{name}"
160
+ end
161
+
162
+ def degenerate_select_fragment
163
+ return "DATE_TRUNC('#{@label}', #{model.quoted_table_name}.#{name}) AS #{name}_#{@label}" if datetime?
87
164
  "#{model.quoted_table_name}.#{name}"
88
165
  end
89
166
 
90
167
  def identifier_fragment
91
- "#{klass.quoted_table_name}.#{klass.primary_key}"
168
+ "#{klass.quoted_table_name}.#{model.connection.quote_column_name(klass.primary_key)}"
92
169
  end
93
170
 
94
171
  def label_fragment
95
- "#{klass.quoted_table_name}.#{@label}"
172
+ "#{klass.quoted_table_name}.#{model.connection.quote_column_name(@label)}"
96
173
  end
97
174
 
98
175
  def dimension_fact_model
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveReporting
2
- VERSION = '0.3.0'.freeze
4
+ VERSION = '0.5.1'
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.3.0
4
+ version: 0.5.1
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: 2018-04-12 00:00:00.000000000 Z
11
+ date: 2020-07-31 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.14
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: []