ar_aggregate_by_interval 1.1.3 → 1.1.4
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/ar_aggregate_by_interval.rb +14 -1
- data/lib/ar_aggregate_by_interval/query_result.rb +23 -18
- data/lib/ar_aggregate_by_interval/query_runner.rb +39 -22
- data/lib/ar_aggregate_by_interval/version.rb +1 -1
- data/spec/lib/ar_aggregate_by_interval/utils_spec.rb +25 -31
- data/spec/lib/ar_aggregate_by_interval_spec.rb +7 -5
- metadata +2 -4
- data/spec/lib/ar_aggregate_by_interval/query_runner_spec.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d67fb1f746590e8801a28802ab2fd1aba5175f11
|
4
|
+
data.tar.gz: c5618c06679af82e4d078ed02ddc150040cad40f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ccdd2612b245ebac746c3e5bb3756a8a021488de55325b5a378fe868ed08c5f1f1987bfd67612257c281d9d5f13cd8997de764fe9c1f00d9f3572037ab0945d0
|
7
|
+
data.tar.gz: 9ccfddcab1829beec3c274ca62bea99f886ef6e9750a3a729d9353cd45e1ca30c588af7212e70fe3bd371ed82098c3b1154e2af7606743f1c1230eb5c73cd3f4
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file.
|
|
3
3
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
4
4
|
This file adheres to [Keep a changelog](http://keepachangelog.com/).
|
5
5
|
|
6
|
+
## [1.1.4] - 2015-03-08 (maintenance)
|
7
|
+
### Changed
|
8
|
+
- Moved functionality from constructors to methods
|
9
|
+
- Build hash from `select_rows` instead of AR objects (performance, simplicity)
|
10
|
+
- Driver of functionality now in method_missing (decoupling)
|
11
|
+
|
6
12
|
## [1.1.3] - 2015-03-02
|
7
13
|
### Fixed
|
8
14
|
- Fix Postgres queries due to AR injecting order clause
|
@@ -30,11 +30,24 @@ module ArAggregateByInterval
|
|
30
30
|
hash_args[col] = hash_args[col].intern if hash_args[col]
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
# build query object
|
34
|
+
query_runner = QueryRunner.new(self, {
|
34
35
|
aggregate_function: aggregate_function,
|
35
36
|
interval: interval
|
36
37
|
}.merge(hash_args))
|
37
38
|
|
39
|
+
# actually run SQL and return a hash of dates => vals
|
40
|
+
date_values_hash = query_runner.run_query
|
41
|
+
|
42
|
+
# takes hash and fills in missing dates
|
43
|
+
# this QueryResult object has 2 attributes: values_and_dates, values
|
44
|
+
QueryResult.new({
|
45
|
+
date_values_hash: date_values_hash,
|
46
|
+
from: query_runner.from,
|
47
|
+
to: query_runner.to,
|
48
|
+
interval: query_runner.interval
|
49
|
+
})
|
50
|
+
|
38
51
|
end
|
39
52
|
|
40
53
|
end
|
@@ -9,10 +9,10 @@ module ArAggregateByInterval
|
|
9
9
|
class QueryResult
|
10
10
|
|
11
11
|
VALID_HASH_ARGS = {
|
12
|
-
|
12
|
+
date_values_hash: [Hash],
|
13
13
|
|
14
|
-
# hash with 1 key where the key is a date column and value is the column being aggegated
|
15
|
-
ar_result_select_col_mapping: -> (v) { v.is_a?(Hash) && v.size == 1 },
|
14
|
+
# # hash with 1 key where the key is a date column and value is the column being aggegated
|
15
|
+
# ar_result_select_col_mapping: -> (v) { v.is_a?(Hash) && v.size == 1 },
|
16
16
|
|
17
17
|
from: [Date, DateTime, Time, ActiveSupport::TimeWithZone],
|
18
18
|
to: [Date, DateTime, Time, ActiveSupport::TimeWithZone],
|
@@ -20,17 +20,18 @@ module ArAggregateByInterval
|
|
20
20
|
interval: -> (v) { Utils.ruby_strftime_map.keys.include?(v) }
|
21
21
|
}
|
22
22
|
|
23
|
-
def initialize(
|
24
|
-
|
23
|
+
def initialize(hash_args)
|
24
|
+
ClassyHash.validate(hash_args, VALID_HASH_ARGS)
|
25
25
|
|
26
|
-
@dates_values_hash = Utils.ar_to_hash(args[:ar_result], args[:ar_result_select_col_mapping])
|
27
|
-
@
|
26
|
+
# @dates_values_hash = Utils.ar_to_hash(args[:ar_result], args[:ar_result_select_col_mapping])
|
27
|
+
@dates_values_hash = hash_args[:date_values_hash]
|
28
|
+
@date_iterator_method = Utils::DATE_ITERATOR_METHOD_MAP[hash_args[:interval]]
|
28
29
|
|
29
30
|
# strformat to match the format out of the database
|
30
|
-
@strftime_format = Utils.ruby_strftime_map[
|
31
|
+
@strftime_format = Utils.ruby_strftime_map[hash_args[:interval]]
|
31
32
|
|
32
|
-
@from =
|
33
|
-
@to =
|
33
|
+
@from = hash_args[:from]
|
34
|
+
@to = hash_args[:to]
|
34
35
|
end
|
35
36
|
|
36
37
|
def values_and_dates
|
@@ -42,16 +43,20 @@ module ArAggregateByInterval
|
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
46
|
+
def values
|
47
|
+
@values ||= values_and_dates.collect { |hash| hash[:value] }
|
48
|
+
end
|
49
|
+
|
45
50
|
private
|
46
51
|
|
47
|
-
def validate_args!(hash_args)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
52
|
+
# def validate_args!(hash_args)
|
53
|
+
# ClassyHash.validate(hash_args, VALID_HASH_ARGS)
|
54
|
+
# first_res = hash_args[:ar_result].first
|
55
|
+
# keys = hash_args[:ar_result_select_col_mapping].to_a.flatten
|
56
|
+
# if first_res && keys.any? { |key| !first_res.respond_to?(key) }
|
57
|
+
# raise RuntimeError.new("the collection passed does not respond to all attribs: #{keys}")
|
58
|
+
# end
|
59
|
+
# end
|
55
60
|
|
56
61
|
def array_of_dates
|
57
62
|
@array_of_dates ||= @from.to_date.send(@date_iterator_method, @to.to_date).map do |date|
|
@@ -19,41 +19,58 @@ module ArAggregateByInterval
|
|
19
19
|
aggregate_column: [:optional, Symbol, NilClass] # required when using sum (as opposed to count)
|
20
20
|
}
|
21
21
|
|
22
|
-
attr_reader :values, :values_and_dates
|
22
|
+
attr_reader :values, :values_and_dates, :from, :to, :interval
|
23
23
|
|
24
24
|
def initialize(ar_model, hash_args)
|
25
25
|
|
26
26
|
validate_args!(hash_args)
|
27
27
|
|
28
|
-
|
29
|
-
to = normalize_to(hash_args[:to] || Time.zone.try(:now) || Time.now, hash_args[:interval])
|
28
|
+
@ar_model = ar_model
|
30
29
|
|
31
|
-
|
30
|
+
@from = normalize_from(hash_args[:from], hash_args[:interval])
|
31
|
+
@to = normalize_to(hash_args[:to] || Time.zone.try(:now) || Time.now, hash_args[:interval])
|
32
|
+
|
33
|
+
@db_vendor_select =
|
32
34
|
Utils.select_for_grouping_column(hash_args[:group_by_column])[hash_args[:interval]]
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
@values_and_dates = agg_int.values_and_dates
|
51
|
-
@values = @values_and_dates.collect { |hash| hash[:value] }
|
36
|
+
@aggregate_function = hash_args[:aggregate_function]
|
37
|
+
@aggregate_column = hash_args[:aggregate_column]
|
38
|
+
@group_by_column = hash_args[:group_by_column]
|
39
|
+
|
40
|
+
@interval = hash_args[:interval]
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_query
|
44
|
+
# actually run query
|
45
|
+
array_of_pairs = ActiveRecord::Base.connection.select_rows(to_sql)
|
46
|
+
|
47
|
+
# workaround ActiveRecord's automatic casting to Date objects
|
48
|
+
# (ideally we could return raw values straight from ActiveRecord to avoid this expensive N)
|
49
|
+
array_of_pairs.collect! do |date_val_pair|
|
50
|
+
date_val_pair.collect(&:to_s)
|
51
|
+
end
|
52
52
|
|
53
|
+
# convert the array of key/values to a hash
|
54
|
+
Hash[array_of_pairs]
|
53
55
|
end
|
54
56
|
|
55
57
|
private
|
56
58
|
|
59
|
+
def to_sql
|
60
|
+
# first col is date, second col is actual value
|
61
|
+
query = @ar_model.
|
62
|
+
select("#{@db_vendor_select} as datechunk__").
|
63
|
+
select("#{@aggregate_function}(#{@aggregate_column || '*'}) as totalchunked__").
|
64
|
+
where(["#{@group_by_column} >= ? and #{@group_by_column} <= ?", @from, @to]).
|
65
|
+
group('datechunk__')
|
66
|
+
|
67
|
+
# workaround Postgres adapter's insistence of adding an order clause
|
68
|
+
query = query.order(nil)
|
69
|
+
|
70
|
+
# an string of the query to run
|
71
|
+
query.to_sql
|
72
|
+
end
|
73
|
+
|
57
74
|
def validate_args!(hash_args)
|
58
75
|
ClassyHash.validate(hash_args, VALID_HASH_ARGS)
|
59
76
|
if hash_args[:aggregate_function] != 'count' && hash_args[:aggregate_column].blank?
|
@@ -5,47 +5,41 @@ module ArAggregateByInterval
|
|
5
5
|
|
6
6
|
describe Utils do
|
7
7
|
|
8
|
-
|
8
|
+
describe 'interval_inflector' do
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
shared_examples 'working inflector' do |int, beg_end, expected|
|
11
|
+
it "works for #{beg_end}_#{int}" do
|
12
|
+
expect(described_class.interval_inflector(int, beg_end)).to eq expected
|
13
|
+
end
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
[
|
17
|
-
OpenStruct.new({
|
18
|
-
date_chunk__: '2014-01-01',
|
19
|
-
value: 5
|
20
|
-
})
|
21
|
-
]
|
22
|
-
end
|
16
|
+
include_examples 'working inflector', 'daily', 'beginning', 'beginning_of_day'
|
17
|
+
include_examples 'working inflector', 'daily', 'end', 'end_of_day'
|
23
18
|
|
24
|
-
|
19
|
+
include_examples 'working inflector', 'weekly', 'beginning', 'beginning_of_week'
|
20
|
+
include_examples 'working inflector', 'weekly', 'end', 'end_of_week'
|
25
21
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
22
|
+
include_examples 'working inflector', 'monthly', 'beginning', 'beginning_of_month'
|
23
|
+
include_examples 'working inflector', 'monthly', 'end', 'end_of_month'
|
30
24
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
OpenStruct.new({
|
35
|
-
id: OpenStruct.new({}),
|
36
|
-
something: 1
|
37
|
-
})
|
38
|
-
]
|
39
|
-
end
|
40
|
-
let(:mapping) { { 'id' => 'something' } }
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'to_f_or_i_or_s' do
|
41
28
|
|
42
|
-
|
43
|
-
|
44
|
-
expect(
|
29
|
+
shared_examples 'working Numeric converter' do |arg1, expected_class|
|
30
|
+
it "works for #{arg1.class.name} to #{expected_class.name}" do
|
31
|
+
expect(described_class.to_f_or_i_or_s(arg1)).to be_instance_of(expected_class)
|
45
32
|
end
|
46
33
|
end
|
47
34
|
|
35
|
+
include_examples 'working Numeric converter', '1.1', Float
|
36
|
+
include_examples 'working Numeric converter', 1.1, Float
|
37
|
+
include_examples 'working Numeric converter', '1.0', Fixnum
|
38
|
+
include_examples 'working Numeric converter', 1.0, Fixnum
|
39
|
+
include_examples 'working Numeric converter', 1, Fixnum
|
40
|
+
|
48
41
|
end
|
42
|
+
|
49
43
|
end
|
50
44
|
|
51
|
-
end
|
45
|
+
end
|
@@ -5,25 +5,27 @@ describe ArAggregateByInterval do
|
|
5
5
|
before(:all) do |example|
|
6
6
|
@from = DateTime.parse '2013-08-05'
|
7
7
|
@to = @from
|
8
|
-
|
9
|
-
|
8
|
+
blog1 = Blog.create arbitrary_number: 10, created_at: @from
|
9
|
+
blog2 = Blog.create arbitrary_number: 20, created_at: @from
|
10
|
+
blog1.page_views.create date: @from
|
11
|
+
blog1.page_views.create date: @from
|
10
12
|
end
|
11
13
|
|
12
14
|
shared_examples_for 'count .values_and_dates' do
|
13
15
|
it 'returns value and date with expected values' do
|
14
|
-
expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value:
|
16
|
+
expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 2])
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
18
20
|
shared_examples_for 'sum .values_and_dates' do
|
19
21
|
it 'returns value and date with expected values' do
|
20
|
-
expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value:
|
22
|
+
expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 30])
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
26
|
shared_examples_for 'avg .values_and_dates' do
|
25
27
|
it 'returns value and date with expected values' do
|
26
|
-
expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value:
|
28
|
+
expect(subject.values_and_dates).to eq([date: @from.beginning_of_week.to_date, value: 15])
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ar_aggregate_by_interval
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Otto
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -161,7 +161,6 @@ files:
|
|
161
161
|
- lib/ar_aggregate_by_interval/version.rb
|
162
162
|
- spec/ar_bootstrap/init.rb
|
163
163
|
- spec/ar_bootstrap/schema.rb
|
164
|
-
- spec/lib/ar_aggregate_by_interval/query_runner_spec.rb
|
165
164
|
- spec/lib/ar_aggregate_by_interval/utils_spec.rb
|
166
165
|
- spec/lib/ar_aggregate_by_interval_spec.rb
|
167
166
|
- spec/spec_helper.rb
|
@@ -191,7 +190,6 @@ summary: add [sum|count]_[daily|weekly|monthly] to your AR models for MySQL AND
|
|
191
190
|
test_files:
|
192
191
|
- spec/ar_bootstrap/init.rb
|
193
192
|
- spec/ar_bootstrap/schema.rb
|
194
|
-
- spec/lib/ar_aggregate_by_interval/query_runner_spec.rb
|
195
193
|
- spec/lib/ar_aggregate_by_interval/utils_spec.rb
|
196
194
|
- spec/lib/ar_aggregate_by_interval_spec.rb
|
197
195
|
- spec/spec_helper.rb
|
@@ -1,66 +0,0 @@
|
|
1
|
-
require 'ar_aggregate_by_interval/query_runner'
|
2
|
-
|
3
|
-
module ArAggregateByInterval
|
4
|
-
describe QueryRunner do
|
5
|
-
|
6
|
-
subject do
|
7
|
-
described_class.new(Blog, {
|
8
|
-
aggregate_function: aggregate_function,
|
9
|
-
aggregate_column: (aggregate_column rescue nil),
|
10
|
-
interval: interval,
|
11
|
-
group_by_column: :created_at,
|
12
|
-
from: from,
|
13
|
-
to: to
|
14
|
-
})
|
15
|
-
end
|
16
|
-
|
17
|
-
context 'one week duration' do
|
18
|
-
|
19
|
-
# monday
|
20
|
-
let(:from) { DateTime.parse '2013-08-05' }
|
21
|
-
# sunday
|
22
|
-
let(:to) { DateTime.parse '2013-08-11' }
|
23
|
-
|
24
|
-
before do |example|
|
25
|
-
Blog.create [
|
26
|
-
{arbitrary_number: 10, created_at: from},
|
27
|
-
{arbitrary_number: 20, created_at: from}
|
28
|
-
]
|
29
|
-
end
|
30
|
-
|
31
|
-
context 'avg daily' do
|
32
|
-
let(:interval) { 'daily' }
|
33
|
-
let(:aggregate_function) { 'avg' }
|
34
|
-
let(:aggregate_column) { :arbitrary_number }
|
35
|
-
|
36
|
-
describe '.values' do
|
37
|
-
it 'returns an array of size 7' do
|
38
|
-
expect(subject.values.size).to eq 7
|
39
|
-
end
|
40
|
-
it 'returns actual averages' do
|
41
|
-
expect(subject.values).to eq([15, 0, 0, 0, 0, 0, 0])
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
context 'count weekly' do
|
47
|
-
let(:interval) { 'weekly' }
|
48
|
-
let(:aggregate_function) { 'count' }
|
49
|
-
|
50
|
-
describe '.values' do
|
51
|
-
it 'returns exactly 1 element array with 1' do
|
52
|
-
expect(subject.values).to eq([2])
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
describe '.value_and_dates' do
|
57
|
-
it 'returns value and date with expected values' do
|
58
|
-
expect(subject.values_and_dates).to eq([date: from.beginning_of_week.to_date, value: 2])
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
end
|