groupdate 6.4.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdca24f5af218da761aff856a2798e39d4469fa5cdf5186d061572cea4b256a6
4
- data.tar.gz: 3b327397c8ba2638ccda503d3d3fd9d641b86cf8018161fc3cbb7891122d3b3f
3
+ metadata.gz: 07ae05090ff5047136472fd30bc78ff078350c4fab330f73d84c36316dbbfbcf
4
+ data.tar.gz: 824c766fa826aeb15bcd0e21ed85b264a6d2f1a45c9e3f06d408b0df3c773e64
5
5
  SHA512:
6
- metadata.gz: 0b06dcf600f13158dbf4d5edeab2212aedd21b4a2e4efb36d26ab1635c035c562e28932b03ed26bdb452c5118c125d38fc8ea665e2dc5c458a986bd87e62912b
7
- data.tar.gz: 0b78a8ee21b370052d1eacffa3c5c42ceb059ec1a1d14a7b992076cd02cfe158dc6ecee027578aeb233d2adc1c3fa4e71b2f572bf87affd51077b2282e6b2e21
6
+ metadata.gz: 5716c00f5cfd9ef21a4500b8936cdc42cbe476ea1acaf8cf72df82ea69d9595284300cdeb4aac43af434dfe3ab192b021fc247910b6f294bb4dcc6cd72807fb1
7
+ data.tar.gz: 845d022ae8bba825fa3f6ee6ba91cb5939a60c5a2933a942a2aa8b0802cb4de7507226570dc648c597b5d8e7dc46211e8d57ea93c32d5fa63247a2a7262f6712
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 6.7.0 (2025-06-02)
2
+
3
+ - Added time zone support for SQLite
4
+ - Added `day_start` support for SQLite
5
+ - Added `group_by_quarter` support for SQLite
6
+
7
+ ## 6.6.0 (2025-05-04)
8
+
9
+ - Dropped support for Ruby < 3.2 and Active Record < 7.1
10
+
11
+ ## 6.5.1 (2024-10-07)
12
+
13
+ - Fixed connection leasing for Active Record 7.2+
14
+
15
+ ## 6.5.0 (2024-10-01)
16
+
17
+ - Added support for Active Record 8
18
+ - Dropped support for Ruby < 3.1 and Active Record < 7
19
+
1
20
  ## 6.4.0 (2023-09-13)
2
21
 
3
22
  - Added support for Trilogy
@@ -173,7 +192,7 @@ Breaking changes
173
192
  - Added `default_value` option
174
193
  - Accept symbol for `format` option
175
194
  - Raise `ArgumentError` if no field specified
176
- - Added support for ActiveRecord 5 beta
195
+ - Added support for Active Record 5 beta
177
196
 
178
197
  ## 2.5.0 (2015-09-29)
179
198
 
@@ -187,18 +206,18 @@ Breaking changes
187
206
  - Added `carry_forward` option
188
207
  - Added `series: false` option for arrays and hashes
189
208
  - Fixed issue w/ Brasilia Summer Time
190
- - Fixed issues w/ ActiveRecord 4.2
209
+ - Fixed issues w/ Active Record 4.2
191
210
 
192
211
  ## 2.3.0 (2014-08-31)
193
212
 
194
- - Raise error when ActiveRecord::Base.default_timezone is not `:utc`
213
+ - Raise error when `ActiveRecord::Base.default_timezone` is not `:utc`
195
214
  - Added `day_of_month`
196
215
  - Added `month_of_year`
197
216
  - Do not quote column name
198
217
 
199
218
  ## 2.2.1 (2014-06-23)
200
219
 
201
- - Fixed ActiveRecord 3 associations
220
+ - Fixed Active Record 3 associations
202
221
 
203
222
  ## 2.2.0 (2014-06-22)
204
223
 
@@ -263,3 +282,44 @@ Breaking changes
263
282
  ## 1.0.0 (2013-05-15)
264
283
 
265
284
  - First major release
