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
data/lib/groupdate/version.rb
CHANGED
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: 5.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -24,106 +24,8 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '5'
|
27
|
-
|
28
|
-
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - ">="
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - ">="
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rake
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: minitest
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: activerecord
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: pg
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: mysql2
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: sqlite3
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - ">="
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
125
|
-
description:
|
126
|
-
email: andrew@chartkick.com
|
27
|
+
description:
|
28
|
+
email: andrew@ankane.org
|
127
29
|
executables: []
|
128
30
|
extensions: []
|
129
31
|
extra_rdoc_files: []
|
@@ -134,18 +36,22 @@ files:
|
|
134
36
|
- README.md
|
135
37
|
- lib/groupdate.rb
|
136
38
|
- lib/groupdate/active_record.rb
|
39
|
+
- lib/groupdate/adapters/base_adapter.rb
|
40
|
+
- lib/groupdate/adapters/mysql_adapter.rb
|
41
|
+
- lib/groupdate/adapters/postgresql_adapter.rb
|
42
|
+
- lib/groupdate/adapters/redshift_adapter.rb
|
43
|
+
- lib/groupdate/adapters/sqlite_adapter.rb
|
137
44
|
- lib/groupdate/enumerable.rb
|
138
45
|
- lib/groupdate/magic.rb
|
139
46
|
- lib/groupdate/query_methods.rb
|
140
47
|
- lib/groupdate/relation.rb
|
141
|
-
- lib/groupdate/relation_builder.rb
|
142
48
|
- lib/groupdate/series_builder.rb
|
143
49
|
- lib/groupdate/version.rb
|
144
50
|
homepage: https://github.com/ankane/groupdate
|
145
51
|
licenses:
|
146
52
|
- MIT
|
147
53
|
metadata: {}
|
148
|
-
post_install_message:
|
54
|
+
post_install_message:
|
149
55
|
rdoc_options: []
|
150
56
|
require_paths:
|
151
57
|
- lib
|
@@ -160,8 +66,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
160
66
|
- !ruby/object:Gem::Version
|
161
67
|
version: '0'
|
162
68
|
requirements: []
|
163
|
-
rubygems_version: 3.
|
164
|
-
signing_key:
|
69
|
+
rubygems_version: 3.2.3
|
70
|
+
signing_key:
|
165
71
|
specification_version: 4
|
166
72
|
summary: The simplest way to group temporal data
|
167
73
|
test_files: []
|
@@ -1,194 +0,0 @@
|
|
1
|
-
module Groupdate
|
2
|
-
class RelationBuilder
|
3
|
-
attr_reader :period, :column, :day_start, :week_start
|
4
|
-
|
5
|
-
def initialize(relation, column:, period:, time_zone:, time_range:, week_start:, day_start:)
|
6
|
-
@relation = relation
|
7
|
-
@column = resolve_column(relation, column)
|
8
|
-
@period = period
|
9
|
-
@time_zone = time_zone
|
10
|
-
@time_range = time_range
|
11
|
-
@week_start = week_start
|
12
|
-
@day_start = day_start
|
13
|
-
|
14
|
-
if relation.default_timezone == :local
|
15
|
-
raise Groupdate::Error, "ActiveRecord::Base.default_timezone must be :utc to use Groupdate"
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def generate
|
20
|
-
@relation.group(group_clause).where(*where_clause)
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def group_clause
|
26
|
-
time_zone = @time_zone.tzinfo.name
|
27
|
-
adapter_name = @relation.connection.adapter_name
|
28
|
-
query =
|
29
|
-
case adapter_name
|
30
|
-
when "MySQL", "Mysql2", "Mysql2Spatial", 'Mysql2Rgeo'
|
31
|
-
case period
|
32
|
-
when :day_of_week
|
33
|
-
["DAYOFWEEK(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1", time_zone]
|
34
|
-
when :day_of_year
|
35
|
-
["DAYOFYEAR(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
36
|
-
when :hour_of_day
|
37
|
-
["(EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?)) + 24 - #{day_start / 3600}) % 24", time_zone]
|
38
|
-
when :minute_of_hour
|
39
|
-
["(EXTRACT(MINUTE from CONVERT_TZ(#{column}, '+00:00', ?)))", time_zone]
|
40
|
-
when :day_of_month
|
41
|
-
["DAYOFMONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
42
|
-
when :month_of_year
|
43
|
-
["MONTH(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?))", time_zone]
|
44
|
-
when :week
|
45
|
-
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL ((#{7 - week_start} + WEEKDAY(CONVERT_TZ(#{column}, '+00:00', ?) - INTERVAL #{day_start} second)) % 7) DAY) - INTERVAL #{day_start} second, '+00:00', ?), '%Y-%m-%d 00:00:00') + INTERVAL #{day_start} second, ?, '+00:00')", time_zone, time_zone, time_zone]
|
46
|
-
when :quarter
|
47
|
-
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(DATE(CONCAT(EXTRACT(YEAR FROM CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)), '-', LPAD(1 + 3 * (QUARTER(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?)) - 1), 2, '00'), '-01')), '%Y-%m-%d %H:%i:%S'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone, time_zone]
|
48
|
-
else
|
49
|
-
format =
|
50
|
-
case period
|
51
|
-
when :second
|
52
|
-
"%Y-%m-%d %H:%i:%S"
|
53
|
-
when :minute
|
54
|
-
"%Y-%m-%d %H:%i:00"
|
55
|
-
when :hour
|
56
|
-
"%Y-%m-%d %H:00:00"
|
57
|
-
when :day
|
58
|
-
"%Y-%m-%d 00:00:00"
|
59
|
-
when :month
|
60
|
-
"%Y-%m-01 00:00:00"
|
61
|
-
else # year
|
62
|
-
"%Y-01-01 00:00:00"
|
63
|
-
end
|
64
|
-
|
65
|
-
["DATE_ADD(CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL #{day_start} second), '+00:00', ?), '#{format}'), ?, '+00:00'), INTERVAL #{day_start} second)", time_zone, time_zone]
|
66
|
-
end
|
67
|
-
when "PostgreSQL", "PostGIS"
|
68
|
-
case period
|
69
|
-
when :day_of_week
|
70
|
-
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
71
|
-
when :day_of_year
|
72
|
-
["EXTRACT(DOY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
73
|
-
when :hour_of_day
|
74
|
-
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
75
|
-
when :minute_of_hour
|
76
|
-
["EXTRACT(MINUTE from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
77
|
-
when :day_of_month
|
78
|
-
["EXTRACT(DAY from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
79
|
-
when :month_of_year
|
80
|
-
["EXTRACT(MONTH from #{column}::timestamptz AT TIME ZONE ? - INTERVAL '#{day_start} second')::integer", time_zone]
|
81
|
-
when :week # start on Sunday, not PostgreSQL default Monday
|
82
|
-
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
83
|
-
else
|
84
|
-
["(DATE_TRUNC('#{period}', (#{column}::timestamptz - INTERVAL '#{day_start} second') AT TIME ZONE ?) + INTERVAL '#{day_start} second') AT TIME ZONE ?", time_zone, time_zone]
|
85
|
-
end
|
86
|
-
when "SQLite"
|
87
|
-
raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
|
88
|
-
raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
|
89
|
-
raise Groupdate::Error, "week_start not supported for SQLite" unless week_start == 6
|
90
|
-
|
91
|
-
if period == :week
|
92
|
-
["strftime('%%Y-%%m-%%d 00:00:00 UTC', #{column}, '-6 days', 'weekday 0')"]
|
93
|
-
else
|
94
|
-
format =
|
95
|
-
case period
|
96
|
-
when :hour_of_day
|
97
|
-
"%H"
|
98
|
-
when :minute_of_hour
|
99
|
-
"%M"
|
100
|
-
when :day_of_week
|
101
|
-
"%w"
|
102
|
-
when :day_of_month
|
103
|
-
"%d"
|
104
|
-
when :month_of_year
|
105
|
-
"%m"
|
106
|
-
when :day_of_year
|
107
|
-
"%j"
|
108
|
-
when :second
|
109
|
-
"%Y-%m-%d %H:%M:%S UTC"
|
110
|
-
when :minute
|
111
|
-
"%Y-%m-%d %H:%M:00 UTC"
|
112
|
-
when :hour
|
113
|
-
"%Y-%m-%d %H:00:00 UTC"
|
114
|
-
when :day
|
115
|
-
"%Y-%m-%d 00:00:00 UTC"
|
116
|
-
when :month
|
117
|
-
"%Y-%m-01 00:00:00 UTC"
|
118
|
-
when :quarter
|
119
|
-
raise Groupdate::Error, "Quarter not supported for SQLite"
|
120
|
-
else # year
|
121
|
-
"%Y-01-01 00:00:00 UTC"
|
122
|
-
end
|
123
|
-
|
124
|
-
["strftime('#{format.gsub(/%/, '%%')}', #{column})"]
|
125
|
-
end
|
126
|
-
when "Redshift"
|
127
|
-
case period
|
128
|
-
when :day_of_week
|
129
|
-
["EXTRACT(DOW from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
130
|
-
when :hour_of_day
|
131
|
-
["EXTRACT(HOUR from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
132
|
-
when :minute_of_hour
|
133
|
-
["EXTRACT(MINUTE from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
134
|
-
when :day_of_month
|
135
|
-
["EXTRACT(DAY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
136
|
-
when :day_of_year
|
137
|
-
["EXTRACT(DOY from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
138
|
-
when :month_of_year
|
139
|
-
["EXTRACT(MONTH from CONVERT_TIMEZONE(?, #{column}::timestamp) - INTERVAL '#{day_start} second')::integer", time_zone]
|
140
|
-
when :week # start on Sunday, not Redshift default Monday
|
141
|
-
# Redshift does not return timezone information; it
|
142
|
-
# always says it is in UTC time, so we must convert
|
143
|
-
# back to UTC to play properly with the rest of Groupdate.
|
144
|
-
#
|
145
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{week_start} day' - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{week_start} day' + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
146
|
-
else
|
147
|
-
["CONVERT_TIMEZONE(?, 'Etc/UTC', DATE_TRUNC(?, CONVERT_TIMEZONE(?, #{column}) - INTERVAL '#{day_start} second'))::timestamp + INTERVAL '#{day_start} second'", time_zone, period, time_zone]
|
148
|
-
end
|
149
|
-
else
|
150
|
-
raise Groupdate::Error, "Connection adapter not supported: #{adapter_name}"
|
151
|
-
end
|
152
|
-
|
153
|
-
if adapter_name == "MySQL" && period == :week
|
154
|
-
query[0] = "CAST(#{query[0]} AS DATETIME)"
|
155
|
-
end
|
156
|
-
|
157
|
-
clause = @relation.send(:sanitize_sql_array, query)
|
158
|
-
|
159
|
-
# cleaner queries in logs
|
160
|
-
clause = clean_group_clause_postgresql(clause)
|
161
|
-
clean_group_clause_mysql(clause)
|
162
|
-
end
|
163
|
-
|
164
|
-
def clean_group_clause_postgresql(clause)
|
165
|
-
clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
|
166
|
-
end
|
167
|
-
|
168
|
-
def clean_group_clause_mysql(clause)
|
169
|
-
clause = clause.gsub("DATE_SUB(#{column}, INTERVAL 0 second)", "#{column}")
|
170
|
-
if clause.start_with?("DATE_ADD(") && clause.end_with?(", INTERVAL 0 second)")
|
171
|
-
clause = clause[9..-21]
|
172
|
-
end
|
173
|
-
clause
|
174
|
-
end
|
175
|
-
|
176
|
-
def where_clause
|
177
|
-
if @time_range.is_a?(Range)
|
178
|
-
op = @time_range.exclude_end? ? "<" : "<="
|
179
|
-
["#{column} >= ? AND #{column} #{op} ?", @time_range.first, @time_range.last]
|
180
|
-
else
|
181
|
-
["#{column} IS NOT NULL"]
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# resolves eagerly
|
186
|
-
# need to convert both where_clause (easy)
|
187
|
-
# and group_clause (not easy) if want to avoid this
|
188
|
-
def resolve_column(relation, column)
|
189
|
-
node = relation.send(:relation).send(:arel_columns, [column]).first
|
190
|
-
node = Arel::Nodes::SqlLiteral.new(node) if node.is_a?(String)
|
191
|
-
relation.connection.visitor.accept(node, Arel::Collectors::SQLString.new).value
|
192
|
-
end
|
193
|
-
end
|
194
|
-
end
|