groupdate 5.2.2 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/LICENSE.txt +1 -1
- data/README.md +12 -17
- data/lib/groupdate/adapters/base_adapter.rb +9 -28
- data/lib/groupdate/adapters/mysql_adapter.rb +17 -10
- data/lib/groupdate/adapters/postgresql_adapter.rb +9 -7
- data/lib/groupdate/adapters/sqlite_adapter.rb +4 -4
- data/lib/groupdate/magic.rb +40 -2
- data/lib/groupdate/relation.rb +3 -0
- data/lib/groupdate/series_builder.rb +5 -52
- data/lib/groupdate/version.rb +1 -1
- data/lib/groupdate.rb +3 -5
- metadata +6 -7
- 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: 12cfc48ae509ac20872022dd07429dd7bd8f24a5bfd81dcb16242f3519cb5385
|
4
|
+
data.tar.gz: 3079f700f956f141184fbc5bc8244ad90380804cea9f15fef51300985380d13e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64b4e317731bc0032a914e4f07ca751ee276e41b7a30a2113bff9d9f2437196916c68e5f1f699c2e107861d0022b712b32315132cd0f530bb7caad5f41955e45
|
7
|
+
data.tar.gz: ea085f2e6985f4df40f591333935b88f8c59b2be87943f39241254dbbc8ac18cc1aaa2b54030880a804117ecdc0451a65463b07ea758d3fc93d99697a11a5711
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## 6.0.1 (2022-01-16)
|
2
|
+
|
3
|
+
- Fixed incorrect results (error before 6.0) with `includes` with Active Record 6.1+
|
4
|
+
|
5
|
+
## 6.0.0 (2022-01-15)
|
6
|
+
|
7
|
+
- Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
|
8
|
+
- Raise `ArgumentError` for ranges with string bounds
|
9
|
+
- Added `n` option for Redshift
|
10
|
+
- Changed SQL to return dates instead of times for day, week, month, quarter, and year
|
11
|
+
- Removed `dates` option
|
12
|
+
- Dropped support for Ruby < 2.6 and Rails < 5.2
|
13
|
+
|
14
|
+
## 5.2.4 (2021-12-15)
|
15
|
+
|
16
|
+
- Simplified queries for Active Record 7 and MySQL
|
17
|
+
|
18
|
+
## 5.2.3 (2021-12-06)
|
19
|
+
|
20
|
+
- Fixed error and warnings with Active Record 7
|
21
|
+
|
1
22
|
## 5.2.2 (2021-02-08)
|
2
23
|
|
3
24
|
- Added support for `nil..nil` ranges in `range` option
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -22,7 +22,7 @@ Supports PostgreSQL, MySQL, and Redshift, plus arrays and hashes (and limited su
|
|
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
28
|
For MySQL and SQLite, also follow [these instructions](#additional-instructions).
|
@@ -210,17 +210,12 @@ If grouping on date columns which don’t need time zone conversion, use:
|
|
210
210
|
User.group_by_week(:created_on, time_zone: false).count
|
211
211
|
```
|
212
212
|
|
213
|
-
###
|
213
|
+
### Default Scopes
|
214
214
|
|
215
|
-
If
|
215
|
+
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:
|
216
216
|
|
217
217
|
```ruby
|
218
|
-
|
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
|
218
|
+
User.unscope(:order).group_by_day(:count).count
|
224
219
|
```
|
225
220
|
|
226
221
|
## Arrays and Hashes
|
@@ -244,7 +239,7 @@ users.group_by_day(series: true) { |u| u.created_at }
|
|
244
239
|
Count
|
245
240
|
|
246
241
|
```ruby
|
247
|
-
users.group_by_day { |u| u.created_at }.
|
242
|
+
users.group_by_day { |u| u.created_at }.to_h { |k, v| [k, v.count] }
|
248
243
|
```
|
249
244
|
|
250
245
|
## Additional Instructions
|
@@ -257,8 +252,6 @@ users.group_by_day { |u| u.created_at }.map { |k, v| [k, v.count] }.to_h
|
|
257
252
|
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
|
258
253
|
```
|
259
254
|
|
260
|
-
or copy and paste [these statements](https://gist.githubusercontent.com/ankane/1d6b0022173186accbf0/raw/time_zone_support.sql) into a SQL console.
|
261
|
-
|
262
255
|
You can confirm it worked with:
|
263
256
|
|
264
257
|
```sql
|
@@ -283,13 +276,15 @@ Groupdate.time_zone = false
|
|
283
276
|
|
284
277
|
## Upgrading
|
285
278
|
|
286
|
-
###
|
279
|
+
### 6.0
|
287
280
|
|
288
|
-
Groupdate
|
281
|
+
Groupdate 6.0 protects against unsafe input by default. For non-attribute arguments, use:
|
282
|
+
|
283
|
+
```ruby
|
284
|
+
User.group_by_day(Arel.sql(known_safe_value)).count
|
285
|
+
```
|
289
286
|
|
290
|
-
|
291
|
-
- The `day_start` option is now consistent between Active Record and enumerable
|
292
|
-
- Deprecated positional arguments for time zone and range have been removed
|
287
|
+
Also, the `dates` option has been removed.
|
293
288
|
|
294
289
|
## History
|
295
290
|
|
@@ -4,11 +4,8 @@ module Groupdate
|
|
4
4
|
attr_reader :period, :column, :day_start, :week_start, :n_seconds
|
5
5
|
|
6
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
|
-
|
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
|
@@ -16,8 +13,14 @@ module Groupdate
|
|
16
13
|
@day_start = day_start
|
17
14
|
@n_seconds = n_seconds
|
18
15
|
|
19
|
-
if
|
20
|
-
|
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
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
@@ -43,28 +46,6 @@ module Groupdate
|
|
43
46
|
["#{column} IS NOT NULL"]
|
44
47
|
end
|
45
48
|
end
|
46
|
-
|
47
|
-
# basic version of Active Record disallow_raw_sql!
|
48
|
-
# symbol = column (safe), Arel node = SQL (safe), other = untrusted
|
49
|
-
# matches table.column and column
|
50
|
-
def validate_column(column)
|
51
|
-
unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
|
52
|
-
column = column.to_s
|
53
|
-
unless /\A\w+(\.\w+)?\z/i.match(column)
|
54
|
-
warn "[groupdate] Non-attribute argument: #{column}. Use Arel.sql() for known-safe values. This will raise an error in Groupdate 6"
|
55
|
-
end
|
56
|
-
end
|
57
|
-
column
|
58
|
-
end
|
59
|
-
|
60
|
-
# resolves eagerly
|
61
|
-
# need to convert both where_clause (easy)
|
62
|
-
# and group_clause (not easy) if want to avoid this
|
63
|
-
def resolve_column(relation, column)
|
64
|
-
node = relation.send(:relation).send(:arel_columns, [column]).first
|
65
|
-
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
66
|
-
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
67
|
-
end
|
68
49
|
end
|
69
50
|
end
|
70
51
|
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]
|
@@ -49,7 +55,8 @@ module Groupdate
|
|
49
55
|
end
|
50
56
|
|
51
57
|
def clean_group_clause(clause)
|
52
|
-
|
58
|
+
# zero quoted in Active Record 7+
|
59
|
+
clause.gsub(/ (\-|\+) INTERVAL 0 second/, "").gsub(/ (\-|\+) INTERVAL '0' second/, "")
|
53
60
|
end
|
54
61
|
end
|
55
62
|
end
|
@@ -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 @relation.connection.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))
|
@@ -7,7 +7,7 @@ module Groupdate
|
|
7
7
|
|
8
8
|
query =
|
9
9
|
if period == :week
|
10
|
-
["strftime('%Y-%m-%d
|
10
|
+
["strftime('%Y-%m-%d', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
|
11
11
|
elsif period == :custom
|
12
12
|
["datetime((strftime('%s', #{column}) / ?) * ?, 'unixepoch')", n_seconds, n_seconds]
|
13
13
|
else
|
@@ -32,13 +32,13 @@ module Groupdate
|
|
32
32
|
when :hour
|
33
33
|
"%Y-%m-%d %H:00:00 UTC"
|
34
34
|
when :day
|
35
|
-
"%Y-%m-%d
|
35
|
+
"%Y-%m-%d"
|
36
36
|
when :month
|
37
|
-
"%Y-%m-01
|
37
|
+
"%Y-%m-01"
|
38
38
|
when :quarter
|
39
39
|
raise Groupdate::Error, "Quarter not supported for SQLite"
|
40
40
|
else # year
|
41
|
-
"%Y-01-01
|
41
|
+
"%Y-01-01"
|
42
42
|
end
|
43
43
|
|
44
44
|
["strftime(?, #{column})", format]
|
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, :reverse]
|
26
26
|
|
27
27
|
if %i[week day_of_week].include?(period)
|
28
28
|
known_keywords << :week_start
|
@@ -145,6 +145,16 @@ module Groupdate
|
|
145
145
|
lambda { |k| k.to_i }
|
146
146
|
when :day_of_week
|
147
147
|
lambda { |k| (k.to_i - 1 - week_start) % 7 }
|
148
|
+
when :day, :week, :month, :quarter, :year
|
149
|
+
# TODO keep as date
|
150
|
+
if day_start != 0
|
151
|
+
day_start_hour = day_start / 3600
|
152
|
+
day_start_min = (day_start % 3600) / 60
|
153
|
+
day_start_sec = (day_start % 3600) % 60
|
154
|
+
lambda { |k| k.in_time_zone(time_zone).change(hour: day_start_hour, min: day_start_min, sec: day_start_sec) }
|
155
|
+
else
|
156
|
+
lambda { |k| k.in_time_zone(time_zone) }
|
157
|
+
end
|
148
158
|
else
|
149
159
|
utc = ActiveSupport::TimeZone["UTC"]
|
150
160
|
lambda { |k| (k.is_a?(String) || !k.respond_to?(:to_time) ? utc.parse(k.to_s) : k.to_time).in_time_zone(time_zone) }
|
@@ -193,11 +203,15 @@ module Groupdate
|
|
193
203
|
adapter = Groupdate.adapters[adapter_name]
|
194
204
|
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}" unless adapter
|
195
205
|
|
206
|
+
# very important
|
207
|
+
column = validate_column(field)
|
208
|
+
column = resolve_column(relation, column)
|
209
|
+
|
196
210
|
# generate ActiveRecord relation
|
197
211
|
relation =
|
198
212
|
adapter.new(
|
199
213
|
relation,
|
200
|
-
column:
|
214
|
+
column: column,
|
201
215
|
period: magic.period,
|
202
216
|
time_zone: magic.time_zone,
|
203
217
|
time_range: magic.time_range,
|
@@ -213,6 +227,30 @@ module Groupdate
|
|
213
227
|
relation
|
214
228
|
end
|
215
229
|
|
230
|
+
class << self
|
231
|
+
# basic version of Active Record disallow_raw_sql!
|
232
|
+
# symbol = column (safe), Arel node = SQL (safe), other = untrusted
|
233
|
+
# matches table.column and column
|
234
|
+
def validate_column(column)
|
235
|
+
unless column.is_a?(Symbol) || column.is_a?(Arel::Nodes::SqlLiteral)
|
236
|
+
column = column.to_s
|
237
|
+
unless /\A\w+(\.\w+)?\z/i.match(column)
|
238
|
+
raise ActiveRecord::UnknownAttributeReference, "Query method called with non-attribute argument(s): #{column.inspect}. Use Arel.sql() for known-safe values."
|
239
|
+
end
|
240
|
+
end
|
241
|
+
column
|
242
|
+
end
|
243
|
+
|
244
|
+
# resolves eagerly
|
245
|
+
# need to convert both where_clause (easy)
|
246
|
+
# and group_clause (not easy) if want to avoid this
|
247
|
+
def resolve_column(relation, column)
|
248
|
+
node = relation.send(:relation).send(:arel_columns, [column]).first
|
249
|
+
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
250
|
+
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
216
254
|
# allow any options to keep flexible for future
|
217
255
|
def self.process_result(relation, result, **options)
|
218
256
|
relation.groupdate_values.reverse.each do |gv|
|
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 ActiveRecord::VERSION::STRING.to_f >= 6.1 && 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
|
@@ -81,7 +50,7 @@ module Groupdate
|
|
81
50
|
|
82
51
|
if day_start != 0
|
83
52
|
# apply day_start to a time object that's not affected by DST
|
84
|
-
time =
|
53
|
+
time = time.change(zone: utc)
|
85
54
|
time -= day_start.seconds
|
86
55
|
end
|
87
56
|
|
@@ -121,23 +90,12 @@ module Groupdate
|
|
121
90
|
|
122
91
|
if day_start != 0 && time.is_a?(Time)
|
123
92
|
time += day_start.seconds
|
124
|
-
time =
|
93
|
+
time = time.change(zone: time_zone)
|
125
94
|
end
|
126
95
|
|
127
96
|
time
|
128
97
|
end
|
129
98
|
|
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
99
|
def time_range
|
142
100
|
@time_range ||= begin
|
143
101
|
time_range = options[:range]
|
@@ -148,10 +106,6 @@ module Groupdate
|
|
148
106
|
case v
|
149
107
|
when nil, Date, Time
|
150
108
|
# 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
109
|
else
|
156
110
|
raise ArgumentError, "Range bounds should be Date or Time, not #{v.class.name}"
|
157
111
|
end
|
@@ -289,7 +243,6 @@ module Groupdate
|
|
289
243
|
def key_format
|
290
244
|
@key_format ||= begin
|
291
245
|
locale = options[:locale] || I18n.locale
|
292
|
-
use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
|
293
246
|
|
294
247
|
if options[:format]
|
295
248
|
if options[:format].respond_to?(:call)
|
@@ -312,7 +265,7 @@ module Groupdate
|
|
312
265
|
I18n.localize(key, format: options[:format], locale: locale)
|
313
266
|
end
|
314
267
|
end
|
315
|
-
elsif [:day, :week, :month, :quarter, :year].include?(period)
|
268
|
+
elsif [:day, :week, :month, :quarter, :year].include?(period)
|
316
269
|
lambda { |k| k.to_date }
|
317
270
|
else
|
318
271
|
lambda { |k| k }
|
data/lib/groupdate/version.rb
CHANGED
data/lib/groupdate.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# dependencies
|
2
|
+
require "active_support"
|
2
3
|
require "active_support/core_ext/module/attribute_accessors"
|
3
4
|
require "active_support/time"
|
4
5
|
|
@@ -11,7 +12,6 @@ require "groupdate/version"
|
|
11
12
|
require "groupdate/adapters/base_adapter"
|
12
13
|
require "groupdate/adapters/mysql_adapter"
|
13
14
|
require "groupdate/adapters/postgresql_adapter"
|
14
|
-
require "groupdate/adapters/redshift_adapter"
|
15
15
|
require "groupdate/adapters/sqlite_adapter"
|
16
16
|
|
17
17
|
module Groupdate
|
@@ -20,10 +20,9 @@ module Groupdate
|
|
20
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]
|
21
21
|
METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
|
22
22
|
|
23
|
-
mattr_accessor :week_start, :day_start, :time_zone
|
23
|
+
mattr_accessor :week_start, :day_start, :time_zone
|
24
24
|
self.week_start = :sunday
|
25
25
|
self.day_start = 0
|
26
|
-
self.dates = true
|
27
26
|
|
28
27
|
# api for gems like ActiveMedian
|
29
28
|
def self.process_result(relation, result, **options)
|
@@ -45,8 +44,7 @@ module Groupdate
|
|
45
44
|
end
|
46
45
|
|
47
46
|
Groupdate.register_adapter ["Mysql2", "Mysql2Spatial", "Mysql2Rgeo"], Groupdate::Adapters::MySQLAdapter
|
48
|
-
Groupdate.register_adapter ["PostgreSQL", "PostGIS"], Groupdate::Adapters::PostgreSQLAdapter
|
49
|
-
Groupdate.register_adapter "Redshift", Groupdate::Adapters::RedshiftAdapter
|
47
|
+
Groupdate.register_adapter ["PostgreSQL", "PostGIS", "Redshift"], Groupdate::Adapters::PostgreSQLAdapter
|
50
48
|
Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
|
51
49
|
|
52
50
|
require "groupdate/enumerable"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: groupdate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 6.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '5'
|
19
|
+
version: '5.2'
|
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: '5'
|
26
|
+
version: '5.2'
|
27
27
|
description:
|
28
28
|
email: andrew@ankane.org
|
29
29
|
executables: []
|
@@ -39,7 +39,6 @@ files:
|
|
39
39
|
- lib/groupdate/adapters/base_adapter.rb
|
40
40
|
- lib/groupdate/adapters/mysql_adapter.rb
|
41
41
|
- lib/groupdate/adapters/postgresql_adapter.rb
|
42
|
-
- lib/groupdate/adapters/redshift_adapter.rb
|
43
42
|
- lib/groupdate/adapters/sqlite_adapter.rb
|
44
43
|
- lib/groupdate/enumerable.rb
|
45
44
|
- lib/groupdate/magic.rb
|
@@ -59,14 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
58
|
requirements:
|
60
59
|
- - ">="
|
61
60
|
- !ruby/object:Gem::Version
|
62
|
-
version: '2.
|
61
|
+
version: '2.6'
|
63
62
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
63
|
requirements:
|
65
64
|
- - ">="
|
66
65
|
- !ruby/object:Gem::Version
|
67
66
|
version: '0'
|
68
67
|
requirements: []
|
69
|
-
rubygems_version: 3.2.
|
68
|
+
rubygems_version: 3.2.32
|
70
69
|
signing_key:
|
71
70
|
specification_version: 4
|
72
71
|
summary: The simplest way to group temporal data
|
@@ -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
|