event-counter 0.0.1 → 0.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.
@@ -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