groupdate 5.2.4 → 6.1.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 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