groupdate 5.2.4 → 6.8.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 +4 -4
- data/CHANGELOG.md +101 -4
- data/LICENSE.txt +1 -1
- data/README.md +23 -52
- data/lib/groupdate/adapters/base_adapter.rb +5 -35
- data/lib/groupdate/adapters/mysql_adapter.rb +15 -9
- data/lib/groupdate/adapters/postgresql_adapter.rb +9 -7
- data/lib/groupdate/adapters/sqlite_adapter.rb +43 -8
- data/lib/groupdate/enumerable.rb +2 -2
- data/lib/groupdate/magic.rb +55 -11
- data/lib/groupdate/relation.rb +3 -0
- data/lib/groupdate/series_builder.rb +48 -86
- data/lib/groupdate/version.rb +1 -1
- data/lib/groupdate.rb +12 -15
- metadata +6 -11
- data/lib/groupdate/adapters/redshift_adapter.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8f13f0acf7f0343411da5d22ddeef37e132971142ef849f72998caa78a32eba
|
|
4
|
+
data.tar.gz: c79112d5544778a1597e7723fc2b7ec6dfc45c906c7707aa74970ad007b01de0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 750c32b3729bda086a69107fbccbed3be1b5e4ad5d56c8e266c8a896856ed1a2364ff537a8d0e78af161d2d6fc74ad23f666b99876216f597e4db44b83f85cc3
|
|
7
|
+
data.tar.gz: 9770b962557af2e2f6356a1594c93e3b60d781bd637c1b34e78a2d241cb7842740652bbec15c2a959a1d9dadf304ef71c159b9985219a1a295d8342013892f58
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
## 6.8.0 (2026-04-04)
|
|
2
|
+
|
|
3
|
+
- Dropped support for Ruby < 3.3 and Active Record < 7.2
|
|
4
|
+
|
|
5
|
+
## 6.7.0 (2025-06-02)
|
|
6
|
+
|
|
7
|
+
- Added time zone support for SQLite
|
|
8
|
+
- Added `day_start` support for SQLite
|
|
9
|
+
- Added `group_by_quarter` support for SQLite
|
|
10
|
+
|
|
11
|
+
## 6.6.0 (2025-05-04)
|
|
12
|
+
|
|
13
|
+
- Dropped support for Ruby < 3.2 and Active Record < 7.1
|
|
14
|
+
|
|
15
|
+
## 6.5.1 (2024-10-07)
|
|
16
|
+
|
|
17
|
+
- Fixed connection leasing for Active Record 7.2+
|
|
18
|
+
|
|
19
|
+
## 6.5.0 (2024-10-01)
|
|
20
|
+
|
|
21
|
+
- Added support for Active Record 8
|
|
22
|
+
- Dropped support for Ruby < 3.1 and Active Record < 7
|
|
23
|
+
|
|
24
|
+
## 6.4.0 (2023-09-13)
|
|
25
|
+
|
|
26
|
+
- Added support for Trilogy
|
|
27
|
+
|
|
28
|
+
## 6.3.0 (2023-07-02)
|
|
29
|
+
|
|
30
|
+
- Dropped support for Ruby < 3 and Rails < 6.1
|
|
31
|
+
|
|
32
|
+
## 6.2.1 (2023-04-18)
|
|
33
|
+
|
|
34
|
+
- Fixed extra day with `DateTime` ranges
|
|
35
|
+
|
|
36
|
+
## 6.2.0 (2023-01-29)
|
|
37
|
+
|
|
38
|
+
- Added support for async methods with Active Record 7.1
|
|
39
|
+
|
|
40
|
+
## 6.1.0 (2022-04-05)
|
|
41
|
+
|
|
42
|
+
- Added `expand_range` option
|
|
43
|
+
|
|
44
|
+
## 6.0.1 (2022-01-16)
|
|
45
|
+
|
|
46
|
+
- Fixed incorrect results (error before 6.0) with `includes` with Active Record 6.1+
|
|
47
|
+
|
|
48
|
+
## 6.0.0 (2022-01-15)
|
|
49
|
+
|
|
50
|
+
- Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
|
|
51
|
+
- Raise `ArgumentError` for ranges with string bounds
|
|
52
|
+
- Added `n` option for Redshift
|
|
53
|
+
- Changed SQL to return dates instead of times for day, week, month, quarter, and year
|
|
54
|
+
- Removed `dates` option
|
|
55
|
+
- Dropped support for Ruby < 2.6 and Rails < 5.2
|
|
56
|
+
|
|
1
57
|
## 5.2.4 (2021-12-15)
|
|
2
58
|
|
|
3
59
|
- Simplified queries for Active Record 7 and MySQL
|
|
@@ -140,7 +196,7 @@ Breaking changes
|
|
|
140
196
|
- Added `default_value` option
|
|
141
197
|
- Accept symbol for `format` option
|
|
142
198
|
- Raise `ArgumentError` if no field specified
|
|
143
|
-
- Added support for
|
|
199
|
+
- Added support for Active Record 5 beta
|
|
144
200
|
|
|
145
201
|
## 2.5.0 (2015-09-29)
|
|
146
202
|
|
|
@@ -154,18 +210,18 @@ Breaking changes
|
|
|
154
210
|
- Added `carry_forward` option
|
|
155
211
|
- Added `series: false` option for arrays and hashes
|
|
156
212
|
- Fixed issue w/ Brasilia Summer Time
|
|
157
|
-
- Fixed issues w/
|
|
213
|
+
- Fixed issues w/ Active Record 4.2
|
|
158
214
|
|
|
159
215
|
## 2.3.0 (2014-08-31)
|
|
160
216
|
|
|
161
|
-
- Raise error when ActiveRecord::Base.default_timezone is not `:utc`
|
|
217
|
+
- Raise error when `ActiveRecord::Base.default_timezone` is not `:utc`
|
|
162
218
|
- Added `day_of_month`
|
|
163
219
|
- Added `month_of_year`
|
|
164
220
|
- Do not quote column name
|
|
165
221
|
|
|
166
222
|
## 2.2.1 (2014-06-23)
|
|
167
223
|
|
|
168
|
-
- Fixed
|
|
224
|
+
- Fixed Active Record 3 associations
|
|
169
225
|
|
|
170
226
|
## 2.2.0 (2014-06-22)
|
|
171
227
|
|
|
@@ -230,3 +286,44 @@ Breaking changes
|
|
|
230
286
|
## 1.0.0 (2013-05-15)
|
|
231
287
|
|
|
232
288
|
- First major release
|
|
289
|
+
|
|
290
|
+
## 0.1.6 (2013-05-07)
|
|
291
|
+
|
|
292
|
+
- Fixed error with Ruby 1.8
|
|
293
|
+
|
|
294
|
+
## 0.1.5 (2013-05-01)
|
|
295
|
+
|
|
296
|
+
- Added support for getting the entire series
|
|
297
|
+
|
|
298
|
+
## 0.1.4 (2013-05-01)
|
|
299
|
+
|
|
300
|
+
- Added support for Ruby 1.8
|
|
301
|
+
|
|
302
|
+
## 0.1.3 (2013-04-25)
|
|
303
|
+
|
|
304
|
+
- Improved field names for ordering
|
|
305
|
+
|
|
306
|
+
## 0.1.2 (2013-04-24)
|
|
307
|
+
|
|
308
|
+
- Added ability to order easily
|
|
309
|
+
|
|
310
|
+
## 0.1.1 (2013-04-21)
|
|
311
|
+
|
|
312
|
+
- Improved return types for MySQL
|
|
313
|
+
|
|
314
|
+
## 0.1.0 (2013-04-21)
|
|
315
|
+
|
|
316
|
+
- Added `day_of_week`
|
|
317
|
+
- Added `hour_of_day`
|
|
318
|
+
|
|
319
|
+
## 0.0.3 (2013-04-18)
|
|
320
|
+
|
|
321
|
+
- Added support for Rails time zone strings
|
|
322
|
+
|
|
323
|
+
## 0.0.2 (2013-04-18)
|
|
324
|
+
|
|
325
|
+
- Added tests
|
|
326
|
+
|
|
327
|
+
## 0.0.1 (2013-04-17)
|
|
328
|
+
|
|
329
|
+
- First release
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -11,30 +11,30 @@ 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
|
|
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
|
-
[](https://github.com/ankane/groupdate/actions)
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
Add this line to your application’s Gemfile:
|
|
23
23
|
|
|
24
24
|
```ruby
|
|
25
|
-
gem
|
|
25
|
+
gem "groupdate"
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
For MySQL and
|
|
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
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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,
|
|
79
|
-
# Sun,
|
|
80
|
-
# Sun,
|
|
78
|
+
# Sun, 05 Jan 2025 => 70,
|
|
79
|
+
# Sun, 12 Jan 2025 => 54,
|
|
80
|
+
# Sun, 19 Jan 2025 => 80
|
|
81
81
|
# }
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -119,6 +119,12 @@ To get a specific time range, use:
|
|
|
119
119
|
User.group_by_day(:created_at, range: 2.weeks.ago.midnight..Time.now).count
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
To expand the range to the start and end of the time period, use:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
User.group_by_day(:created_at, range: 2.weeks.ago..Time.now, expand_range: true).count
|
|
126
|
+
```
|
|
127
|
+
|
|
122
128
|
To get the most recent time periods, use:
|
|
123
129
|
|
|
124
130
|
```ruby
|
|
@@ -148,8 +154,8 @@ To get keys in a different format, use:
|
|
|
148
154
|
```ruby
|
|
149
155
|
User.group_by_month(:created_at, format: "%b %Y").count
|
|
150
156
|
# {
|
|
151
|
-
# "Jan
|
|
152
|
-
# "Feb
|
|
157
|
+
# "Jan 2025" => 10
|
|
158
|
+
# "Feb 2025" => 12
|
|
153
159
|
# }
|
|
154
160
|
```
|
|
155
161
|
|
|
@@ -164,7 +170,7 @@ User.group_by_hour_of_day(:created_at, format: "%-l %P").count
|
|
|
164
170
|
# }
|
|
165
171
|
```
|
|
166
172
|
|
|
167
|
-
Takes a `String`, which is passed to [strftime](
|
|
173
|
+
Takes a `String`, which is passed to [strftime](https://www.strfti.me/), or a `Symbol`, which is looked up by `I18n.localize` in `i18n` scope 'time.formats', or a `Proc`. You can pass a locale with the `locale` option.
|
|
168
174
|
|
|
169
175
|
### Series
|
|
170
176
|
|
|
@@ -210,19 +216,6 @@ If grouping on date columns which don’t need time zone conversion, use:
|
|
|
210
216
|
User.group_by_week(:created_on, time_zone: false).count
|
|
211
217
|
```
|
|
212
218
|
|
|
213
|
-
### User Input
|
|
214
|
-
|
|
215
|
-
If passing user input as the column, be sure to sanitize it first [like you must](https://rails-sqli.org/) with `group`.
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
column = params[:column]
|
|
219
|
-
|
|
220
|
-
# check against permitted columns
|
|
221
|
-
raise "Unpermitted column" unless ["column_a", "column_b"].include?(column)
|
|
222
|
-
|
|
223
|
-
User.group_by_day(column).count
|
|
224
|
-
```
|
|
225
|
-
|
|
226
219
|
### Default Scopes
|
|
227
220
|
|
|
228
221
|
If you use Postgres and have a default scope that uses `order`, you may get a `column must appear in the GROUP BY clause` error (just like with Active Record’s `group` method). Remove the `order` scope with:
|
|
@@ -252,12 +245,14 @@ users.group_by_day(series: true) { |u| u.created_at }
|
|
|
252
245
|
Count
|
|
253
246
|
|
|
254
247
|
```ruby
|
|
255
|
-
users.group_by_day { |u| u.created_at }.
|
|
248
|
+
users.group_by_day { |u| u.created_at }.to_h { |k, v| [k, v.count] }
|
|
256
249
|
```
|
|
257
250
|
|
|
258
251
|
## Additional Instructions
|
|
259
252
|
|
|
260
|
-
|
|
253
|
+
<a name="for-mysql"></a>
|
|
254
|
+
|
|
255
|
+
### For MySQL and MariaDB
|
|
261
256
|
|
|
262
257
|
[Time zone support](https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html) must be installed on the server.
|
|
263
258
|
|
|
@@ -273,30 +268,6 @@ SELECT CONVERT_TZ(NOW(), '+00:00', 'Pacific/Honolulu');
|
|
|
273
268
|
|
|
274
269
|
It should return the time instead of `NULL`.
|
|
275
270
|
|
|
276
|
-
### For SQLite
|
|
277
|
-
|
|
278
|
-
Groupdate has limited support for SQLite.
|
|
279
|
-
|
|
280
|
-
- No time zone support
|
|
281
|
-
- No `day_start` option
|
|
282
|
-
- No `group_by_quarter` method
|
|
283
|
-
|
|
284
|
-
If your application’s time zone is set to something other than `Etc/UTC` (the default), create an initializer with:
|
|
285
|
-
|
|
286
|
-
```ruby
|
|
287
|
-
Groupdate.time_zone = false
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
## Upgrading
|
|
291
|
-
|
|
292
|
-
### 5.0
|
|
293
|
-
|
|
294
|
-
Groupdate 5.0 brings a number of improvements. Here are a few to be aware of:
|
|
295
|
-
|
|
296
|
-
- The `week_start` option is now supported for SQLite
|
|
297
|
-
- The `day_start` option is now consistent between Active Record and enumerable
|
|
298
|
-
- Deprecated positional arguments for time zone and range have been removed
|
|
299
|
-
|
|
300
271
|
## History
|
|
301
272
|
|
|
302
273
|
View the [changelog](https://github.com/ankane/groupdate/blob/master/CHANGELOG.md)
|
|
@@ -3,27 +3,19 @@ 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:)
|
|
7
|
-
# very important
|
|
8
|
-
column = validate_column(column)
|
|
9
|
-
|
|
6
|
+
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:, n_seconds:, adapter_name: nil)
|
|
10
7
|
@relation = relation
|
|
11
|
-
@column =
|
|
8
|
+
@column = column
|
|
12
9
|
@period = period
|
|
13
10
|
@time_zone = time_zone
|
|
14
11
|
@time_range = time_range
|
|
15
12
|
@week_start = week_start
|
|
16
13
|
@day_start = day_start
|
|
17
14
|
@n_seconds = n_seconds
|
|
15
|
+
@adapter_name = adapter_name
|
|
18
16
|
|
|
19
|
-
if ActiveRecord
|
|
20
|
-
|
|
21
|
-
raise Groupdate::Error, "ActiveRecord.default_timezone must be :utc to use Groupdate"
|
|
22
|
-
end
|
|
23
|
-
else
|
|
24
|
-
if relation.default_timezone == :local
|
|
25
|
-
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
|
26
|
-
end
|
|
17
|
+
if ActiveRecord.default_timezone == :local
|
|
18
|
+
raise Groupdate::Error, "ActiveRecord.default_timezone must be :utc to use Groupdate"
|
|
27
19
|
end
|
|
28
20
|
end
|
|
29
21
|
|
|
@@ -49,28 +41,6 @@ module Groupdate
|
|
|
49
41
|
["#{column} IS NOT NULL"]
|
|
50
42
|
end
|
|
51
43
|
end
|
|
52
|
-
|
|
53
|
-
# basic version of Active Record disallow_raw_sql!
|
|
54
|
-
# symbol = column (safe), Arel node = SQL (safe), other = untrusted
|
|
55
|
-
# matches table.column and column
|
|
56
|
-
def validate_column(column)
|
|
57
|
-
unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
|
|
58
|
-
column = column.to_s
|
|
59
|
-
unless /\A\w+(\.\w+)?\z/i.match(column)
|
|
60
|
-
warn "[groupdate] Non-attribute argument: #{column}. Use Arel.sql() for known-safe values. This will raise an error in Groupdate 6"
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
column
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# resolves eagerly
|
|
67
|
-
# need to convert both where_clause (easy)
|
|
68
|
-
# and group_clause (not easy) if want to avoid this
|
|
69
|
-
def resolve_column(relation, column)
|
|
70
|
-
node = relation.send(:relation).send(:arel_columns, [column]).first
|
|
71
|
-
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
|
72
|
-
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
|
73
|
-
end
|
|
74
44
|
end
|
|
75
45
|
end
|
|
76
46
|
end
|
|
@@ -20,9 +20,21 @@ module Groupdate
|
|
|
20
20
|
when :month_of_year
|
|
21
21
|
["MONTH(#{day_start_column})", time_zone, day_start]
|
|
22
22
|
when :week
|
|
23
|
-
["
|
|
23
|
+
["CAST(DATE_FORMAT(#{day_start_column} - INTERVAL ((? + DAYOFWEEK(#{day_start_column})) % 7) DAY, '%Y-%m-%d') AS DATE)", time_zone, day_start, 12 - week_start, time_zone, day_start]
|
|
24
24
|
when :quarter
|
|
25
|
-
["
|
|
25
|
+
["CAST(CONCAT(YEAR(#{day_start_column}), '-', LPAD(1 + 3 * (QUARTER(#{day_start_column}) - 1), 2, '00'), '-01') AS DATE)", time_zone, day_start, time_zone, day_start]
|
|
26
|
+
when :day, :month, :year
|
|
27
|
+
format =
|
|
28
|
+
case period
|
|
29
|
+
when :day
|
|
30
|
+
"%Y-%m-%d"
|
|
31
|
+
when :month
|
|
32
|
+
"%Y-%m-01"
|
|
33
|
+
else # year
|
|
34
|
+
"%Y-01-01"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
["CAST(DATE_FORMAT(#{day_start_column}, ?) AS DATE)", time_zone, day_start, format]
|
|
26
38
|
when :custom
|
|
27
39
|
["FROM_UNIXTIME((UNIX_TIMESTAMP(#{column}) DIV ?) * ?)", n_seconds, n_seconds]
|
|
28
40
|
else
|
|
@@ -32,14 +44,8 @@ module Groupdate
|
|
|
32
44
|
"%Y-%m-%d %H:%i:%S"
|
|
33
45
|
when :minute
|
|
34
46
|
"%Y-%m-%d %H:%i:00"
|
|
35
|
-
|
|
47
|
+
else # hour
|
|
36
48
|
"%Y-%m-%d %H:00:00"
|
|
37
|
-
when :day
|
|
38
|
-
"%Y-%m-%d 00:00:00"
|
|
39
|
-
when :month
|
|
40
|
-
"%Y-%m-01 00:00:00"
|
|
41
|
-
else # year
|
|
42
|
-
"%Y-01-01 00:00:00"
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
|
|
@@ -21,16 +21,18 @@ module Groupdate
|
|
|
21
21
|
when :month_of_year
|
|
22
22
|
["EXTRACT(MONTH FROM #{day_start_column})::integer", time_zone, day_start_interval]
|
|
23
23
|
when :week
|
|
24
|
-
["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?)
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
if day_start == 0
|
|
29
|
-
# prettier
|
|
30
|
-
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
|
26
|
+
if @adapter_name == "Redshift"
|
|
27
|
+
["TIMESTAMP 'epoch' + (FLOOR(EXTRACT(EPOCH FROM #{column}::timestamp) / ?) * ?) * INTERVAL '1 second'", n_seconds, n_seconds]
|
|
31
28
|
else
|
|
32
|
-
["(
|
|
29
|
+
["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
|
|
33
30
|
end
|
|
31
|
+
when :day, :month, :quarter, :year
|
|
32
|
+
["DATE_TRUNC(?, #{day_start_column})::date", period, time_zone, day_start_interval]
|
|
33
|
+
else
|
|
34
|
+
# day start is always 0 for seconds, minute, hour
|
|
35
|
+
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
clean_group_clause(@relation.send(:sanitize_sql_array, query))
|
|
@@ -2,12 +2,9 @@ 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
|
-
["strftime('%Y-%m-%d
|
|
7
|
+
["strftime('%Y-%m-%d', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
|
|
11
8
|
elsif period == :custom
|
|
12
9
|
["datetime((strftime('%s', #{column}) / ?) * ?, 'unixepoch')", n_seconds, n_seconds]
|
|
13
10
|
else
|
|
@@ -32,20 +29,58 @@ module Groupdate
|
|
|
32
29
|
when :hour
|
|
33
30
|
"%Y-%m-%d %H:00:00 UTC"
|
|
34
31
|
when :day
|
|
35
|
-
"%Y-%m-%d
|
|
32
|
+
"%Y-%m-%d"
|
|
36
33
|
when :month
|
|
37
|
-
"%Y-%m-01
|
|
34
|
+
"%Y-%m-01"
|
|
38
35
|
when :quarter
|
|
39
|
-
|
|
36
|
+
nil
|
|
40
37
|
else # year
|
|
41
|
-
"%Y-01-01
|
|
38
|
+
"%Y-01-01"
|
|
42
39
|
end
|
|
43
40
|
|
|
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
|
data/lib/groupdate/enumerable.rb
CHANGED
|
@@ -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 {
|
|
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 {
|
|
22
|
+
scoping { klass.group_by_period(period, *args, **options, &block) }
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
data/lib/groupdate/magic.rb
CHANGED
|
@@ -22,7 +22,7 @@ module Groupdate
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def validate_keywords
|
|
25
|
-
known_keywords = [:time_zone, :
|
|
25
|
+
known_keywords = [:time_zone, :series, :format, :locale, :range, :expand_range, :reverse]
|
|
26
26
|
|
|
27
27
|
if %i[week day_of_week].include?(period)
|
|
28
28
|
known_keywords << :week_start
|
|
@@ -125,6 +125,10 @@ module Groupdate
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def perform(relation, result, default_value:)
|
|
128
|
+
if defined?(ActiveRecord::Promise) && result.is_a?(ActiveRecord::Promise)
|
|
129
|
+
return result.then { |r| perform(relation, r, default_value: default_value) }
|
|
130
|
+
end
|
|
131
|
+
|
|
128
132
|
multiple_groups = relation.group_values.size > 1
|
|
129
133
|
|
|
130
134
|
check_nils(result, multiple_groups, relation)
|
|
@@ -145,6 +149,16 @@ module Groupdate
|
|
|
145
149
|
lambda { |k| k.to_i }
|
|
146
150
|
when :day_of_week
|
|
147
151
|
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
|
152
|
+
when :day, :week, :month, :quarter, :year
|
|
153
|
+
# TODO keep as date
|
|
154
|
+
if day_start != 0
|
|
155
|
+
day_start_hour = day_start / 3600
|
|
156
|
+
day_start_min = (day_start % 3600) / 60
|
|
157
|
+
day_start_sec = (day_start % 3600) % 60
|
|
158
|
+
lambda { |k| k.in_time_zone(time_zone).change(hour: day_start_hour, min: day_start_min, sec: day_start_sec) }
|
|
159
|
+
else
|
|
160
|
+
lambda { |k| k.in_time_zone(time_zone) }
|
|
161
|
+
end
|
|
148
162
|
else
|
|
149
163
|
utc = ActiveSupport::TimeZone["UTC"]
|
|
150
164
|
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
|
@@ -166,12 +180,13 @@ module Groupdate
|
|
|
166
180
|
end
|
|
167
181
|
|
|
168
182
|
def time_zone_support?(relation)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
175
190
|
end
|
|
176
191
|
end
|
|
177
192
|
|
|
@@ -189,21 +204,26 @@ module Groupdate
|
|
|
189
204
|
def self.generate_relation(relation, field:, **options)
|
|
190
205
|
magic = Groupdate::Magic::Relation.new(**options)
|
|
191
206
|
|
|
192
|
-
adapter_name = relation.
|
|
207
|
+
adapter_name = relation.connection_pool.with_connection { |c| c.adapter_name }
|
|
193
208
|
adapter = Groupdate.adapters[adapter_name]
|
|
194
209
|
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}" unless adapter
|
|
195
210
|
|
|
211
|
+
# very important
|
|
212
|
+
column = validate_column(field)
|
|
213
|
+
column = resolve_column(relation, column)
|
|
214
|
+
|
|
196
215
|
# generate ActiveRecord relation
|
|
197
216
|
relation =
|
|
198
217
|
adapter.new(
|
|
199
218
|
relation,
|
|
200
|
-
column:
|
|
219
|
+
column: column,
|
|
201
220
|
period: magic.period,
|
|
202
221
|
time_zone: magic.time_zone,
|
|
203
222
|
time_range: magic.time_range,
|
|
204
223
|
week_start: magic.week_start,
|
|
205
224
|
day_start: magic.day_start,
|
|
206
|
-
n_seconds: magic.n_seconds
|
|
225
|
+
n_seconds: magic.n_seconds,
|
|
226
|
+
adapter_name: adapter_name
|
|
207
227
|
).generate
|
|
208
228
|
|
|
209
229
|
# add Groupdate info
|
|
@@ -213,9 +233,33 @@ module Groupdate
|
|
|
213
233
|
relation
|
|
214
234
|
end
|
|
215
235
|
|
|
236
|
+
class << self
|
|
237
|
+
# basic version of Active Record disallow_raw_sql!
|
|
238
|
+
# symbol = column (safe), Arel node = SQL (safe), other = untrusted
|
|
239
|
+
# matches table.column and column
|
|
240
|
+
def validate_column(column)
|
|
241
|
+
unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
|
|
242
|
+
column = column.to_s
|
|
243
|
+
unless /\A\w+(\.\w+)?\z/i.match?(column)
|
|
244
|
+
raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
column
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# resolves eagerly
|
|
251
|
+
# need to convert both where_clause (easy)
|
|
252
|
+
# and group_clause (not easy) if want to avoid this
|
|
253
|
+
def resolve_column(relation, column)
|
|
254
|
+
node = relation.send(:relation).send(:arel_columns, [column]).first
|
|
255
|
+
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
|
256
|
+
relation.connection_pool.with_connection { |c| c.visitor.accept(node, Arel::Collectors::SQLString.new).value }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
216
260
|
# allow any options to keep flexible for future
|
|
217
261
|
def self.process_result(relation, result, **options)
|
|
218
|
-
relation.groupdate_values.
|
|
262
|
+
relation.groupdate_values.reverse_each do |gv|
|
|
219
263
|
result = gv.perform(relation, result, default_value: options[:default_value])
|
|
220
264
|
end
|
|
221
265
|
result
|
data/lib/groupdate/relation.rb
CHANGED
|
@@ -9,6 +9,9 @@ module Groupdate
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def calculate(*args, &block)
|
|
12
|
+
# prevent calculate from being called twice
|
|
13
|
+
return super if has_include?(args[1])
|
|
14
|
+
|
|
12
15
|
default_value = [:count, :sum].include?(args[0]) ? 0 : nil
|
|
13
16
|
Groupdate.process_result(self, super, default_value: default_value)
|
|
14
17
|
end
|
|
@@ -2,8 +2,6 @@ module Groupdate
|
|
|
2
2
|
class SeriesBuilder
|
|
3
3
|
attr_reader :period, :time_zone, :day_start, :week_start, :n_seconds, :options
|
|
4
4
|
|
|
5
|
-
CHECK_PERIODS = [:day, :week, :month, :quarter, :year]
|
|
6
|
-
|
|
7
5
|
def initialize(period:, time_zone:, day_start:, week_start:, n_seconds:, **options)
|
|
8
6
|
@period = period
|
|
9
7
|
@time_zone = time_zone
|
|
@@ -23,41 +21,12 @@ module Groupdate
|
|
|
23
21
|
verified_data[k] = data.delete(k)
|
|
24
22
|
end
|
|
25
23
|
|
|
26
|
-
# this is a fun one
|
|
27
|
-
# PostgreSQL and Ruby both return the 2nd hour when converting/parsing a backward DST change
|
|
28
|
-
# Other databases and Active Support return the 1st hour (as expected)
|
|
29
|
-
# Active Support good: ActiveSupport::TimeZone["America/Los_Angeles"].parse("2013-11-03 01:00:00")
|
|
30
|
-
# MySQL good: SELECT CONVERT_TZ('2013-11-03 01:00:00', 'America/Los_Angeles', 'Etc/UTC');
|
|
31
|
-
# Ruby not good: Time.parse("2013-11-03 01:00:00")
|
|
32
|
-
# PostgreSQL not good: SELECT '2013-11-03 01:00:00'::timestamp AT TIME ZONE 'America/Los_Angeles';
|
|
33
|
-
# we need to account for this here
|
|
34
|
-
if series_default && CHECK_PERIODS.include?(period)
|
|
35
|
-
data.each do |k, v|
|
|
36
|
-
key = multiple_groups ? k[group_index] : k
|
|
37
|
-
# TODO only do this for PostgreSQL
|
|
38
|
-
# this may mask some inconsistent time zone errors
|
|
39
|
-
# but not sure there's a better approach
|
|
40
|
-
if key.hour == (key - 1.hour).hour && series.include?(key - 1.hour)
|
|
41
|
-
key -= 1.hour
|
|
42
|
-
if multiple_groups
|
|
43
|
-
k[group_index] = key
|
|
44
|
-
else
|
|
45
|
-
k = key
|
|
46
|
-
end
|
|
47
|
-
verified_data[k] = v
|
|
48
|
-
elsif key != round_time(key)
|
|
49
|
-
# only need to show what database returned since it will cast in Ruby time zone
|
|
50
|
-
raise Groupdate::Error, "Database and Ruby have inconsistent time zone info. Database returned #{key}"
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
24
|
unless entire_series?(series_default)
|
|
56
25
|
series = series.select { |k| verified_data[k] }
|
|
57
26
|
end
|
|
58
27
|
|
|
59
28
|
value = 0
|
|
60
|
-
result =
|
|
29
|
+
result = series.to_h do |k|
|
|
61
30
|
value = verified_data[k] || (@options[:carry_forward] && value) || default_value
|
|
62
31
|
key =
|
|
63
32
|
if multiple_groups
|
|
@@ -67,7 +36,7 @@ module Groupdate
|
|
|
67
36
|
end
|
|
68
37
|
|
|
69
38
|
[key, value]
|
|
70
|
-
end
|
|
39
|
+
end
|
|
71
40
|
|
|
72
41
|
result
|
|
73
42
|
end
|
|
@@ -77,11 +46,16 @@ module Groupdate
|
|
|
77
46
|
return time_zone.at((time.to_time.to_i / n_seconds) * n_seconds)
|
|
78
47
|
end
|
|
79
48
|
|
|
80
|
-
|
|
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)
|
|
81
55
|
|
|
82
56
|
if day_start != 0
|
|
83
57
|
# apply day_start to a time object that's not affected by DST
|
|
84
|
-
time =
|
|
58
|
+
time = time.change(zone: utc)
|
|
85
59
|
time -= day_start.seconds
|
|
86
60
|
end
|
|
87
61
|
|
|
@@ -96,7 +70,7 @@ module Groupdate
|
|
|
96
70
|
when :day
|
|
97
71
|
time.beginning_of_day
|
|
98
72
|
when :week
|
|
99
|
-
time.beginning_of_week(
|
|
73
|
+
time.beginning_of_week(week_start_key)
|
|
100
74
|
when :month
|
|
101
75
|
time.beginning_of_month
|
|
102
76
|
when :quarter
|
|
@@ -108,7 +82,7 @@ module Groupdate
|
|
|
108
82
|
when :minute_of_hour
|
|
109
83
|
time.min
|
|
110
84
|
when :day_of_week
|
|
111
|
-
time.days_to_week_start(
|
|
85
|
+
time.days_to_week_start(week_start_key)
|
|
112
86
|
when :day_of_month
|
|
113
87
|
time.day
|
|
114
88
|
when :month_of_year
|
|
@@ -121,23 +95,12 @@ module Groupdate
|
|
|
121
95
|
|
|
122
96
|
if day_start != 0 && time.is_a?(Time)
|
|
123
97
|
time += day_start.seconds
|
|
124
|
-
time =
|
|
98
|
+
time = time.change(zone: time_zone)
|
|
125
99
|
end
|
|
126
100
|
|
|
127
101
|
time
|
|
128
102
|
end
|
|
129
103
|
|
|
130
|
-
def change_zone
|
|
131
|
-
@change_zone ||= begin
|
|
132
|
-
if ActiveSupport::VERSION::STRING >= "5.2"
|
|
133
|
-
->(time, zone) { time.change(zone: zone) }
|
|
134
|
-
else
|
|
135
|
-
# TODO make more efficient
|
|
136
|
-
->(time, zone) { zone.parse(time.strftime("%Y-%m-%d %H:%M:%S")) }
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
104
|
def time_range
|
|
142
105
|
@time_range ||= begin
|
|
143
106
|
time_range = options[:range]
|
|
@@ -148,10 +111,6 @@ module Groupdate
|
|
|
148
111
|
case v
|
|
149
112
|
when nil, Date, Time
|
|
150
113
|
# good
|
|
151
|
-
when String
|
|
152
|
-
# TODO raise error in Groupdate 6
|
|
153
|
-
warn "[groupdate] Range bounds should be Date or Time, not #{v.class.name}. This will raise an error in Groupdate 6"
|
|
154
|
-
break
|
|
155
114
|
else
|
|
156
115
|
raise ArgumentError, "Range bounds should be Date or Time, not #{v.class.name}"
|
|
157
116
|
end
|
|
@@ -164,37 +123,37 @@ module Groupdate
|
|
|
164
123
|
|
|
165
124
|
finish = time_range.end
|
|
166
125
|
finish = finish.in_time_zone(time_zone) if finish
|
|
167
|
-
if time_range.end.is_a?(Date) && !exclude_end
|
|
126
|
+
if time_range.end.is_a?(Date) && !time_range.end.is_a?(DateTime) && !exclude_end
|
|
168
127
|
finish += 1.day
|
|
169
128
|
exclude_end = true
|
|
170
129
|
end
|
|
171
130
|
|
|
131
|
+
if options[:expand_range]
|
|
132
|
+
start = round_time(start) if start
|
|
133
|
+
if finish && !(finish == round_time(finish) && exclude_end)
|
|
134
|
+
finish = round_time(finish) + step
|
|
135
|
+
exclude_end = true
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
172
139
|
time_range = Range.new(start, finish, exclude_end)
|
|
173
140
|
elsif !time_range && options[:last]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
elsif period == :custom
|
|
177
|
-
step = n_seconds
|
|
178
|
-
elsif 1.respond_to?(period)
|
|
179
|
-
step = 1.send(period)
|
|
180
|
-
else
|
|
181
|
-
raise ArgumentError, "Cannot use last option with #{period}"
|
|
182
|
-
end
|
|
183
|
-
if step
|
|
184
|
-
# loop instead of multiply to change start_at - see #151
|
|
185
|
-
start_at = now
|
|
186
|
-
(options[:last].to_i - 1).times do
|
|
187
|
-
start_at -= step
|
|
188
|
-
end
|
|
141
|
+
step = step()
|
|
142
|
+
raise ArgumentError, "Cannot use last option with #{period}" unless step
|
|
189
143
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# extend to end of current period
|
|
195
|
-
round_time(start_at)...(round_time(now) + step)
|
|
196
|
-
end
|
|
144
|
+
# loop instead of multiply to change start_at - see #151
|
|
145
|
+
start_at = now
|
|
146
|
+
(options[:last].to_i - 1).times do
|
|
147
|
+
start_at -= step
|
|
197
148
|
end
|
|
149
|
+
|
|
150
|
+
time_range =
|
|
151
|
+
if options[:current] == false
|
|
152
|
+
round_time(start_at - step)...round_time(now)
|
|
153
|
+
else
|
|
154
|
+
# extend to end of current period
|
|
155
|
+
round_time(start_at)...(round_time(now) + step)
|
|
156
|
+
end
|
|
198
157
|
end
|
|
199
158
|
time_range
|
|
200
159
|
end
|
|
@@ -256,13 +215,7 @@ module Groupdate
|
|
|
256
215
|
if time_range.begin
|
|
257
216
|
series = [round_time(time_range.begin)]
|
|
258
217
|
|
|
259
|
-
|
|
260
|
-
step = 3.months
|
|
261
|
-
elsif period == :custom
|
|
262
|
-
step = n_seconds
|
|
263
|
-
else
|
|
264
|
-
step = 1.send(period)
|
|
265
|
-
end
|
|
218
|
+
step = step()
|
|
266
219
|
|
|
267
220
|
last_step = series.last
|
|
268
221
|
day_start_hour = day_start / 3600
|
|
@@ -289,7 +242,6 @@ module Groupdate
|
|
|
289
242
|
def key_format
|
|
290
243
|
@key_format ||= begin
|
|
291
244
|
locale = options[:locale] || I18n.locale
|
|
292
|
-
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
|
293
245
|
|
|
294
246
|
if options[:format]
|
|
295
247
|
if options[:format].respond_to?(:call)
|
|
@@ -312,7 +264,7 @@ module Groupdate
|
|
|
312
264
|
I18n.localize(key, format: options[:format], locale: locale)
|
|
313
265
|
end
|
|
314
266
|
end
|
|
315
|
-
elsif [:day, :week, :month, :quarter, :year].include?(period)
|
|
267
|
+
elsif [:day, :week, :month, :quarter, :year].include?(period)
|
|
316
268
|
lambda { |k| k.to_date }
|
|
317
269
|
else
|
|
318
270
|
lambda { |k| k }
|
|
@@ -320,6 +272,16 @@ module Groupdate
|
|
|
320
272
|
end
|
|
321
273
|
end
|
|
322
274
|
|
|
275
|
+
def step
|
|
276
|
+
if period == :quarter
|
|
277
|
+
3.months
|
|
278
|
+
elsif period == :custom
|
|
279
|
+
n_seconds
|
|
280
|
+
elsif 1.respond_to?(period)
|
|
281
|
+
1.send(period)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
323
285
|
def handle_multiple(data, series, multiple_groups, group_index)
|
|
324
286
|
reverse = options[:reverse]
|
|
325
287
|
|
|
@@ -340,7 +302,7 @@ module Groupdate
|
|
|
340
302
|
options.key?(:series) ? options[:series] : series_default
|
|
341
303
|
end
|
|
342
304
|
|
|
343
|
-
def utc
|
|
305
|
+
def self.utc
|
|
344
306
|
@utc ||= ActiveSupport::TimeZone["Etc/UTC"]
|
|
345
307
|
end
|
|
346
308
|
end
|
data/lib/groupdate/version.rb
CHANGED
data/lib/groupdate.rb
CHANGED
|
@@ -4,16 +4,15 @@ require "active_support/core_ext/module/attribute_accessors"
|
|
|
4
4
|
require "active_support/time"
|
|
5
5
|
|
|
6
6
|
# modules
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
require_relative "groupdate/magic"
|
|
8
|
+
require_relative "groupdate/series_builder"
|
|
9
|
+
require_relative "groupdate/version"
|
|
10
10
|
|
|
11
11
|
# adapters
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
require "groupdate/adapters/sqlite_adapter"
|
|
12
|
+
require_relative "groupdate/adapters/base_adapter"
|
|
13
|
+
require_relative "groupdate/adapters/mysql_adapter"
|
|
14
|
+
require_relative "groupdate/adapters/postgresql_adapter"
|
|
15
|
+
require_relative "groupdate/adapters/sqlite_adapter"
|
|
17
16
|
|
|
18
17
|
module Groupdate
|
|
19
18
|
class Error < RuntimeError; end
|
|
@@ -21,10 +20,9 @@ module Groupdate
|
|
|
21
20
|
PERIODS = [:second, :minute, :hour, :day, :week, :month, :quarter, :year, :day_of_week, :hour_of_day, :minute_of_hour, :day_of_month, :day_of_year, :month_of_year]
|
|
22
21
|
METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
|
|
23
22
|
|
|
24
|
-
mattr_accessor :week_start, :day_start, :time_zone
|
|
23
|
+
mattr_accessor :week_start, :day_start, :time_zone
|
|
25
24
|
self.week_start = :sunday
|
|
26
25
|
self.day_start = 0
|
|
27
|
-
self.dates = true
|
|
28
26
|
|
|
29
27
|
# api for gems like ActiveMedian
|
|
30
28
|
def self.process_result(relation, result, **options)
|
|
@@ -45,13 +43,12 @@ module Groupdate
|
|
|
45
43
|
end
|
|
46
44
|
end
|
|
47
45
|
|
|
48
|
-
Groupdate.register_adapter ["Mysql2", "Mysql2Spatial", "Mysql2Rgeo"], Groupdate::Adapters::MySQLAdapter
|
|
49
|
-
Groupdate.register_adapter ["PostgreSQL", "PostGIS"], Groupdate::Adapters::PostgreSQLAdapter
|
|
50
|
-
Groupdate.register_adapter "Redshift", Groupdate::Adapters::RedshiftAdapter
|
|
46
|
+
Groupdate.register_adapter ["Mysql2", "Mysql2Spatial", "Mysql2Rgeo", "Trilogy"], Groupdate::Adapters::MySQLAdapter
|
|
47
|
+
Groupdate.register_adapter ["PostgreSQL", "PostGIS", "Redshift"], Groupdate::Adapters::PostgreSQLAdapter
|
|
51
48
|
Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
require_relative "groupdate/enumerable"
|
|
54
51
|
|
|
55
52
|
ActiveSupport.on_load(:active_record) do
|
|
56
|
-
|
|
53
|
+
require_relative "groupdate/active_record"
|
|
57
54
|
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:
|
|
4
|
+
version: 6.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
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: '
|
|
18
|
+
version: '7.2'
|
|
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: '
|
|
27
|
-
description:
|
|
25
|
+
version: '7.2'
|
|
28
26
|
email: andrew@ankane.org
|
|
29
27
|
executables: []
|
|
30
28
|
extensions: []
|
|
@@ -39,7 +37,6 @@ files:
|
|
|
39
37
|
- lib/groupdate/adapters/base_adapter.rb
|
|
40
38
|
- lib/groupdate/adapters/mysql_adapter.rb
|
|
41
39
|
- lib/groupdate/adapters/postgresql_adapter.rb
|
|
42
|
-
- lib/groupdate/adapters/redshift_adapter.rb
|
|
43
40
|
- lib/groupdate/adapters/sqlite_adapter.rb
|
|
44
41
|
- lib/groupdate/enumerable.rb
|
|
45
42
|
- lib/groupdate/magic.rb
|
|
@@ -51,7 +48,6 @@ homepage: https://github.com/ankane/groupdate
|
|
|
51
48
|
licenses:
|
|
52
49
|
- MIT
|
|
53
50
|
metadata: {}
|
|
54
|
-
post_install_message:
|
|
55
51
|
rdoc_options: []
|
|
56
52
|
require_paths:
|
|
57
53
|
- lib
|
|
@@ -59,15 +55,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
59
55
|
requirements:
|
|
60
56
|
- - ">="
|
|
61
57
|
- !ruby/object:Gem::Version
|
|
62
|
-
version: '
|
|
58
|
+
version: '3.3'
|
|
63
59
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
60
|
requirements:
|
|
65
61
|
- - ">="
|
|
66
62
|
- !ruby/object:Gem::Version
|
|
67
63
|
version: '0'
|
|
68
64
|
requirements: []
|
|
69
|
-
rubygems_version:
|
|
70
|
-
signing_key:
|
|
65
|
+
rubygems_version: 4.0.6
|
|
71
66
|
specification_version: 4
|
|
72
67
|
summary: The simplest way to group temporal data
|
|
73
68
|
test_files: []
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
module Groupdate
|
|
2
|
-
module Adapters
|
|
3
|
-
class RedshiftAdapter < BaseAdapter
|
|
4
|
-
def group_clause
|
|
5
|
-
time_zone = @time_zone.tzinfo.name
|
|
6
|
-
day_start_column = "CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL ?"
|
|
7
|
-
day_start_interval = "#{day_start} second"
|
|
8
|
-
|
|
9
|
-
query =
|
|
10
|
-
case period
|
|
11
|
-
when :minute_of_hour
|
|
12
|
-
["EXTRACT(MINUTE from #{day_start_column})::integer", time_zone, day_start_interval]
|
|
13
|
-
when :hour_of_day
|
|
14
|
-
["EXTRACT(HOUR from #{day_start_column})::integer", time_zone, day_start_interval]
|
|
15
|
-
when :day_of_week
|
|
16
|
-
["EXTRACT(DOW from #{day_start_column})::integer", time_zone, day_start_interval]
|
|
17
|
-
when :day_of_month
|
|
18
|
-
["EXTRACT(DAY from #{day_start_column})::integer", time_zone, day_start_interval]
|
|
19
|
-
when :day_of_year
|
|
20
|
-
["EXTRACT(DOY from #{day_start_column})::integer", time_zone, day_start_interval]
|
|
21
|
-
when :month_of_year
|
|
22
|
-
["EXTRACT(MONTH from #{day_start_column})::integer", time_zone, day_start_interval]
|
|
23
|
-
when :week # start on Sunday, not Redshift default Monday
|
|
24
|
-
# Redshift does not return timezone information; it
|
|
25
|
-
# always says it is in UTC time, so we must convert
|
|
26
|
-
# back to UTC to play properly with the rest of Groupdate.
|
|
27
|
-
week_start_interval = "#{week_start} day"
|
|
28
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC('week', #{day_start_column} - INTERVAL ?) + INTERVAL ? + INTERVAL ?)::timestamp", time_zone, time_zone, day_start_interval, week_start_interval, week_start_interval, day_start_interval]
|
|
29
|
-
when :custom
|
|
30
|
-
raise Groupdate::Error, "Not implemented yet"
|
|
31
|
-
else
|
|
32
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?)::timestamp", time_zone, period, time_zone, day_start_interval, day_start_interval]
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
@relation.send(:sanitize_sql_array, query)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|