groupdate 5.2.4 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b6c83f192d439e35025586a076762413d80daab15b85e81561404bdf078acad
4
- data.tar.gz: 382dd612bbd1db8e131345981576b7a6c61c525d8a991fb8a3fc60678ce8d6ef
3
+ metadata.gz: 95d89de45bf1e6828b313f8181a5b68ba1ba7c32b173bc0a9c15290e7aec9cd6
4
+ data.tar.gz: e39b910219e23681dac05095ae0a6a81de70e846f7bc089bb45d0bd3a4849211
5
5
  SHA512:
6
- metadata.gz: 2d33022921907669f28eed522daec36e84552aabd41a411ff556923c0029cc72a003d30cd9cbeecba27e3d795fc279f71ca05a01cbc8c5cf6a0bd7256a478f72
7
- data.tar.gz: 87c778a1972590cc6609a3c5a096fef6dde88c70ab663b5ebac820de89ec356359f802ea1d8ba38280378e877caabb1974e1d6a81c689eef92e6d7accf2f356a
6
+ metadata.gz: 74a5a30dc46ad1f8087889dcd5ef19f995e60b691adaadf8a01576b14086085ee9dece80bc45e8b14bac0dd001f51ca6015e4b503c339cc2ae20c9f8b7dc10f9
7
+ data.tar.gz: 1067a26e0e5b99c36a93e501eda2f152056e46b8c96ae6cfd1468464cebf0113455fdcbad288730d31d9b527b05411a6c20fb71de6575501b183d07e3aed3d36
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 6.1.0 (2022-04-05)
2
+
3
+ - Added `expand_range` option
4
+
5
+ ## 6.0.1 (2022-01-16)
6
+
7
+ - Fixed incorrect results (error before 6.0) with `includes` with Active Record 6.1+
8
+
9
+ ## 6.0.0 (2022-01-15)
10
+
11
+ - Raise `ActiveRecord::UnknownAttributeReference` for non-attribute arguments
12
+ - Raise `ArgumentError` for ranges with string bounds
13
+ - Added `n` option for Redshift
14
+ - Changed SQL to return dates instead of times for day, week, month, quarter, and year
15
+ - Removed `dates` option
16
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
17
+
1
18
  ## 5.2.4 (2021-12-15)
2
19
 
3
20
  - Simplified queries for Active Record 7 and MySQL
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013-2021 Andrew Kane
1
+ Copyright (c) 2013-2022 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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 'groupdate'
25
+ gem "groupdate"
26
26
  ```
27
27
 
28
28
  For MySQL and SQLite, also follow [these instructions](#additional-instructions).
@@ -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
@@ -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,7 +245,7 @@ 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 }.map { |k, v| [k, v.count] }.to_h
248
+ users.group_by_day { |u| u.created_at }.to_h { |k, v| [k, v.count] }
256
249
  ```
257
250
 
258
251
  ## Additional Instructions
@@ -289,13 +282,15 @@ Groupdate.time_zone = false
289
282
 
290
283
  ## Upgrading
291
284
 
292
- ### 5.0
285
+ ### 6.0
293
286
 
294
- Groupdate 5.0 brings a number of improvements. Here are a few to be aware of:
287
+ Groupdate 6.0 protects against unsafe input by default. For non-attribute arguments, use:
288
+
289
+ ```ruby
290
+ User.group_by_day(Arel.sql(known_safe_value)).count
291
+ ```
295
292
 
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
293
+ Also, the `dates` option has been removed.
299
294
 
300
295
  ## History
301
296
 
@@ -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 = resolve_column(relation, column)
8
+ @column = column
12
9
  @period = period
13
10
  @time_zone = time_zone
14
11
  @time_range = time_range
@@ -49,28 +46,6 @@ module Groupdate
49
46
  ["#{column} IS NOT NULL"]
50
47
  end
51
48
  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
49
  end
75
50
  end
