event-counter 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rspec"
7
+ gem "database_cleaner"
8
+ gem "activerecord", "~> 4.0"
9
+ gem "activesupport", "~> 4.0"
10
+ gem "pg"
11
+
12
+ gemspec :path => "../"
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ event-counter (0.1.0)
5
+ activerecord (>= 3)
6
+ activesupport (>= 3)
7
+ pg (~> 0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activemodel (4.1.6)
13
+ activesupport (= 4.1.6)
14
+ builder (~> 3.1)
15
+ activerecord (4.1.6)
16
+ activemodel (= 4.1.6)
17
+ activesupport (= 4.1.6)
18
+ arel (~> 5.0.0)
19
+ activesupport (4.1.6)
20
+ i18n (~> 0.6, >= 0.6.9)
21
+ json (~> 1.7, >= 1.7.7)
22
+ minitest (~> 5.1)
23
+ thread_safe (~> 0.1)
24
+ tzinfo (~> 1.1)
25
+ appraisal (1.0.2)
26
+ bundler
27
+ rake
28
+ thor (>= 0.14.0)
29
+ arel (5.0.1.20140414130214)
30
+ builder (3.2.2)
31
+ database_cleaner (1.3.0)
32
+ diff-lcs (1.2.5)
33
+ i18n (0.6.11)
34
+ json (1.8.1)
35
+ minitest (5.4.1)
36
+ pg (0.17.1)
37
+ rake (10.3.2)
38
+ rspec (3.1.0)
39
+ rspec-core (~> 3.1.0)
40
+ rspec-expectations (~> 3.1.0)
41
+ rspec-mocks (~> 3.1.0)
42
+ rspec-core (3.1.7)
43
+ rspec-support (~> 3.1.0)
44
+ rspec-expectations (3.1.2)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.1.0)
47
+ rspec-mocks (3.1.3)
48
+ rspec-support (~> 3.1.0)
49
+ rspec-support (3.1.2)
50
+ thor (0.19.1)
51
+ thread_safe (0.3.4)
52
+ tzinfo (1.2.2)
53
+ thread_safe (~> 0.1)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ activerecord (~> 4.0)
60
+ activesupport (~> 4.0)
61
+ appraisal
62
+ database_cleaner
63
+ event-counter!
64
+ pg
65
+ rspec
@@ -0,0 +1,12 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "rspec"
7
+ gem "database_cleaner"
8
+ gem "activerecord", "~> 4.1"
9
+ gem "activesupport", "~> 4.1"
10
+ gem "pg"
11
+
12
+ gemspec :path => "../"
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ event-counter (0.1.0)
5
+ activerecord (>= 3)
6
+ activesupport (>= 3)
7
+ pg (~> 0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activemodel (4.1.6)
13
+ activesupport (= 4.1.6)
14
+ builder (~> 3.1)
15
+ activerecord (4.1.6)
16
+ activemodel (= 4.1.6)
17
+ activesupport (= 4.1.6)
18
+ arel (~> 5.0.0)
19
+ activesupport (4.1.6)
20
+ i18n (~> 0.6, >= 0.6.9)
21
+ json (~> 1.7, >= 1.7.7)
22
+ minitest (~> 5.1)
23
+ thread_safe (~> 0.1)
24
+ tzinfo (~> 1.1)
25
+ appraisal (1.0.2)
26
+ bundler
27
+ rake
28
+ thor (>= 0.14.0)
29
+ arel (5.0.1.20140414130214)
30
+ builder (3.2.2)
31
+ database_cleaner (1.3.0)
32
+ diff-lcs (1.2.5)
33
+ i18n (0.6.11)
34
+ json (1.8.1)
35
+ minitest (5.4.1)
36
+ pg (0.17.1)
37
+ rake (10.3.2)
38
+ rspec (3.1.0)
39
+ rspec-core (~> 3.1.0)
40
+ rspec-expectations (~> 3.1.0)
41
+ rspec-mocks (~> 3.1.0)
42
+ rspec-core (3.1.7)
43
+ rspec-support (~> 3.1.0)
44
+ rspec-expectations (3.1.2)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.1.0)
47
+ rspec-mocks (3.1.3)
48
+ rspec-support (~> 3.1.0)
49
+ rspec-support (3.1.2)
50
+ thor (0.19.1)
51
+ thread_safe (0.3.4)
52
+ tzinfo (1.2.2)
53
+ thread_safe (~> 0.1)
54
+
55
+ PLATFORMS
56
+ ruby
57
+
58
+ DEPENDENCIES
59
+ activerecord (~> 4.1)
60
+ activesupport (~> 4.1)
61
+ appraisal
62
+ database_cleaner
63
+ event-counter!
64
+ pg
65
+ rspec
data/lib/event_counter.rb CHANGED
@@ -6,6 +6,8 @@ class EventCounter < ActiveRecord::Base
6
6
 
