groupdate.rb
6.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
require "groupdate/version"
require "active_record"
module Groupdate
extend ActiveSupport::Concern
# Pattern from kaminari
# https://github.com/amatsuda/kaminari/blob/master/lib/kaminari/models/active_record_extension.rb
included do
# Future subclasses will pick up the model extension
class << self
def inherited_with_groupdate(kls) #:nodoc:
inherited_without_groupdate kls
kls.send(:include, ClassMethods) if kls.superclass == ActiveRecord::Base
end
alias_method_chain :inherited, :groupdate
end
# Existing subclasses pick up the model extension as well
self.descendants.each do |kls|
kls.send(:include, ClassMethods) if kls.superclass == ActiveRecord::Base
end
end
module ClassMethods
extend ActiveSupport::Concern
included do
# Field list from
# http://www.postgresql.org/docs/9.1/static/functions-datetime.html
time_fields = %w(second minute hour day week month year)
number_fields = %w(day_of_week hour_of_day)
(time_fields + number_fields).each do |field|
self.scope :"group_by_#{field}", lambda {|*args|
column = connection.quote_table_name(args[0])
time_zone = args[1] || Time.zone || "Etc/UTC"
if time_zone.is_a?(ActiveSupport::TimeZone) or time_zone = ActiveSupport::TimeZone[time_zone]
time_zone = time_zone.tzinfo.name
else
raise "Unrecognized time zone"
end
query =
case connection.adapter_name
when "MySQL", "Mysql2"
case field
when "day_of_week" # Sunday = 0, Monday = 1, etc
# use CONCAT for consistent return type (String)
["DAYOFWEEK(CONVERT_TZ(#{column}, '+00:00', ?)) - 1", time_zone]
when "hour_of_day"
["EXTRACT(HOUR from CONVERT_TZ(#{column}, '+00:00', ?))", time_zone]
when "week"
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(DATE_SUB(#{column}, INTERVAL (DAYOFWEEK(CONVERT_TZ(#{column}, '+00:00', ?)) - 1) DAY), '+00:00', ?), '%Y-%m-%d 00:00:00'), ?, '+00:00')", time_zone, time_zone, time_zone]
else
format =
case field
when "second"
"%Y-%m-%d %H:%i:%S"
when "minute"
"%Y-%m-%d %H:%i:00"
when "hour"
"%Y-%m-%d %H:00:00"
when "day"
"%Y-%m-%d 00:00:00"
when "month"
"%Y-%m-01 00:00:00"
else # year
"%Y-01-01 00:00:00"
end
["CONVERT_TZ(DATE_FORMAT(CONVERT_TZ(#{column}, '+00:00', ?), '#{format}'), ?, '+00:00')", time_zone, time_zone]
end
when "PostgreSQL"
case field
when "day_of_week"
["EXTRACT(DOW from #{column}::timestamptz AT TIME ZONE ?)", time_zone]
when "hour_of_day"
["EXTRACT(HOUR from #{column}::timestamptz AT TIME ZONE ?)", time_zone]
when "week" # start on Sunday, not PostgreSQL default Monday
["(DATE_TRUNC('#{field}', (#{column}::timestamptz + INTERVAL '1 day') AT TIME ZONE ?) - INTERVAL '1 day') AT TIME ZONE ?", time_zone, time_zone]
else
["DATE_TRUNC('#{field}', #{column}::timestamptz AT TIME ZONE ?) AT TIME ZONE ?", time_zone, time_zone]
end
else
raise "Connection adapter not supported: #{connection.adapter_name}"
end
if args[2] # zeros
if time_fields.include?(field)
range = args[2]
unless range.is_a?(Range)
raise "Expecting a range"
end
# determine start time
time = range.first.in_time_zone(time_zone)
starts_at =
case field
when "second"
time.change(:usec => 0)
when "minute"
time.change(:sec => 0)
when "hour"
time.change(:min => 0)
when "day"
time.beginning_of_day
when "week"
time.beginning_of_week(:sunday)
when "month"
time.beginning_of_month
else # year
time.beginning_of_year
end
series = [starts_at]
step = 1.send(field)
while range.cover?(series.last + step)
series << series.last + step
end
end
derived_table =
case connection.adapter_name
when "PostgreSQL"
case field
when "day_of_week", "hour_of_day"
max = field == "day_of_week" ? 6 : 23
"SELECT generate_series(0, #{max}, 1) AS #{field}"
else
sanitize_sql_array(["SELECT (generate_series(CAST(? AS timestamptz) AT TIME ZONE ?, ?, ?) AT TIME ZONE ?) AS #{field}", starts_at, time_zone, series.last, "1 #{field}", time_zone])
end
else # MySQL
case field
when "day_of_week", "hour_of_day"
max = field == "day_of_week" ? 6 : 23
(0..max).map{|i| "SELECT #{i} AS #{field}" }.join(" UNION ")
else
sanitize_sql_array([series.map{|i| "SELECT CAST(? AS DATETIME) AS #{field}" }.join(" UNION ")] + series)
end
end
joins("RIGHT OUTER JOIN (#{derived_table}) groupdate_series ON groupdate_series.#{field} = (#{sanitize_sql_array(query)})").group(Groupdate::OrderHack.new("groupdate_series.#{field}", field))
else
group(Groupdate::OrderHack.new(sanitize_sql_array(query), field))
end
}
end
end
end
class OrderHack < String
attr_reader :field
def initialize(str, field)
super(str)
@field = field
end
end
end
ActiveRecord::Base.send :include, Groupdate
# hack for **unfixed** rails issue
# https://github.com/rails/rails/issues/7121
module ActiveRecord
module Calculations
def column_alias_for_with_hack(*keys)
if keys.first.is_a?(Groupdate::OrderHack)
keys.first.field
else
column_alias_for_without_hack(*keys)
end
end
alias_method_chain :column_alias_for, :hack
end
end