active_reporting 0.3.0 → 0.5.1

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: 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: []