7
7
  belongs_to :countable, polymorphic: true
8
8
 
9
+ scope :within, ->(range) { range && where(created_at: range) }
10
+
9
11
  def increase_by(val)
10
12
  self.class.where(id: id).update_all(['value = value + ?', val])
11
13
  increment(:value, val)
@@ -86,13 +88,13 @@ class EventCounter < ActiveRecord::Base
86
88
  end
87
89
 
88
90
  def self.normalize_on_time(on_time)
89
- on_time ||= Time.now
91
+ on_time ||= Time.zone.now
90
92
  on_time =
91
93
  case current_interval
92
94
  when Symbol
93
- on_time.send(:"beginning_of_#{current_interval}")
95
+ on_time.in_time_zone.send(:"beginning_of_#{current_interval}")
94
96
  else
95
- on_time.floor(current_interval)
97
+ on_time.in_time_zone.floor(current_interval)
96
98
  end
97
99
  on_time
98
100
  end
@@ -123,16 +125,26 @@ class EventCounter < ActiveRecord::Base
123
125
  end
124
126
 
125
127
  require 'event_counter/active_record_extension'
126
-
127
128
  ActiveRecord::Base.send(:include, EventCounter::ActiveRecordExtension)
128
129
 
130
+ if ActiveSupport::VERSION::MAJOR > 3
131
+ require 'active_support/core_ext/time'
132
+ else
133
+ require 'active_support/time'
134
+ end
135
+
129
136
  # :nodoc:
130
- class Time
137
+ class ActiveSupport::TimeWithZone
138
+ def round_off(seconds = 60)
139
+ Time.zone.at((to_f / seconds).round * seconds)
140
+ end
141
+
131
142
  def floor(seconds = 60)
132
- Time.at((to_f / seconds).floor * seconds)
143
+ Time.zone.at((to_f / seconds).floor * seconds)
133
144
  end
134
145
  end
135
146
 
147
+ # :nodoc:
136
148
  class String
137
149
 
138
150
  unless method_defined?(:squish!)
@@ -42,8 +42,8 @@ class EventCounter < ActiveRecord::Base
42
42
  rotate_counter(*args, opts)
43
43
  end
44
44
 
45
- def data_for(name, interval: nil, range: nil, raw: false)
46
- self.class.data_for(name, id, interval: interval, range: range, raw: raw)
45
+ def data_for(name, opts = {})
46
+ self.class.data_for(name, id, opts)
47
47
  end
48
48
 
49
49
  private
@@ -68,99 +68,113 @@ class EventCounter < ActiveRecord::Base
68
68
  day: 1.day
69
69
  }.freeze
70
70
 
71
- def data_for(counter_name, id = nil, interval: nil, range: nil, raw: false)
72
- interval = normalize_interval!(counter_name, interval)
71
+ def data_for(name, id = nil, interval: nil, range: nil, raw: nil, tz: nil)
72
+ interval = normalize_interval!(name, interval)
73
73
 