76
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
- ["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]
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
- ["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]
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
- when :hour
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 ?) AT TIME ZONE ?", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval, time_zone]
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
- ["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]
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
- ["(DATE_TRUNC(?, #{day_start_column}) + INTERVAL ?) AT TIME ZONE ?", period, time_zone, day_start_interval, day_start_interval, time_zone]
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 00:00:00 UTC', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
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 00:00:00 UTC"
35
+ "%Y-%m-%d"
36
36
  when :month
37
- "%Y-%m-01 00:00:00 UTC"
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 00:00:00 UTC"
41
+ "%Y-01-01"
42
42
  end
43
43
 
44
44
  ["strftime(?, #{column})", format]
@@ -22,7 +22,7 @@ module Groupdate
22
22
  end
23
23
 
24
24
  def validate_keywords
25
- known_keywords = [:time_zone, :dates, :series, :format, :locale, :range, :reverse]
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
@@ -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: field,
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|
@@ -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 = Hash[series.map do |k|
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 = change_zone.call(time, utc)
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 = change_zone.call(time, time_zone)
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
@@ -169,32 +123,32 @@ module Groupdate
169
123
  exclude_end = true
170
124
  end
171
125
 
126
+ if options[:expand_range]
127
+ start = round_time(start) if start
128
+ if finish && !(finish == round_time(finish) && exclude_end)
129
+ finish = round_time(finish) + step
130
+ exclude_end = true
131
+ end
132
+ end
133
+
172
134
  time_range = Range.new(start, finish, exclude_end)
173
135
  elsif !time_range && options[:last]
174
- if period == :quarter
175
- step = 3.months
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
136
+ step = step()
137
+ raise ArgumentError, "Cannot use last option with #{period}" unless step
189
138
 
190
- time_range =
191
- if options[:current] == false
192
- round_time(start_at - step)...round_time(now)
193
- else
194
- # extend to end of current period
195
- round_time(start_at)...(round_time(now) + step)
196
- end
139
+ # loop instead of multiply to change start_at - see #151
140
+ start_at = now
141
+ (options[:last].to_i - 1).times do
142
+ start_at -= step
197
143
  end
144
+
145
+ time_range =
146
+ if options[:current] == false
147
+ round_time(start_at - step)...round_time(now)
148
+ else
149
+ # extend to end of current period
150
+ round_time(start_at)...(round_time(now) + step)
151
+ end
198
152
  end
199
153
  time_range
200
154
  end
@@ -256,13 +210,7 @@ module Groupdate
256
210
  if time_range.begin
257
211
  series = [round_time(time_range.begin)]
258
212
 
259
- if period == :quarter
260
- step = 3.months
261
- elsif period == :custom
262
- step = n_seconds
263
- else
264
- step = 1.send(period)
265
- end
213
+ step = step()
266
214
 
267
215
  last_step = series.last
268
216
  day_start_hour = day_start / 3600
@@ -289,7 +237,6 @@ module Groupdate
289
237
  def key_format
290
238
  @key_format ||= begin
291
239
  locale = options[:locale] || I18n.locale
292
- use_dates = options.key?(:dates) ? options[:dates] : Groupdate.dates
293
240
 
294
241
  if options[:format]
295
242
  if options[:format].respond_to?(:call)
@@ -312,7 +259,7 @@ module Groupdate
312
259
  I18n.localize(key, format: options[:format], locale: locale)
313
260
  end
314
261
  end
315
- elsif [:day, :week, :month, :quarter, :year].include?(period) && use_dates
262
+ elsif [:day, :week, :month, :quarter, :year].include?(period)
316
263
  lambda { |k| k.to_date }
317
264
  else
318
265
  lambda { |k| k }
@@ -320,6 +267,16 @@ module Groupdate
320
267
  end
321
268
  end
322
269
 
270
+ def step
271
+ if period == :quarter
272
+ 3.months
273
+ elsif period == :custom
274
+ n_seconds
275
+ elsif 1.respond_to?(period)
276
+ 1.send(period)
277
+ end
278
+ end
279
+
323
280
  def handle_multiple(data, series, multiple_groups, group_index)
324
281
  reverse = options[:reverse]
325
282
 
@@ -1,3 +1,3 @@
1
1
  module Groupdate
2
- VERSION = "5.2.4"
2
+ VERSION = "6.1.0"
3
3
  end
data/lib/groupdate.rb CHANGED
@@ -12,7 +12,6 @@ require "groupdate/version"
12
12
  require "groupdate/adapters/base_adapter"
13
13
  require "groupdate/adapters/mysql_adapter"
14
14
  require "groupdate/adapters/postgresql_adapter"
15
- require "groupdate/adapters/redshift_adapter"
16
15
  require "groupdate/adapters/sqlite_adapter"
17
16
 
18
17
  module Groupdate
@@ -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, :dates
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)
@@ -46,8 +44,7 @@ module Groupdate
46
44
  end
47
45
 
48
46
  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
47
+ Groupdate.register_adapter ["PostgreSQL", "PostGIS", "Redshift"], Groupdate::Adapters::PostgreSQLAdapter
51
48
  Groupdate.register_adapter "SQLite", Groupdate::Adapters::SQLiteAdapter
52
49
 
53
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: 5.2.4
4
+ version: 6.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-16 00:00:00.000000000 Z
11
+ date: 2022-04-05 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.4'
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.32
68
+ rubygems_version: 3.3.7
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