285
+
286
+ ## 0.1.6 (2013-05-07)
287
+
288
+ - Fixed error with Ruby 1.8
289
+
290
+ ## 0.1.5 (2013-05-01)
291
+
292
+ - Added support for getting the entire series
293
+
294
+ ## 0.1.4 (2013-05-01)
295
+
296
+ - Added support for Ruby 1.8
297
+
298
+ ## 0.1.3 (2013-04-25)
299
+
300
+ - Improved field names for ordering
301
+
302
+ ## 0.1.2 (2013-04-24)
303
+
304
+ - Added ability to order easily
305
+
306
+ ## 0.1.1 (2013-04-21)
307
+
308
+ - Improved return types for MySQL
309
+
310
+ ## 0.1.0 (2013-04-21)
311
+
312
+ - Added `day_of_week`
313
+ - Added `hour_of_day`
314
+
315
+ ## 0.0.3 (2013-04-18)
316
+
317
+ - Added support for Rails time zone strings
318
+
319
+ ## 0.0.2 (2013-04-18)
320
+
321
+ - Added tests
322
+
323
+ ## 0.0.1 (2013-04-17)
324
+
325
+ - First release
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2022 Andrew Kane
1
+ Copyright (c) 2013-2025 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -11,11 +11,11 @@ The simplest way to group by:
11
11
 
12
12
  :cake: Get the entire series - **the other best part**
13
13
 
