groupdate 3.2.0 → 6.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
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
+ @relation = relation
8
+ @column = column
9
+ @period = period
10
+ @time_zone = time_zone
11
+ @time_range = time_range
12
+ @week_start = week_start
13
+ @day_start = day_start
14
+ @n_seconds = n_seconds
15
+
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
24
+ end
25
+ end
26
+
27
+ def generate
28
+ @relation.group(group_clause).where(*where_clause)
29
+ end
30
+
31
+ private
32
+
33
+ def where_clause
34
+ if @time_range.is_a?(Range)
35
+ if @time_range.end
36
+ op = @time_range.exclude_end? ? "<" : "<="
37
+ if @time_range.begin
38
+ ["#{column} >= ? AND #{column} #{op} ?", @time_range.begin, @time_range.end]
39
+ else
40
+ ["#{column} #{op} ?", @time_range.end]
41
+ end
42
+ else
43
+ ["#{column} >= ?", @time_range.begin]
44
+ end
45
+ else
46
+ ["#{column} IS NOT NULL"]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
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
+ ["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
+ when :quarter
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]
38
+ when :custom
39
+ ["FROM_UNIXTIME((UNIX_TIMESTAMP(#{column}) DIV ?) * ?)", n_seconds, n_seconds]
40
+ else
41
+ format =
42
+ case period
43
+ when :second
44
+ "%Y-%m-%d %H:%i:%S"
45
+ when :minute
46
+ "%Y-%m-%d %H:%i:00"
47
+ else # hour
48
+ "%Y-%m-%d %H:00:00"
49
+ end
50
+
51
+ ["CONVERT_TZ(DATE_FORMAT(#{day_start_column}, ?) + INTERVAL ? second, ?, '+00:00')", time_zone, day_start, format, day_start, time_zone]
52
+ end
53
+
54
+ clean_group_clause(@relation.send(:sanitize_sql_array, query))
55
+ end
56
+
57
+ def clean_group_clause(clause)
58
+ # zero quoted in Active Record 7+
59
+ clause.gsub(/ (\-|\+) INTERVAL 0 second/, "").gsub(/ (\-|\+) INTERVAL '0' second/, "")
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,46 @@
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 ?)::date", time_zone, day_start_interval, 13 - week_start, time_zone, day_start_interval, day_start_interval]
25
+ when :custom
26
+ if @relation.connection.adapter_name == "Redshift"
27
+ ["TIMESTAMP 'epoch' + (FLOOR(EXTRACT(EPOCH FROM #{column}::timestamp) / ?) * ?) * INTERVAL '1 second'", n_seconds, n_seconds]
28
+ else
29
+ ["TO_TIMESTAMP(FLOOR(EXTRACT(EPOCH FROM #{column}::timestamptz) / ?) * ?)", n_seconds, n_seconds]
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]
36
+ end
37
+
38
+ clean_group_clause(@relation.send(:sanitize_sql_array, query))
39
+ end
40
+
41
+ def clean_group_clause(clause)
42
+ clause.gsub(/ (\-|\+) INTERVAL '0 second'/, "")
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,51 @@
1
+ module Groupdate
2
+ module Adapters
3
+ class SQLiteAdapter < BaseAdapter
4
+ def group_clause
5
+ raise Groupdate::Error, "Time zones not supported for SQLite" unless @time_zone.utc_offset.zero?
6
+ raise Groupdate::Error, "day_start not supported for SQLite" unless day_start.zero?
7
+
8
+ query =
9
+ if period == :week
10
+ ["strftime('%Y-%m-%d', #{column}, '-6 days', ?)", "weekday #{(week_start + 1) % 7}"]
11
+ elsif period == :custom
12
+ ["datetime((strftime('%s', #{column}) / ?) * ?, 'unixepoch')", n_seconds, n_seconds]
13
+ else
14
+ format =
15
+ case period
16
+ when :minute_of_hour
17
+ "%M"
18
+ when :hour_of_day
19
+ "%H"
20
+ when :day_of_week
21
+ "%w"
22
+ when :day_of_month
23
+ "%d"
24
+ when :day_of_year
25
+ "%j"
26
+ when :month_of_year
27
+ "%m"
28
+ when :second
29
+ "%Y-%m-%d %H:%M:%S UTC"
30
+ when :minute
31
+ "%Y-%m-%d %H:%M:00 UTC"
32
+ when :hour
33
+ "%Y-%m-%d %H:00:00 UTC"
34
+ when :day
35
+ "%Y-%m-%d"
36
+ when :month
37
+ "%Y-%m-01"
38
+ when :quarter
39
+ raise Groupdate::Error, "Quarter not supported for SQLite"
40
+ else # year
41
+ "%Y-01-01"
42
+ end
43
+
44
+ ["strftime(?, #{column})", format]
45
+ end
46
+
47
+ @relation.send(:sanitize_sql_array, query)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,30 +1,25 @@
1
1
  module Enumerable
2
2
  Groupdate::PERIODS.each do |period|
3
- define_method :"group_by_#{period}" do |*args, &block|
3
+ define_method :"group_by_#{period}" do |*args, **options, &block|
4
4
  if block
5
- Groupdate::Magic.new(period, args[0] || {}).group_by(self, &block)
5
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" if args.any?
6
+ Groupdate::Magic::Enumerable.group_by(self, period, options, &block)
6
7
  elsif respond_to?(:scoping)
7
- scoping { @klass.send(:"group_by_#{period}", *args, &block) }
8
+ scoping { @klass.group_by_period(period, *args, **options, &block) }
8
9
  else
9
10
  raise ArgumentError, "no block given"
10
11
  end
11
12
  end
12
13
  end
13
14
 
14
- def group_by_period(*args, &block)
15
+ def group_by_period(period, *args, **options, &block)
15
16
  if block || !respond_to?(:scoping)
16
- period = args[0]
17
- options = args[1] || {}
17
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 1}, expected 1)" if args.any?
18
18
 
19
- # to_sym is unsafe on user input, so convert to strings
20
- permitted_periods = ((options[:permit] || Groupdate::PERIODS).map(&:to_sym) & Groupdate::PERIODS).map(&:to_s)
21
- if permitted_periods.include?(period.to_s)
22
- send("group_by_#{period}", options, &block)
23
- else
24
- raise ArgumentError, "Unpermitted period"
25
- end
19
+ Groupdate::Magic.validate_period(period, options.delete(:permit))
20
+ send("group_by_#{period}", **options, &block)
26
21
  else
27
- scoping { @klass.send(:group_by_period, *args, &block) }
22
+ scoping { @klass.group_by_period(period, *args, **options, &block) }
28
23
  end
29
24
  end
30
25
  end