groupdate 4.3.0 → 5.2.2
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 +31 -0
- data/LICENSE.txt +1 -1
- data/README.md +27 -36
- data/lib/groupdate.rb +28 -4
- data/lib/groupdate/adapters/base_adapter.rb +70 -0
- data/lib/groupdate/adapters/mysql_adapter.rb +56 -0
- data/lib/groupdate/adapters/postgresql_adapter.rb +44 -0
- data/lib/groupdate/adapters/redshift_adapter.rb +39 -0
- data/lib/groupdate/adapters/sqlite_adapter.rb +51 -0
- data/lib/groupdate/enumerable.rb +8 -15
- data/lib/groupdate/magic.rb +72 -11
- data/lib/groupdate/query_methods.rb +3 -12
- data/lib/groupdate/series_builder.rb +150 -71
- data/lib/groupdate/version.rb +1 -1
- metadata +13 -107
- data/lib/groupdate/relation_builder.rb +0 -194
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05ffc10283f2de5fa74bacff7a6368636f62e8ddb263dd33d191502c0b418edc
|
|
4
|
+
data.tar.gz: 887827ef96299b9916f97e554706e670e83c99dad2522755c760348bdd3077cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d8cdf01f590ff7575539e39e9207e14eac7bd04417483fe453a7f1fb281e9bf9869678fbb7e2cc2381ccdfb678f47c6ca82b644ea291149ed48fdfba61963a3
|
|
7
|
+
data.tar.gz: 190dd79d14e7b00a7aa5492a46cfa18d50b56af08bd489fc8e68560530e0edb91a124932a24444116dd0cda09c926834c9ac33ed2b4b8c5117601e24b2ee0eac
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
## 5.2.2 (2021-02-08)
|
|
2
|
+
|
|
3
|
+
- Added support for `nil..nil` ranges in `range` option
|
|
4
|
+
|
|
5
|
+
## 5.2.1 (2020-09-09)
|
|
6
|
+
|
|
7
|
+
- Improved error message for invalid ranges
|
|
8
|
+
- Fixed bug with date string ranges
|
|
9
|
+
|
|
10
|
+
## 5.2.0 (2020-09-07)
|
|
11
|
+
|
|
12
|
+
- Added warning for non-attribute argument
|
|
13
|
+
- Added support for beginless and endless ranges in `range` option
|
|
14
|
+
|
|
15
|
+
## 5.1.0 (2020-07-30)
|
|
16
|
+
|
|
17
|
+
- Added `n` option to minute and second for custom durations
|
|
18
|
+
|
|
19
|
+
## 5.0.0 (2020-02-18)
|
|
20
|
+
|
|
21
|
+
- Added support for `week_start` for SQLite
|
|
22
|
+
- Added support for full weekday names
|
|
23
|
+
- Made `day_start` behavior consistent between Active Record and enumerable
|
|
24
|
+
- Made `last` option extend to end of current period
|
|
25
|
+
- Raise error when `day_start` and `week_start` passed to unsupported methods
|
|
26
|
+
- The `day_start` option no longer applies to shorter periods
|
|
27
|
+
- Fixed `inconsistent time zone info` errors around DST with MySQL and PostgreSQL
|
|
28
|
+
- Improved performance of `format` option
|
|
29
|
+
- Removed deprecated positional arguments for time zone and range
|
|
30
|
+
- Dropped support for `mysql` gem (last release was 2013)
|
|
31
|
+
|
|
1
32
|
## 4.3.0 (2019-12-26)
|
|
2
33
|
|
|
3
34
|
- Fixed error with empty results in Ruby 2.7
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Supports PostgreSQL, MySQL, and Redshift, plus arrays and hashes (and limited su
|
|
|
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
|
|
|
@@ -32,9 +32,9 @@ For MySQL and SQLite, also follow [these instructions](#additional-instructions)
|
|
|
32
32
|
```ruby
|
|
33
33
|
User.group_by_day(:created_at).count
|
|
34
34
|
# {
|
|
35
|
-
# Sat,
|
|
36
|
-
# Sun,
|
|
37
|
-
# Mon,
|
|
35
|
+
# Sat, 24 May 2020 => 50,
|
|
36
|
+
# Sun, 25 May 2020 => 100,
|
|
37
|
+
# Mon, 26 May 2020 => 34
|
|
38
38
|
# }
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -53,13 +53,14 @@ You can group by:
|
|
|
53
53
|
|
|
54
54
|
and
|
|
55
55
|
|
|
56
|
+
- minute_of_hour
|
|
56
57
|
- hour_of_day
|
|
57
58
|
- day_of_week (Sunday = 0, Monday = 1, etc)
|
|
58
59
|
- day_of_month
|
|
59
60
|
- day_of_year
|
|
60
61
|
- month_of_year
|
|
61
62
|
|
|
62
|
-
Use it anywhere you can use `group`. Works with `count`, `sum`, `minimum`, `maximum`, and `average`. For `median`, check out [ActiveMedian](https://github.com/ankane/active_median).
|
|
63
|
+
Use it anywhere you can use `group`. Works with `count`, `sum`, `minimum`, `maximum`, and `average`. For `median` and `percentile`, check out [ActiveMedian](https://github.com/ankane/active_median).
|
|
63
64
|
|
|
64
65
|
### Time Zones
|
|
65
66
|
|
|
@@ -74,9 +75,9 @@ or
|
|
|
74
75
|
```ruby
|
|
75
76
|
User.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
|
|
76
77
|
# {
|
|
77
|
-
# Sun,
|
|
78
|
-
# Sun,
|
|
79
|
-
# Sun,
|
|
78
|
+
# Sun, 08 Mar 2020 => 70,
|
|
79
|
+
# Sun, 15 Mar 2020 => 54,
|
|
80
|
+
# Sun, 22 Mar 2020 => 80
|
|
80
81
|
# }
|
|
81
82
|
```
|
|
82
83
|
|
|
@@ -87,13 +88,13 @@ Time zone objects also work. To see a list of available time zones in Rails, run
|
|
|
87
88
|
Weeks start on Sunday by default. Change this with:
|
|
88
89
|
|
|
89
90
|
```ruby
|
|
90
|
-
Groupdate.week_start = :
|
|
91
|
+
Groupdate.week_start = :monday
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
or
|
|
94
95
|
|
|
95
96
|
```ruby
|
|
96
|
-
User.group_by_week(:created_at, week_start: :
|
|
97
|
+
User.group_by_week(:created_at, week_start: :monday).count
|
|
97
98
|
```
|
|
98
99
|
|
|
99
100
|
### Day Start
|
|
@@ -147,8 +148,8 @@ To get keys in a different format, use:
|
|
|
147
148
|
```ruby
|
|
148
149
|
User.group_by_month(:created_at, format: "%b %Y").count
|
|
149
150
|
# {
|
|
150
|
-
# "Jan
|
|
151
|
-
# "Feb
|
|
151
|
+
# "Jan 2020" => 10
|
|
152
|
+
# "Feb 2020" => 12
|
|
152
153
|
# }
|
|
153
154
|
```
|
|
154
155
|
|
|
@@ -193,6 +194,14 @@ User.group_by_period(params[:period], :created_at, permit: ["day", "week"]).coun
|
|
|
193
194
|
|
|
194
195
|
Raises an `ArgumentError` for unpermitted periods.
|
|
195
196
|
|
|
197
|
+
### Custom Duration
|
|
198
|
+
|
|
199
|
+
To group by a specific number of minutes or seconds, use:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
User.group_by_minute(:created_at, n: 10).count # 10 minutes
|
|
203
|
+
```
|
|
204
|
+
|
|
196
205
|
### Date Columns
|
|
197
206
|
|
|
198
207
|
If grouping on date columns which don’t need time zone conversion, use:
|
|
@@ -263,7 +272,7 @@ It should return the time instead of `NULL`.
|
|
|
263
272
|
Groupdate has limited support for SQLite.
|
|
264
273
|
|
|
265
274
|
- No time zone support
|
|
266
|
-
- No `day_start`
|
|
275
|
+
- No `day_start` option
|
|
267
276
|
- No `group_by_quarter` method
|
|
268
277
|
|
|
269
278
|
If your application’s time zone is set to something other than `Etc/UTC` (the default), create an initializer with:
|
|
@@ -274,36 +283,18 @@ Groupdate.time_zone = false
|
|
|
274
283
|
|
|
275
284
|
## Upgrading
|
|
276
285
|
|
|
277
|
-
###
|
|
278
|
-
|
|
279
|
-
Groupdate 4.0 brings a number of improvements. Here are a few to be aware of:
|
|
286
|
+
### 5.0
|
|
280
287
|
|
|
281
|
-
|
|
282
|
-
- Invalid options now throw an `ArgumentError`
|
|
283
|
-
- `week_start` now affects `day_of_week`
|
|
284
|
-
- Custom calculation methods are supported by default
|
|
288
|
+
Groupdate 5.0 brings a number of improvements. Here are a few to be aware of:
|
|
285
289
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
- `Date` objects are now returned for day, week, month, quarter, and year by default. Use `dates: false` for the previous behavior, or change this globally with `Groupdate.dates = false`.
|
|
291
|
-
- Array and hash methods no longer return the entire series by default. Use `series: true` for the previous behavior.
|
|
292
|
-
- The `series: false` option now returns the correct type and order, and plays nicely with other options.
|
|
293
|
-
|
|
294
|
-
### 2.0
|
|
295
|
-
|
|
296
|
-
Groupdate 2.0 brings a number of improvements. Here are two things to be aware of:
|
|
297
|
-
|
|
298
|
-
- the entire series is returned by default
|
|
299
|
-
- `ActiveSupport::TimeWithZone` keys are now returned for every database adapter - adapters previously returned `Time` or `String` keys
|
|
290
|
+
- The `week_start` option is now supported for SQLite
|
|
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
|
|
300
293
|
|
|
301
294
|
## History
|
|
302
295
|
|
|
303
296
|
View the [changelog](https://github.com/ankane/groupdate/blob/master/CHANGELOG.md)
|
|
304
297
|
|
|
305
|
-
Groupdate follows [Semantic Versioning](https://semver.org/)
|
|
306
|
-
|
|
307
298
|
## Contributing
|
|
308
299
|
|
|
309
300
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
data/lib/groupdate.rb
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
# dependencies
|
|
1
2
|
require "active_support/core_ext/module/attribute_accessors"
|
|
2
3
|
require "active_support/time"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require "groupdate/series_builder"
|
|
4
|
+
|
|
5
|
+
# modules
|
|
6
6
|
require "groupdate/magic"
|
|
7
|
+
require "groupdate/series_builder"
|
|
8
|
+
require "groupdate/version"
|
|
9
|
+
|
|
10
|
+
# adapters
|
|
11
|
+
require "groupdate/adapters/base_adapter"
|
|
12
|
+
require "groupdate/adapters/mysql_adapter"
|
|
13
|
+
require "groupdate/adapters/postgresql_adapter"
|
|
14
|
+
require "groupdate/adapters/redshift_adapter"
|
|
15
|
+
require "groupdate/adapters/sqlite_adapter"
|
|
7
16
|
|
|
8
17
|
module Groupdate
|
|
9
18
|
class Error < RuntimeError; end
|
|
@@ -12,7 +21,7 @@ module Groupdate
|
|
|
12
21
|
METHODS = PERIODS.map { |v| :"group_by_#{v}" } + [:group_by_period]
|
|
13
22
|
|
|
14
23
|
mattr_accessor :week_start, :day_start, :time_zone, :dates
|
|
15
|
-
self.week_start = :
|
|
24
|
+
self.week_start = :sunday
|
|
16
25
|
self.day_start = 0
|
|
17
26
|
self.dates = true
|
|
18
27
|
|
|
@@ -23,8 +32,23 @@ module Groupdate
|
|
|
23
32
|
end
|
|
24
33
|
result
|
|
25
34
|
end
|
|
35
|
+
|
|
36
|
+
def self.adapters
|
|
37
|
+
@adapters ||= {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.register_adapter(name, adapter)
|
|
41
|
+
Array(name).each do |n|
|
|
42
|
+
adapters[n] = adapter
|
|
43
|
+
end
|
|
44
|
+
end
|
|
26
45
|
end
|
|
27
46
|
|
|
47
|
+
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
|
|
50
|
+
Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
|
|
51
|
+
|
|
28
52
|
require "groupdate/enumerable"
|
|
29
53
|
|
|
30
54
|
ActiveSupport.on_load(:active_record) do
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Groupdate
|
|
2
|
+
module Adapters
|
|
3
|
+
class BaseAdapter
|
|
4
|
+
attr_reader :period, :column, :day_start, :week_start, :n_seconds
|
|
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
|
+
|
|
10
|
+
@relation = relation
|
|
11
|
+
@column = resolve_column(relation, column)
|
|
12
|
+
@period = period
|
|
13
|
+
@time_zone = time_zone
|
|
14
|
+
@time_range = time_range
|
|
15
|
+
@week_start = week_start
|
|
16
|
+
@day_start = day_start
|
|
17
|
+
@n_seconds = n_seconds
|
|
18
|
+
|
|
19
|
+
if relation.default_timezone == :local
|
|
20
|
+
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def generate
|
|
25
|
+
@relation.group(group_clause).where(*where_clause)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def where_clause
|
|
31
|
+
if @time_range.is_a?(Range)
|
|
32
|
+
if @time_range.end
|
|
33
|
+
op = @time_range.exclude_end? ? "<" : "<="
|
|
34
|
+
if @time_range.begin
|
|
35
|
+
["#{column} >= ? AND #{column} #{op} ?", @time_range.begin, @time_range.end]
|
|
36
|
+
else
|
|
37
|
+
["#{column} #{op} ?", @time_range.end]
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
["#{column} >= ?", @time_range.begin]
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
["#{column} IS NOT NULL"]
|
|
44
|
+
end
|
|
45
|
+
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
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Groupdate
|
|
2
|
+
module Adapters
|
|
3
|
+
class MySQLAdapter < BaseAdapter
|
|
4
|
+
def group_clause
|
|
5
|
+
time_zone = @time_zone.tzinfo.name
|
|
6
|
+
day_start_column = "CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL ? second"
|
|
7
|
+
|
|
8
|
+
query =
|
|
9
|
+
case period
|
|
10
|
+
when :minute_of_hour
|
|
11
|
+
["MINUTE(#{day_start_column})", time_zone, day_start]
|
|
12
|
+
when :hour_of_day
|
|
13
|
+
["HOUR(#{day_start_column})", time_zone, day_start]
|
|
14
|
+
when :day_of_week
|
|
15
|
+
["DAYOFWEEK(#{day_start_column}) - 1", time_zone, day_start]
|
|
16
|
+
when :day_of_month
|
|
17
|
+
["DAYOFMONTH(#{day_start_column})", time_zone, day_start]
|
|
18
|
+
when :day_of_year
|
|
19
|
+
["DAYOFYEAR(#{day_start_column})", time_zone, day_start]
|
|
20
|
+
when :month_of_year
|
|
21
|
+
["MONTH(#{day_start_column})", time_zone, day_start]
|
|
22
|
+
when :week
|
|
23
|
+
["CONVERT_TZ(DATE_FORMAT(#{day_start_column} - INTERVAL ((? + DAYOFWEEK(#{day_start_column})) % 7) DAY, '%Y-%m-%d 00:00:00') + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, 12 - week_start, time_zone, day_start, day_start, time_zone]
|
|
24
|
+
when :quarter
|
|
25
|
+
["CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(YEAR(#{day_start_column}), '-', LPAD(1 + 3 * (QUARTER(#{day_start_column}) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S') + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, time_zone, day_start, day_start, time_zone]
|
|
26
|
+
when :custom
|
|
27
|
+
["FROM_UNIXTIME((UNIX_TIMESTAMP(#{column}) DIV ?) * ?)", n_seconds, n_seconds]
|
|
28
|
+
else
|
|
29
|
+
format =
|
|
30
|
+
case period
|
|
31
|
+
when :second
|
|
32
|
+
"%Y-%m-%d %H:%i:%S"
|
|
33
|
+
when :minute
|
|
34
|
+
"%Y-%m-%d %H:%i:00"
|
|
35
|
+
when :hour
|
|
36
|
+
"%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
|
+
end
|
|
44
|
+
|
|
45
|
+
["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
clean_group_clause(@relation.send(:sanitize_sql_array, query))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def clean_group_clause(clause)
|
|
52
|
+
clause.gsub(/ (\-|\+) INTERVAL 0 second/, "")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Groupdate
|
|
2
|
+
module Adapters
|
|
3
|
+
class PostgreSQLAdapter < BaseAdapter
|
|
4
|
+
def group_clause
|
|
5
|
+
time_zone = @time_zone.tzinfo.name
|
|
6
|
+
day_start_column = "#{column}::timestamptz AT TIME ZONE ? - 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
|
|
24
|
+
["(DATE_TRUNC('day', #{day_start_column} - INTERVAL '1 day' * ((? + EXTRACT(DOW FROM #{day_start_column})::integer) % 7)) + INTERVAL ?) AT TIME ZONE ?", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval, time_zone]
|
|
25
|
+
when :custom
|
|
26
|
+
["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
|
|
27
|
+
else
|
|
28
|
+
if day_start == 0
|
|
29
|
+
# prettier
|
|
30
|
+
["DATE_TRUNC(?, #{day_start_column}) AT TIME ZONE ?", period, time_zone, day_start_interval, time_zone]
|
|
31
|
+
else
|
|
32
|
+
["(DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?) AT TIME ZONE ?", period, time_zone, day_start_interval, day_start_interval, time_zone]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
clean_group_clause(@relation.send(:sanitize_sql_array, query))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clean_group_clause(clause)
|
|
40
|
+
clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|