14
- Supports PostgreSQL, MySQL, and Redshift, plus arrays and hashes (and limited support for [SQLite](#for-sqlite))
14
+ Supports PostgreSQL, MySQL, MariaDB, SQLite, and Redshift, plus arrays and hashes
15
15
 
16
16
  :cupid: Goes hand in hand with [Chartkick](https://www.chartkick.com)
17
17
 
18
- [![Build Status](https://github.com/ankane/groupdate/workflows/build/badge.svg?branch=master)](https://github.com/ankane/groupdate/actions)
18
+ [![Build Status](https://github.com/ankane/groupdate/actions/workflows/build.yml/badge.svg)](https://github.com/ankane/groupdate/actions)
19
19
 
20
20
  ## Installation
21
21
 
@@ -25,16 +25,16 @@ Add this line to your application’s Gemfile:
25
25
  gem "groupdate"
26
26
  ```
27
27
 
28
- For MySQL and SQLite, also follow [these instructions](#additional-instructions).
28
+ For MySQL and MariaDB, also follow [these instructions](#additional-instructions).
29
29
 
30
30
  ## Getting Started
31
31
 
32
32
  ```ruby
33
33
  User.group_by_day(:created_at).count
34
34
  # {
35
- # Sat, 24 May 2020 => 50,
36
- # Sun, 25 May 2020 => 100,
37
- # Mon, 26 May 2020 => 34
35
+ # Wed, 01 Jan 2025 => 50,
36
+ # Thu, 02 Jan 2025 => 100,
37
+ # Fri, 03 Jan 2025 => 34
38
38
  # }
39
39
  ```
40
40
 
@@ -75,9 +75,9 @@ or
75
75
  ```ruby
76
76
  User.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
77
77
  # {
78
- # Sun, 08 Mar 2020 => 70,
79
- # Sun, 15 Mar 2020 => 54,
80
- # Sun, 22 Mar 2020 => 80
78
+ # Sun, 05 Jan 2025 => 70,
79
+ # Sun, 12 Jan 2025 => 54,
80
+ # Sun, 19 Jan 2025 => 80
81
81
  # }
82
82
  ```
83
83
 
@@ -154,8 +154,8 @@ To get keys in a different format, use:
154
154
  ```ruby
155
155
  User.group_by_month(:created_at, format: "%b %Y").count
156
156
  # {
157
- # "Jan 2020" => 10
158
- # "Feb 2020" => 12
157
+ # "Jan 2025" => 10
158
+ # "Feb 2025" => 12
159
159
  # }
160
160
  ```
161
161
 
@@ -250,7 +250,9 @@ users.group_by_day { |u| u.created_at }.to_h { |k, v| [k, v.count] }
250
250
 
251
251
  ## Additional Instructions
252
252
 
253
- ### For MySQL
253
+ <a name="for-mysql"></a>
254
+
255
+ ### For MySQL and MariaDB
254
256
 
255
257
  [Time zone support](https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html) must be installed on the server.
256
258
 
@@ -266,32 +268,6 @@ SELECT CONVERT_TZ(NOW(), '+00:00', 'Pacific/Honolulu');
266
268
 
267
269
  It should return the time instead of `NULL`.
268
270
 
269
- ### For SQLite
270
-
271
- Groupdate has limited support for SQLite.
272
-
273
- - No time zone support
274
- - No `day_start` option
275
- - No `group_by_quarter` method
276
-
277
- If your application’s time zone is set to something other than `Etc/UTC` (the default), create an initializer with:
278
-
279
- ```ruby
280
- Groupdate.time_zone = false
281
- ```
282
-
283
- ## Upgrading
284
-
285
- ### 6.0
286
-
287
- Groupdate 6.0 protects against unsafe input by default. For non-attribute arguments, use:
288
-
289
- ```ruby
290
- User.group_by_day(Arel.sql(known_safe_value)).count
291
- ```
292
-
293
- Also, the `dates` option has been removed.
294
-
295
271
  ## History
296
272
 
297
273
  View the [changelog](https://github.com/ankane/groupdate/blob/master/CHANGELOG.md)
@@ -3,7 +3,7 @@ module Groupdate
3
3
  class BaseAdapter
4
4
  attr_reader :period, :column, :day_start, :week_start, :n_seconds
5
5
 
6
- def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:, n_seconds:)
6
+ def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:, n_seconds:, adapter_name: nil)
7
7
  @relation = relation
8
8
  @column = column
9
9
  @period = period
@@ -12,15 +12,10 @@ module Groupdate
12
12
  @week_start = week_start
13
13
  @day_start = day_start
14
14
  @n_seconds = n_seconds
15
+ @adapter_name = adapter_name
15
16
 
16
- if ActiveRecord::VERSION::MAJOR >= 7
17
- if ActiveRecord.default_timezone == :local
18
- raise Groupdate::Error, "ActiveRecord.default_timezone must be :utc to use Groupdate"
19
- end
20
- else
21
- if relation.default_timezone == :local
22
- raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
23
- end
17
+ if ActiveRecord.default_timezone == :local
18
+ raise Groupdate::Error, "ActiveRecord.default_timezone must be :utc to use Groupdate"
24
19
  end
25
20
  end
26
21
 
@@ -23,7 +23,7 @@ module Groupdate
23
23
  when :week
24
24
  ["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?)::date", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval]
25
25
  when :custom
26
- if @relation.connection.adapter_name == "Redshift"
26
+ if @adapter_name == "Redshift"
27
27
  ["TIMESTAMP 'epoch' + (FLOOR(EXTRACT(EPOCH FROM #{column}::timestamp) / ?) * ?) * INTERVAL '1 second'", n_seconds, n_seconds]
28
28
  else
29
29
  ["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
@@ -2,9 +2,6 @@ module Groupdate
2
2
  module Adapters
3
3
  class SQLiteAdapter < BaseAdapter
4
4
  def group_clause
5
- raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
6
- raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
7
-
8
5
  query =
9
6
  if period == :week
10
7
  ["strftime('%Y-%m-%d', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
@@ -36,7 +33,7 @@ module Groupdate
36
33
  when :month
37
34
  "%Y-%m-01"
38
35
  when :quarter
39
- raise Groupdate::Error, "Quarter not supported for SQLite"
36
+ nil
40
37
  else # year
41
38
  "%Y-01-01"
42
39
  end
@@ -44,8 +41,46 @@ module Groupdate
44
41
  ["strftime(?, #{column})", format]
45
42
  end
46
43
 
44
+ if period != :custom && (@time_zone != SeriesBuilder.utc || day_start != 0 || period == :quarter)
45
+ setup_function
46
+ week_start = period == :week ? Groupdate::Magic::DAYS[self.week_start].to_s : nil
47
+ query = ["groupdate_internal(?, #{column}, ?, ?, ?)", period, @time_zone.tzinfo.name, day_start, week_start]
48
+ end
49
+
47
50
  @relation.send(:sanitize_sql_array, query)
48
51
  end
52
+
53
+ private
54
+
55
+ def setup_function
56
+ @relation.connection_pool.with_connection do |connection|
57
+ raw_connection = connection.raw_connection
58
+ return if raw_connection.instance_variable_defined?(:@groupdate_function)
59
+
60
+ utc = SeriesBuilder.utc
61
+ date_periods = %i[day week month quarter year]
62
+
63
+ # note: this function is part of the internal API and may change between releases
64
+ # TODO improve performance
65
+ raw_connection.create_function("groupdate_internal", 4) do |func, period, value, time_zone, day_start, week_start|
66
+ if value.nil?
67
+ func.result = nil
68
+ else
69
+ period = period.to_sym
70
+ # cast_result handles week_start for day_of_week
71
+ week_start = :sunday if period == :day_of_week
72
+ result = SeriesBuilder.round_time(utc.parse(value), period, ActiveSupport::TimeZone[time_zone], day_start.to_i, week_start&.to_sym)
73
+ if date_periods.include?(period)
74
+ result = result.strftime("%Y-%m-%d")
75
+ elsif result.is_a?(Time)
76
+ result = result.in_time_zone(utc).strftime("%Y-%m-%d %H:%M:%S")
77
+ end
78
+ func.result = result
79
+ end
80
+ end
81
+ raw_connection.instance_variable_set(:@groupdate_function, true)
82
+ end
83
+ end
49
84
  end
50
85
  end
51
86
  end
@@ -5,7 +5,7 @@ module Enumerable
5
5
  raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" if args.any?
6
6
  Groupdate::Magic::Enumerable.group_by(self, period, options, &block)
7
7
  elsif respond_to?(:scoping)
8
- scoping { @klass.group_by_period(period, *args, **options, &block) }
8
+ scoping { klass.group_by_period(period, *args, **options, &block) }
9
9
  else
10
10
  raise ArgumentError, "no block given"
11
11
  end
@@ -19,7 +19,7 @@ module Enumerable
19
19
  Groupdate::Magic.validate_period(period, options.delete(:permit))
20
20
  send("group_by_#{period}", **options, &block)
21
21
  else
22
- scoping { @klass.group_by_period(period, *args, **options, &block) }
22
+ scoping { klass.group_by_period(period, *args, **options, &block) }
23
23
  end
24
24
  end
25
25
  end
@@ -180,12 +180,13 @@ module Groupdate
180
180
  end
181
181
 
182
182
  def time_zone_support?(relation)
183
- if relation.connection.adapter_name =~ /mysql/i
184
- # need to call klass for Rails < 5.2
185
- sql = relation.klass.send(:sanitize_sql_array, ["SELECT CONVERT_TZ(NOW(), '+00:00', ?)", time_zone.tzinfo.name])
186
- !relation.connection.select_all(sql).first.values.first.nil?
187
- else
188
- true
183
+ relation.connection_pool.with_connection do |connection|
184
+ if connection.adapter_name.match?(/mysql|trilogy/i)
185
+ sql = relation.send(:sanitize_sql_array, ["SELECT CONVERT_TZ(NOW(), '+00:00', ?)", time_zone.tzinfo.name])
186
+ !connection.select_all(sql).to_a.first.values.first.nil?
187
+ else
188
+ true
189
+ end
189
190
  end
190
191
  end
191
192
 
@@ -203,7 +204,7 @@ module Groupdate
203
204
  def self.generate_relation(relation, field:, **options)
204
205
  magic = Groupdate::Magic::Relation.new(**options)
205
206
 
206
- adapter_name = relation.connection.adapter_name
207
+ adapter_name = relation.connection_pool.with_connection { |c| c.adapter_name }
207
208
  adapter = Groupdate.adapters[adapter_name]
208
209
  raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}" unless adapter
209
210
 
@@ -221,7 +222,8 @@ module Groupdate
221
222
  time_range: magic.time_range,
222
223
  week_start: magic.week_start,
223
224
  day_start: magic.day_start,
224
- n_seconds: magic.n_seconds
225
+ n_seconds: magic.n_seconds,
226
+ adapter_name: adapter_name
225
227
  ).generate
226
228
 
227
229
  # add Groupdate info
@@ -238,7 +240,7 @@ module Groupdate
238
240
  def validate_column(column)
239
241
  unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
240
242
  column = column.to_s
241
- unless /\A\w+(\.\w+)?\z/i.match(column)
243
+ unless /\A\w+(\.\w+)?\z/i.match?(column)
242
244
  raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
243
245
  end
244
246
  end
@@ -251,13 +253,13 @@ module Groupdate
251
253
  def resolve_column(relation, column)
252
254
  node = relation.send(:relation).send(:arel_columns, [column]).first
253
255
  node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
254
- relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
256
+ relation.connection_pool.with_connection { |c| c.visitor.accept(node, Arel::Collectors::SQLString.new).value }
255
257
  end
256
258
  end
257
259
 
258
260
  # allow any options to keep flexible for future
259
261
  def self.process_result(relation, result, **options)
260
- relation.groupdate_values.reverse.each do |gv|
262
+ relation.groupdate_values.reverse_each do |gv|
261
263
  result = gv.perform(relation, result, default_value: options[:default_value])
262
264
  end
263
265
  result
@@ -46,7 +46,12 @@ module Groupdate
46
46
  return time_zone.at((time.to_time.to_i / n_seconds) * n_seconds)
47
47
  end
48
48
 
49
- time = time.to_time.in_time_zone(time_zone)
49
+ # TODO avoid calling to_time on dates
50
+ self.class.round_time(time.to_time, period, time_zone, day_start, @week_start_key)
51
+ end
52
+
53
+ def self.round_time(time, period, time_zone, day_start, week_start_key)
54
+ time = time.in_time_zone(time_zone)
50
55
 
51
56
  if day_start != 0
52
57
  # apply day_start to a time object that's not affected by DST
@@ -65,7 +70,7 @@ module Groupdate
65
70
  when :day
66
71
  time.beginning_of_day
67
72
  when :week
68
- time.beginning_of_week(@week_start_key)
73
+ time.beginning_of_week(week_start_key)
69
74
  when :month
70
75
  time.beginning_of_month
71
76
  when :quarter
@@ -77,7 +82,7 @@ module Groupdate
77
82
  when :minute_of_hour
78
83
  time.min
79
84
  when :day_of_week
80
- time.days_to_week_start(@week_start_key)
85
+ time.days_to_week_start(week_start_key)
81
86
  when :day_of_month
82
87
  time.day
83
88
  when :month_of_year
@@ -297,7 +302,7 @@ module Groupdate
297
302
  options.key?(:series) ? options[:series] : series_default
298
303
  end
299
304
 
300
- def utc
305
+ def self.utc
301
306
  @utc ||= ActiveSupport::TimeZone["Etc/UTC"]
302
307
  end
303
308
  end
@@ -1,3 +1,3 @@
1
1
  module Groupdate
2
- VERSION = "6.4.0"
2
+ VERSION = "6.7.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: groupdate
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.4.0
4
+ version: 6.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-09-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -16,15 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '6.1'
18
+ version: '7.1'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '6.1'
27
- description:
25
+ version: '7.1'
28
26
  email: andrew@ankane.org
29
27
  executables: []
30
28
  extensions: []
@@ -50,7 +48,6 @@ homepage: https://github.com/ankane/groupdate
50
48
  licenses:
51
49
  - MIT
52
50
  metadata: {}
53
- post_install_message:
54
51
  rdoc_options: []
55
52
  require_paths:
56
53
  - lib
@@ -58,15 +55,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
58
55
  requirements:
59
56
  - - ">="
60
57
  - !ruby/object:Gem::Version
61
- version: '3'
58
+ version: '3.2'
62
59
  required_rubygems_version: !ruby/object:Gem::Requirement
63
60
  requirements:
64
61
  - - ">="
65
62
  - !ruby/object:Gem::Version
66
63
  version: '0'
67
64
  requirements: []
68
- rubygems_version: 3.4.10
69
- signing_key:
65
+ rubygems_version: 3.6.7
70
66
  specification_version: 4
71
67
  summary: The simplest way to group temporal data
72
68
  test_files: []