74
- sql = <<SQL.squish!
75
- #{cte_definition(counter_name, interval, id)}
76
- SELECT
77
- created_at,
78
- COALESCE(sum(value) OVER (PARTITION BY counters.created_at) , 0) AS value
79
- FROM (#{series_definition(interval, range)}) intervals
80
- LEFT JOIN CTE counters USING (created_at)
81
- ORDER BY 1
82
- SQL
74
+ range = normalize_range!(range, interval) if range
75
+
76
+ tz ||= (Time.zone || 'UTC')
77
+ tz_abbr = tz.now.zone
78
+
79
+ subq = EventCounter
80
+ .select(subq_select(interval, tz_abbr))
81
+ .where(name: name, countable_type: self)
82
+ .where(id && { countable_id: id })
83
+ .within(range)
84
+ .group("1")
85
+ .order("1")
86
+ .to_sql
87
+
88
+ sql = <<-SQL.squish!
89
+ SELECT created_at, value
90
+ FROM (#{series(interval, range, tz_abbr)}) intervals
91
+ LEFT OUTER JOIN (#{subq}) counters USING (created_at)
92
+ ORDER BY 1
93
+ SQL
83
94
 
84
95
  result = connection.execute(sql).to_a
85
96
 
86
- raw ? result : normalize_counters_data(result)
97
+ raw ? result : normalize_counters_data(result, tz)
87
98
  end
88
99
 
89
- def cte_definition(counter_name, interval, id = nil)
90
- <<SQL
91
- WITH CTE AS (
92
- SELECT #{cte_extract(interval)} as created_at, sum(value) AS value
93
- FROM event_counters
94
- WHERE
95
- countable_type = #{sanitize(name)} AND
96
- #{ "countable_id = #{sanitize(id)} AND" if id.present? }
97
- name = #{sanitize(counter_name)}
98
- GROUP BY 1
99
- )
100
- SQL
100
+ def subq_select(interval, tz)
101
+ "#{subq_extract(interval, tz)} as created_at, sum(value) AS value"
101
102
  end
102
103
 
103
- def cte_extract(interval)
104
+ def subq_extract(interval, tz)
104
105
  case interval
105
106
  when Symbol
106
- "date_trunc(#{sanitize(interval)}, created_at)"
107
+ "date_trunc(#{sanitize(interval)}, #{tstamp_tz('created_at', tz)})"
107
108
  else
108
- tstamp(<<SQL)
109
- floor(EXTRACT(EPOCH FROM created_at::timestamp with time zone) /
110
- #{sanitize(interval)})::int * #{sanitize(interval)}
111
- SQL
109
+ time = <<-SQL
110
+ floor(EXTRACT(EPOCH FROM created_at) /
111
+ #{sanitize(interval)})::int * #{sanitize(interval)}
112
+ SQL
113
+ tstamp_tz("to_timestamp(#{time})", tz)
112
114
  end
113
115
  end
114
116
 
115
- def series_definition(interval, range)
116
- range_min, range_max = min_and_max_of_range(interval, range)
117
-
118
- args =
117
+ def series(interval, range, tz)
118
+ a =
119
119
  case interval
120
120
  when Symbol
121
- interval_sql = "interval '1 #{interval}'"
122
- if range
123
- [
124
- "date_trunc(#{sanitize(interval)}, #{tstamp(range.min.to_i)} )",
125
- "date_trunc(#{sanitize(interval)}, #{tstamp(range.max.to_i)} )",
126
- interval_sql
127
- ]
128
- else
129
- [
130
- "date_trunc(#{sanitize(interval)}, min(created_at))",
131
- "date_trunc(#{sanitize(interval)}, max(created_at))",
132
- interval_sql
133
- ]
134
- end
121
+ series_for_symbol(interval, range, tz)
135
122
  else
136
- interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
137
- if range
138
- [
139
- tstamp(sanitize(range_min)),
140
- tstamp(sanitize(range_max)),
141
- interval_sql
142
- ]
143
- else
144
- [ 'min(created_at)', 'max(created_at)', interval_sql ]
145
- end
123
+ series_for_integer(interval, range, tz)
146
124
  end
147
- <<SQL
148
- SELECT
149
- count(*), generate_series(#{args[0]}, #{args[1] }, #{args[2]}) AS created_at
150
- FROM CTE
151
- SQL
125
+ EventCounter.within(range).select(<<-SQL).to_sql
126
+ count(*), generate_series(#{a[0]}, #{a[1] }, #{a[2]}) AS created_at
127
+ SQL
128
+ end
129
+
130
+ def series_for_symbol(interval, range, tz)
131
+ interval_sql = "interval '1 #{interval}'"
132
+ if range
133
+ a = [
134
+ dtrunc(interval, sanitize(range.min).to_s, tz),
135
+ dtrunc(interval, sanitize(range.max).to_s, tz),
136
+ interval_sql
137
+ ]
138
+ else
139
+ a = [
140
+ dtrunc(interval, 'min(created_at)', tz),
141
+ dtrunc(interval, 'max(created_at)', tz),
142
+ interval_sql
143
+ ]
144
+ end
152
145
  end
153
146
 
154
- def tstamp(val)
155
- "to_timestamp(#{val})"
147
+ def series_for_integer(interval, range, tz)
148
+ interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
149
+ if range
150
+ a = [
151
+ tstamp_tz("to_timestamp(#{sanitize(range.min.to_i)})", tz),
152
+ tstamp_tz("to_timestamp(#{sanitize(range.max.to_i)})", tz),
153
+ interval_sql
154
+ ]
155
+ else
156
+ a = [
157
+ tstamp_tz('min(created_at)', tz),
158
+ tstamp_tz('max(created_at)', tz),
159
+ interval_sql
160
+ ]
161
+ end
162
+ end
163
+
164
+ def dtrunc(interval, value, tz)
165
+ "date_trunc(#{sanitize(interval)}, #{tstamp_tz(value, tz)})"
166
+ end
167
+
168
+ def tstamp_tz(str, tz)
169
+ "#{str}::timestamptz AT TIME ZONE #{sanitize(tz)}"
156
170
  end
157
171
 
158
172
  def counter_error!(*args)
159
173
  fail EventCounter::CounterError, args
160
174
  end
161
175
 
162
- def normalize_interval!(counter_name, interval)
163
- default_interval = default_interval_for(counter_name)
176
+ def normalize_interval!(name, interval)
177
+ default_interval = default_interval_for(name)
164
178
 
165
179
  h = {
166
180
  default_interval: default_interval,
@@ -195,28 +209,30 @@ SQL
195
209
  interval.is_a?(Symbol) ? INTERVALS[interval] : interval
196
210
  end
197
211
 
198
- def normalize_counters_data(raw_data)
199
- raw_data.map do |i|
200
- [ Time.parse(i['created_at']), i['value'].to_i ]
212
+ def normalize_counters_data(data, tz)
213
+ Time.use_zone(tz) do
214
+ data.map { |i| [ Time.zone.parse(i['created_at']), i['value'].to_i ] }
201
215
  end
202
216
  end
203
217
 
204
- def default_interval_for(counter_name)
205
- event_counters[counter_name.to_sym]
218
+ def default_interval_for(name)
219
+ event_counters[name.to_sym]
206
220
  end
207
221
 
208
- def min_and_max_of_range(interval, range)
209
- return unless range
222
+ def normalize_range!(range, interval)
223
+ range_min, range_max =
224
+ case interval
225
+ when Symbol
226
+ [
227
+ range.min.send(:"beginning_of_#{interval}"),
228
+ range.max.send(:"end_of_#{interval}")
229
+ ]
230
+ else
231
+ [ range.min.floor(interval), range.max.floor(interval) ]
232
+ end
210
233
 
211
- case interval
212
- when Symbol
213
- [
214
- range.min.send(:"beginning_of_#{interval}").to_i,
215
- range.max.send(:"end_of_#{interval}").to_i
216
- ]
217
- else
218
- [ range.min.floor(interval).to_i, range.max.floor(interval).to_i ]
219
- end
234
+ # TODO: ensure that range in time zone
235
+ range_min..range_max
220
236
  end
221
237
  end
222
238
  end