aggrobot 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.gitignore +1 -0
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/Gemfile +6 -0
- data/Guardfile +24 -0
- data/Rakefile +69 -1
- data/aggrobot.gemspec +11 -8
- data/lib/aggrobot.rb +34 -3
- data/lib/aggrobot/aggregator.rb +93 -0
- data/lib/aggrobot/aggrobot.rb +115 -0
- data/lib/aggrobot/aggrobot_error.rb +6 -0
- data/lib/aggrobot/helper.rb +14 -0
- data/lib/aggrobot/query_planner.rb +36 -0
- data/lib/aggrobot/query_planner/bucketed_groups_query_planner.rb +50 -0
- data/lib/aggrobot/query_planner/default_query_planner.rb +31 -0
- data/lib/aggrobot/query_planner/group_limit_query_planner.rb +56 -0
- data/lib/aggrobot/railtie.rb +11 -0
- data/lib/aggrobot/sql_functions.rb +55 -0
- data/lib/aggrobot/version.rb +1 -1
- data/spec/spec_helper.rb +48 -0
- data/spec/support/chain_query.rb +20 -0
- data/spec/unit/aggrobot/query_planners/bucketed_groups_query_planner_spec.rb +96 -0
- data/spec/unit/aggrobot/query_planners/default_query_planner_spec.rb +53 -0
- data/spec/unit/aggrobot/query_planners/group_limit_query_planner_spec.rb +136 -0
- data/spec/unit/aggrobot/sql_functions_spec.rb +84 -0
- data/spec/unit/aggrobot_spec.rb +4 -0
- metadata +72 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YjhjNWE3NWU3M2IxOWQxZWIxMjIwYTVjMmQ5Mjc5ZDFmYTRhZTI5Yg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
OTc4YjYzNjlmMjdlMjIzNzY4MTcwZTA5YWUwNjk2MTkyMzM4M2Q5MQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ODk4NjY1NGJjZTIxMWZjMWZhODk4OTI4YTBhYmFiMzRjMDM1NjkzOTM4YTJi
|
10
|
+
OTdkYTA1MjEwNTBhODk2OGQwODU0NDIyNzA5YzI5OWQ3OTJlN2I4MWI4ZjMw
|
11
|
+
NzczYjY1OWRlYzY4ZDI5ZjE1MDcwMmZkZTdkYTYyMzAwYzgyNTE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YTA2MjhlMzI0ZjU1YzY4NWRlYWE3Y2VhMjRiOGJlNzA5YjljMWJhYWM1MGY4
|
14
|
+
MjZmZTA0MjEwOGNkNmZiMDc4ZWJmMmIwOWNjNTZkMjQ5MTgxYTFkMWRmZTcx
|
15
|
+
NjZjNDA1YzcxODgzYTRkZmY2N2VkZjU4OGJlMDI3YTU3YWI4ZTM=
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
|
17
|
+
# Capybara features specs
|
18
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
|
19
|
+
|
20
|
+
# Turnip features and steps
|
21
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
22
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
23
|
+
end
|
24
|
+
|
data/Rakefile
CHANGED
@@ -1 +1,69 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
# Add default task. When you type just rake command this would run. Travis CI runs this. Making this run spec
|
5
|
+
desc 'Default: run specs.'
|
6
|
+
task :default => [:spec]
|
7
|
+
|
8
|
+
desc 'Spec: Runs both unit and integration tests'
|
9
|
+
task :spec => ['spec:unit', 'spec:integration']
|
10
|
+
|
11
|
+
namespace :spec do
|
12
|
+
|
13
|
+
desc 'Run unit specs'
|
14
|
+
RSpec::Core::RakeTask.new('unit') do |spec|
|
15
|
+
spec.pattern = FileList['spec/unit/**/*_spec.rb']
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Run integration specs'
|
19
|
+
RSpec::Core::RakeTask.new('integration') do |spec|
|
20
|
+
spec.pattern = 'spec/integration/**/*_spec.rb'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
# Run the rdoc task to generate rdocs for this gem
|
26
|
+
require 'rdoc/task'
|
27
|
+
RDoc::Task.new do |rdoc|
|
28
|
+
require 'aggrobot/version'
|
29
|
+
rdoc.rdoc_dir = 'rdoc'
|
30
|
+
rdoc.title = "aggrobot #{Aggrobot::VERSION}"
|
31
|
+
rdoc.rdoc_files.include('README*')
|
32
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
33
|
+
end
|
34
|
+
|
35
|
+
desc 'Spec: Runs both unit and integration tests'
|
36
|
+
task :coverage => ['coverage:pre', 'coverage:unit', 'coverage:integration']
|
37
|
+
namespace :coverage do
|
38
|
+
task :pre do
|
39
|
+
require 'fileutils'
|
40
|
+
coverage_folder = File.expand_path('../coverage', __FILE__)
|
41
|
+
FileUtils.mkdir_p coverage_folder
|
42
|
+
|
43
|
+
coverage_html = <<-HTML
|
44
|
+
<html><body>
|
45
|
+
<ul style="list-style:none">
|
46
|
+
<li>Aggrobot - Code Coverage</li>
|
47
|
+
<li><a href="integration/index.html">Integration Tests</a></li>
|
48
|
+
<li><a href="unit/index.html">Unit Tests</a></li>
|
49
|
+
</ul>
|
50
|
+
</body></html>
|
51
|
+
HTML
|
52
|
+
File.open(File.join(coverage_folder, 'index.html'), 'w') { |f| f << coverage_html }
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Ruby 1.9+ using simplecov
|
57
|
+
desc "Code coverage unit"
|
58
|
+
task :unit do
|
59
|
+
ENV['COVERAGE'] = "unit"
|
60
|
+
Rake::Task['spec:unit'].execute
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Code coverage integration"
|
64
|
+
task :integration do
|
65
|
+
ENV['COVERAGE'] = "integration"
|
66
|
+
Rake::Task['spec:integration'].execute
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
data/aggrobot.gemspec
CHANGED
@@ -4,20 +4,23 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'aggrobot/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'aggrobot'
|
8
8
|
spec.version = Aggrobot::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Shadab Ahmed']
|
10
|
+
spec.email = ['shadab.ansari@gmail.com']
|
11
11
|
spec.description = %q{Easy and performant aggregation for rails}
|
12
12
|
spec.summary = %q{Rails aggregation library}
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.
|
22
|
-
spec.add_development_dependency
|
21
|
+
spec.add_runtime_dependency 'rails', '~> 4.0'
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency('rspec', ['~> 2.14.1'])
|
25
|
+
spec.add_development_dependency('rdoc')
|
23
26
|
end
|
data/lib/aggrobot.rb
CHANGED
@@ -1,5 +1,36 @@
|
|
1
|
-
require
|
1
|
+
require 'aggrobot/railtie'
|
2
|
+
require 'active_support/core_ext/module/delegation.rb'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
require 'aggrobot/version'
|
5
|
+
require 'aggrobot/aggrobot_error'
|
6
|
+
require 'aggrobot/helper'
|
7
|
+
require 'aggrobot/sql_functions'
|
8
|
+
require 'aggrobot/query_planner'
|
9
|
+
require 'aggrobot/aggregator'
|
10
|
+
require 'aggrobot/aggrobot'
|
11
|
+
|
2
12
|
|
3
13
|
module Aggrobot
|
4
|
-
|
5
|
-
|
14
|
+
|
15
|
+
DEFAULT_GROUP_BY = SqlFunctions.sanitize('aggrobot_default_group')
|
16
|
+
|
17
|
+
def self.start(collection = nil, block_arg = nil, block_opts = nil, &block)
|
18
|
+
block_opts ||= block_arg if block
|
19
|
+
block = block_arg if block_arg && block_arg.respond_to?(:call)
|
20
|
+
raise 'Block parameter required' unless block
|
21
|
+
original_block_context = eval "self", block.binding
|
22
|
+
attrs = if block.arity > 0
|
23
|
+
block_opts.is_a?(Hash) ? block_opts : {count: collection.count}
|
24
|
+
end
|
25
|
+
Aggrobot.new(original_block_context, collection).instance_exec(attrs, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.block(&block)
|
29
|
+
block
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.setup(app)
|
33
|
+
SqlFunctions.const_set(:ROUNDING_DIGITS, app.config.aggrobot.percent_precision || 2)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
class Aggregator
|
3
|
+
|
4
|
+
include Helper
|
5
|
+
|
6
|
+
def initialize(collection)
|
7
|
+
@collection = collection
|
8
|
+
@group_name_attribute, @count_attribute = :name, :count
|
9
|
+
@group_labels_map = {}
|
10
|
+
@attribute_mapping = {}
|
11
|
+
self.collection(collection) if collection
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def group_labels(map = nil, &block)
|
16
|
+
if map || block
|
17
|
+
if map.is_a?(Hash)
|
18
|
+
@group_labels_map = ActiveSupport::HashWithIndifferentAccess.new(map)
|
19
|
+
elsif map.respond_to?(:call) || block
|
20
|
+
@group_labels_map = block || map
|
21
|
+
end
|
22
|
+
else
|
23
|
+
@group_labels_map
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def collection(values = nil)
|
28
|
+
if values
|
29
|
+
raise_error 'Collection should be an ActiveRecord::Relation or ActiveRecord::Base' unless
|
30
|
+
[ActiveRecord::Relation, ActiveRecord::Base].any?{|m| values.is_a?(m) }
|
31
|
+
@collection = values
|
32
|
+
else
|
33
|
+
@collection
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def group_by(group, opts = nil)
|
38
|
+
raise_error "Group_by takes only symbol or a string as argument" unless group.is_a?(Symbol) or group.is_a?(String)
|
39
|
+
@query_planner = QueryPlanner.create(@collection, group, opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
def override(attr, override_attr = false)
|
43
|
+
case attr
|
44
|
+
when :name
|
45
|
+
@group_name_attribute = override_attr
|
46
|
+
when :count
|
47
|
+
@count_attribute = override_attr
|
48
|
+
when Hash
|
49
|
+
attr.each { |k, v| override(k, v) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def set(name = nil, opts)
|
54
|
+
if opts.is_a? Hash
|
55
|
+
@attribute_mapping.merge!(opts)
|
56
|
+
elsif name && opts
|
57
|
+
@attribute_mapping[name] = opts
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def query_planner
|
62
|
+
@query_planner ||= QueryPlanner.create(@collection, DEFAULT_GROUP_BY)
|
63
|
+
end
|
64
|
+
|
65
|
+
def yield_results
|
66
|
+
# yield on actual query results
|
67
|
+
query_planner.query_results(extra_columns).each do |real_group_name, count, *rest|
|
68
|
+
mapped_group_name = @group_labels_map[real_group_name] || real_group_name
|
69
|
+
relation = @query_planner.sub_query(real_group_name)
|
70
|
+
yield(mapped_attributes(mapped_group_name, count, rest), mapped_group_name, relation)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def extra_columns
|
77
|
+
@attribute_mapping.values
|
78
|
+
end
|
79
|
+
|
80
|
+
def extra_attributes
|
81
|
+
@attribute_mapping.keys
|
82
|
+
end
|
83
|
+
|
84
|
+
def mapped_attributes(group_name, count, result_row)
|
85
|
+
ActiveSupport::HashWithIndifferentAccess.new.tap do |attributes|
|
86
|
+
attributes.merge!(Hash[extra_attributes.zip(result_row)]) unless result_row.empty?
|
87
|
+
attributes[@count_attribute] = count if @count_attribute
|
88
|
+
attributes[@group_name_attribute] = group_name if @group_name_attribute
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
class Aggrobot
|
3
|
+
|
4
|
+
include SqlFunctions
|
5
|
+
include Helper
|
6
|
+
|
7
|
+
delegate :collection, :group_by, :default_groups, :override, :set, :group_labels, :to => :@aggregator
|
8
|
+
|
9
|
+
def run(block)
|
10
|
+
instance_eval(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(caller_context, collection = nil)
|
14
|
+
@caller_context = caller_context
|
15
|
+
@aggregator = Aggregator.new(collection)
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(method, *args, &block)
|
19
|
+
@caller_context.send method, *args, &block
|
20
|
+
end
|
21
|
+
|
22
|
+
def hash(collection = nil, opts = {}, &block)
|
23
|
+
self.collection(collection) if collection
|
24
|
+
@top_level_object = ActiveSupport::HashWithIndifferentAccess.new
|
25
|
+
proceed(block, opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
def list(collection = nil, opts = {}, &block)
|
29
|
+
self.collection(collection) if collection
|
30
|
+
@top_level_object = []
|
31
|
+
proceed(block, opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
def default(default_val = nil, &block)
|
35
|
+
block = block_from_args(default_val, block, false)
|
36
|
+
default_val = ::Aggrobot.start(collection, &block) if block
|
37
|
+
@top_level_object = default_val
|
38
|
+
end
|
39
|
+
|
40
|
+
alias set_current_value default
|
41
|
+
|
42
|
+
def default_group_attrs(opts = nil)
|
43
|
+
if opts
|
44
|
+
raise_error 'Arguments must be a hash' unless opts.is_a?(Hash)
|
45
|
+
@default_group_attrs = ActiveSupport::HashWithIndifferentAccess.new(opts)
|
46
|
+
else
|
47
|
+
@default_group_attrs
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def current_value
|
52
|
+
@top_level_object
|
53
|
+
end
|
54
|
+
|
55
|
+
def attr(attribute, value = nil, &block)
|
56
|
+
block = block_from_args(value, block, false)
|
57
|
+
raise_error 'attr can only be used with a hash type' unless @top_level_object.is_a?(Hash)
|
58
|
+
raise_error 'attribute should be a symbol or a string' unless attribute.is_a?(Symbol) || attribute.is_a?(String)
|
59
|
+
raise_error 'attr should receive a block or a value' if value.nil? && block.nil?
|
60
|
+
value = ::Aggrobot.start(collection, &block) if block
|
61
|
+
@top_level_object[attribute] = value
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_attr(attribute)
|
65
|
+
@top_level_object[attribute]
|
66
|
+
end
|
67
|
+
|
68
|
+
def collect_each_group_attributes
|
69
|
+
each_group do |attr|
|
70
|
+
attr
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def each_group(block_arg = nil, &block)
|
75
|
+
block = block_from_args(block_arg, block)
|
76
|
+
@aggregator.yield_results do |attrs, group_name, sub_collection|
|
77
|
+
attrs = @default_group_attrs.merge(attrs) if @default_group_attrs
|
78
|
+
block_value = ::Aggrobot.start(sub_collection) do
|
79
|
+
instance_exec(attrs, &block)
|
80
|
+
end
|
81
|
+
update_top_level_obj(group_name, block_value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def evaluate(block_arg = nil, &block)
|
86
|
+
block = block_from_args(block_arg, block)
|
87
|
+
list(&block).first
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def evaluate_opts(opts)
|
93
|
+
opts.each do |method_name, arg|
|
94
|
+
send(method_name, arg)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def update_top_level_obj(group, val)
|
99
|
+
if @top_level_object.is_a? Hash
|
100
|
+
@top_level_object[group] = val
|
101
|
+
elsif @top_level_object.is_a? Array
|
102
|
+
@top_level_object << val
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def proceed(block, opts)
|
107
|
+
raise_error "no block given for api" unless block
|
108
|
+
evaluate_opts(opts)
|
109
|
+
instance_eval(&block)
|
110
|
+
@top_level_object
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
module Helper
|
3
|
+
|
4
|
+
def block_from_args(block_arg, block, required = true)
|
5
|
+
block = block_arg if block_arg && block_arg.respond_to?(:call)
|
6
|
+
raise ArgumentError.new 'Block parameter required' if required && !block
|
7
|
+
block
|
8
|
+
end
|
9
|
+
|
10
|
+
def raise_error(msg)
|
11
|
+
raise AggrobotError.new(msg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'aggrobot/query_planner/default_query_planner'
|
2
|
+
require 'aggrobot/query_planner/group_limit_query_planner'
|
3
|
+
require 'aggrobot/query_planner/bucketed_groups_query_planner'
|
4
|
+
|
5
|
+
module Aggrobot::QueryPlanner
|
6
|
+
|
7
|
+
def self.create(collection, group_by, opts = nil)
|
8
|
+
case
|
9
|
+
when opts.nil?
|
10
|
+
DefaultQueryPlanner.new(collection, group_by)
|
11
|
+
when opts.key?(:limit_to)
|
12
|
+
GroupLimitQueryPlanner.new(collection, group_by, opts)
|
13
|
+
when opts.key?(:buckets)
|
14
|
+
BucketedGroupsQueryPlanner.new(collection, group_by, opts)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ParametersValidator
|
19
|
+
def self.validate_options(opts, required_parameters, optional_parameters)
|
20
|
+
params = opts.keys
|
21
|
+
# raise errors for required parameters
|
22
|
+
raise_argument_error(opts, required_parameters, optional_parameters) unless (required_parameters - params).empty?
|
23
|
+
# raise errors if any extra arguments given
|
24
|
+
raise_argument_error(opts, required_parameters, optional_parameters) unless (params - required_parameters - optional_parameters).empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.raise_argument_error(opts, required_parameters, optional_parameters)
|
28
|
+
raise ArgumentError, <<-ERR
|
29
|
+
Wrong arguments given - #{opts}
|
30
|
+
Required parameters are #{required_parameters}
|
31
|
+
Optional parameters are #{optional_parameters}
|
32
|
+
ERR
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
module QueryPlanner
|
3
|
+
class BucketedGroupsQueryPlanner < DefaultQueryPlanner
|
4
|
+
|
5
|
+
def initialize(collection, group, opts = {})
|
6
|
+
ParametersValidator.validate_options(opts, [:buckets], [:keep_empty])
|
7
|
+
raise_error 'Need to set group first' unless group
|
8
|
+
super(collection, group)
|
9
|
+
create_query_map(opts[:buckets])
|
10
|
+
@keep_empty = opts[:keep_empty]
|
11
|
+
end
|
12
|
+
|
13
|
+
def sub_query(group_name)
|
14
|
+
@query_map[group_name]
|
15
|
+
end
|
16
|
+
|
17
|
+
def query_results(extra_cols = [])
|
18
|
+
return empty_default_groups if collection_is_none?
|
19
|
+
results = collect_query_results(extra_cols)
|
20
|
+
results.reject! { |r| r[1] == 0 } unless @keep_empty
|
21
|
+
results
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def collect_query_results(extra_cols)
|
27
|
+
columns = ['', SqlFunctions.count] + extra_cols
|
28
|
+
@query_map.collect do |group_name, query|
|
29
|
+
sanitized_group_name = SqlFunctions.sanitize(group_name)
|
30
|
+
columns[0] = sanitized_group_name
|
31
|
+
results = query.group(sanitized_group_name).limit(1).pluck(*columns).first
|
32
|
+
@query_map[group_name] = @query_map[group_name].none unless results
|
33
|
+
results || [group_name, 0]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def empty_default_groups
|
38
|
+
@keep_empty ? @query_map.keys.collect { |k| [k, 0] } : []
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_query_map(groups)
|
42
|
+
@query_map = {}
|
43
|
+
groups.each do |group|
|
44
|
+
@query_map[group.to_s] = @collection.where(@group => group)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
module QueryPlanner
|
3
|
+
class DefaultQueryPlanner
|
4
|
+
include Aggrobot::Helper
|
5
|
+
|
6
|
+
def initialize(collection, group)
|
7
|
+
@collection, @group = collection, group
|
8
|
+
end
|
9
|
+
|
10
|
+
def sub_query(group_name)
|
11
|
+
@group == DEFAULT_GROUP_BY ? @collection : @collection.where(@group => group_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def query_results(extra_cols = [])
|
15
|
+
return [] if collection_is_none?
|
16
|
+
columns = [@group, SqlFunctions.count] + extra_cols
|
17
|
+
results_query.pluck(*columns)
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def results_query
|
22
|
+
@result_query ||= @collection.group(@group)
|
23
|
+
end
|
24
|
+
|
25
|
+
def collection_is_none?
|
26
|
+
@collection.extending_values.include?(ActiveRecord::NullRelation)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
module QueryPlanner
|
3
|
+
class GroupLimitQueryPlanner < DefaultQueryPlanner
|
4
|
+
|
5
|
+
def initialize(collection, group, opts)
|
6
|
+
ParametersValidator.validate_options(opts, [:limit_to, :sort_by], [:always_include, :other_group, :order])
|
7
|
+
raise_error 'limit_to has to be a number' unless opts[:limit_to].is_a?(Fixnum)
|
8
|
+
super(collection, group)
|
9
|
+
@query_map = {}
|
10
|
+
process_top_groups_options(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def sub_query(group_name)
|
14
|
+
group_name == @other_group ? @collection.where.not(@top_groups_conditions) : @collection.where(@group => group_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def query_results(extra_cols = [])
|
18
|
+
return [] if collection_is_none?
|
19
|
+
columns = [@group, SqlFunctions.count] + extra_cols
|
20
|
+
top_group_results = results_query.where(@top_groups_conditions).pluck(*columns)
|
21
|
+
top_group_results + other_group_results(columns)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def other_group_results(columns)
|
27
|
+
if @other_group
|
28
|
+
columns[0] = SqlFunctions.sanitize(@other_group)
|
29
|
+
@collection.where.not(@top_groups_conditions).group(columns[0]).pluck(*columns)
|
30
|
+
else
|
31
|
+
[]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def results_query
|
36
|
+
@results_query ||= @collection.group(@group)
|
37
|
+
end
|
38
|
+
|
39
|
+
def calculate_top_groups(opts)
|
40
|
+
@collection.group(@group).order("#{opts[:sort_by]} #{opts[:order]}").limit(opts[:limit_to]).pluck(@group).flatten
|
41
|
+
end
|
42
|
+
|
43
|
+
def process_top_groups_options(opts)
|
44
|
+
opts[:order] ||= 'desc'
|
45
|
+
top_groups = calculate_top_groups(opts)
|
46
|
+
if opts[:always_include] && !top_groups.include?(opts[:always_include])
|
47
|
+
top_groups.pop
|
48
|
+
top_groups << opts[:always_include]
|
49
|
+
end
|
50
|
+
@top_groups_conditions = {@group => top_groups}
|
51
|
+
@other_group = opts[:other_group]
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Aggrobot
|
2
|
+
module SqlFunctions
|
3
|
+
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def sanitize(attr)
|
7
|
+
"'#{attr}'"
|
8
|
+
end
|
9
|
+
|
10
|
+
def desc(attr)
|
11
|
+
"#{attr} desc"
|
12
|
+
end
|
13
|
+
|
14
|
+
def count(attr = '*')
|
15
|
+
"COUNT(#{attr})"
|
16
|
+
end
|
17
|
+
|
18
|
+
def unique_count(attr = '*')
|
19
|
+
"COUNT(DISTINCT #{attr})"
|
20
|
+
end
|
21
|
+
|
22
|
+
def max(attr)
|
23
|
+
"MAX(#{attr})"
|
24
|
+
end
|
25
|
+
|
26
|
+
def min(attr)
|
27
|
+
"MIN(#{attr})"
|
28
|
+
end
|
29
|
+
|
30
|
+
def sum(attr = count)
|
31
|
+
"SUM(#{attr})"
|
32
|
+
end
|
33
|
+
|
34
|
+
def avg(attr, rounding = ROUNDING_DIGITS)
|
35
|
+
"ROUND(AVG(#{attr}), #{rounding})"
|
36
|
+
end
|
37
|
+
|
38
|
+
def group_collect(attr)
|
39
|
+
"GROUP_CONCAT(DISTINCT #{attr})"
|
40
|
+
end
|
41
|
+
|
42
|
+
def percent(total, attr = count, rounding = ROUNDING_DIGITS)
|
43
|
+
total == 0 ? "0" : "ROUND((#{attr}*100.0)/#{total}, #{rounding})"
|
44
|
+
end
|
45
|
+
|
46
|
+
def multiply(attr, multiplier, rounding = ROUNDING_DIGITS)
|
47
|
+
"ROUND(#{attr}*#{multiplier}, #{rounding})"
|
48
|
+
end
|
49
|
+
|
50
|
+
def divide(attr, divider, rounding = ROUNDING_DIGITS)
|
51
|
+
"ROUND(#{attr}/#{divider}, #{rounding})"
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/aggrobot/version.rb
CHANGED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Notice there is a .rspec file in the root folder. It defines rspec arguments
|
2
|
+
|
3
|
+
# Ruby 1.9 uses simplecov. The ENV['COVERAGE'] is set when rake coverage is run in ruby 1.9
|
4
|
+
if ENV['COVERAGE']
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start do
|
7
|
+
# Remove the spec folder from coverage. By default all code files are included. For more config options see
|
8
|
+
# https://github.com/colszowka/simplecov
|
9
|
+
add_filter File.expand_path('../../spec/', __FILE__)
|
10
|
+
coverage_dir(ENV['COVERAGE'] == 'integration' ? 'coverage/integration' :'coverage/unit')
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Modify load path so you can require 'mckinsey_external_ad' directly.
|
16
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
|
17
|
+
|
18
|
+
require 'rubygems'
|
19
|
+
# Loads bundler setup tasks. Now if I run spec without installing gems then it would say gem not installed and
|
20
|
+
# do bundle install instead of ugly load error on require.
|
21
|
+
require 'bundler/setup'
|
22
|
+
|
23
|
+
# This will require me all the gems automatically for the groups.
|
24
|
+
Bundler.require(:default, :test)
|
25
|
+
|
26
|
+
require 'aggrobot'
|
27
|
+
|
28
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
29
|
+
# in spec/support/ and its subdirectories.
|
30
|
+
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
|
31
|
+
|
32
|
+
# Set Rails environment as test
|
33
|
+
ENV['RAILS_ENV'] = 'test'
|
34
|
+
|
35
|
+
|
36
|
+
RSpec.configure do |config|
|
37
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
38
|
+
config.run_all_when_everything_filtered = true
|
39
|
+
config.filter_run :focus
|
40
|
+
|
41
|
+
# Run specs in random order to surface order dependencies. If you find an
|
42
|
+
# order dependency and want to debug it, you can fix the order by providing
|
43
|
+
# the seed, which is printed after each run.
|
44
|
+
# --seed 1234
|
45
|
+
config.order = 'random'
|
46
|
+
end
|
47
|
+
|
48
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
def add_expectations(obj, method, params)
|
2
|
+
if params.is_a?(Array)
|
3
|
+
obj.should_receive(method).with(*params)
|
4
|
+
elsif params.nil?
|
5
|
+
obj.should_receive(method)
|
6
|
+
else
|
7
|
+
obj.should_receive(method).with(params)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def should_receive_queries(obj, method_chain)
|
12
|
+
method_chain.each_with_index do |(method, params), idx|
|
13
|
+
if (idx + 1) == method_chain.size
|
14
|
+
return add_expectations(obj, method, params)
|
15
|
+
else
|
16
|
+
proxy = add_expectations(obj, method, params)
|
17
|
+
proxy.and_return(obj)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Aggrobot
|
4
|
+
module QueryPlanner
|
5
|
+
|
6
|
+
describe BucketedGroupsQueryPlanner do
|
7
|
+
|
8
|
+
let(:collection) { double }
|
9
|
+
let(:group) { 'group_col' }
|
10
|
+
let(:buckets) { [1..2, [3, 4, 5], 8, 9] }
|
11
|
+
subject(:query_planner) { BucketedGroupsQueryPlanner.new(collection, group, buckets: buckets) }
|
12
|
+
|
13
|
+
describe '#sub_query' do
|
14
|
+
before do
|
15
|
+
collection.stub(:where).with('group_col' => 1..2).and_return('collection for 1..2')
|
16
|
+
collection.stub(:where).with('group_col' => [3,4,5]).and_return('collection for 3,4,5')
|
17
|
+
collection.stub(:where).with('group_col' => 8).and_return('collection for 8')
|
18
|
+
collection.stub(:where).with('group_col' => 9).and_return('collection for 9')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns the correct subquery' do
|
22
|
+
expect(query_planner.sub_query((1..2).to_s)).to eq 'collection for 1..2'
|
23
|
+
expect(query_planner.sub_query([3,4,5].to_s)).to eq 'collection for 3,4,5'
|
24
|
+
expect(query_planner.sub_query(8.to_s)).to eq 'collection for 8'
|
25
|
+
expect(query_planner.sub_query(9.to_s)).to eq 'collection for 9'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#query_results' do
|
30
|
+
let(:bucketed_relation) { double }
|
31
|
+
before do
|
32
|
+
collection.stub(:where).and_return(bucketed_relation)
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'collection is none' do
|
36
|
+
before do
|
37
|
+
query_planner.stub(:collection_is_none? => true, :empty_default_groups => [])
|
38
|
+
end
|
39
|
+
it 'returns empty result set' do
|
40
|
+
expect(query_planner.query_results).to be_empty
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'collection is not none' do
|
45
|
+
before do
|
46
|
+
query_planner.stub(:collection_is_none? => false)
|
47
|
+
should_receive_queries(bucketed_relation, :group => SqlFunctions.sanitize((1..2).to_s), limit: 1,
|
48
|
+
pluck: [SqlFunctions.sanitize((1..2).to_s), SqlFunctions.count, :col1, :col2])
|
49
|
+
.and_return(['results for 1..2'])
|
50
|
+
should_receive_queries(bucketed_relation, :group => SqlFunctions.sanitize([3,4,5].to_s), limit: 1,
|
51
|
+
pluck: [SqlFunctions.sanitize([3,4,5].to_s), SqlFunctions.count, :col1, :col2])
|
52
|
+
.and_return(['results for 3,4,5'])
|
53
|
+
should_receive_queries(bucketed_relation, :group => SqlFunctions.sanitize(8.to_s), limit: 1,
|
54
|
+
pluck: [SqlFunctions.sanitize(8.to_s), SqlFunctions.count, :col1, :col2])
|
55
|
+
.and_return(['results for 8'])
|
56
|
+
should_receive_queries(bucketed_relation, :group => SqlFunctions.sanitize(9.to_s), limit: 1,
|
57
|
+
pluck: [SqlFunctions.sanitize(9.to_s), SqlFunctions.count, :col1, :col2])
|
58
|
+
.and_return(['results for 9'])
|
59
|
+
end
|
60
|
+
it 'returns empty result set' do
|
61
|
+
expect(query_planner.query_results([:col1, :col2])).to eq ["results for 1..2", "results for 3,4,5",
|
62
|
+
"results for 8", "results for 9"]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'empty buckets' do
|
67
|
+
before do
|
68
|
+
query_planner.stub(:collection_is_none? => false)
|
69
|
+
should_receive_queries(bucketed_relation, :group => SqlFunctions.sanitize(:populated.to_s), limit: 1,
|
70
|
+
pluck: [SqlFunctions.sanitize(:populated.to_s), SqlFunctions.count, :col1, :col2])
|
71
|
+
.and_return(['results for populated group'])
|
72
|
+
|
73
|
+
should_receive_queries(bucketed_relation, :group => SqlFunctions.sanitize(:empty.to_s), limit: 1,
|
74
|
+
pluck: [SqlFunctions.sanitize(:empty.to_s), SqlFunctions.count, :col1, :col2])
|
75
|
+
.and_return([])
|
76
|
+
bucketed_relation.stub(:none)
|
77
|
+
end
|
78
|
+
context 'without keep_empty option' do
|
79
|
+
subject(:query_planner) { BucketedGroupsQueryPlanner.new(collection, group, buckets: [:populated, :empty]) }
|
80
|
+
it 'returns only populated result set' do
|
81
|
+
expect(query_planner.query_results([:col1, :col2])).to eq ["results for populated group"]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'with keep_empty option' do
|
86
|
+
subject(:query_planner) { BucketedGroupsQueryPlanner.new(collection, group, buckets: [:populated, :empty], keep_empty: true) }
|
87
|
+
it 'returns both empty and populated result sets' do
|
88
|
+
expect(query_planner.query_results([:col1, :col2])).to eq ["results for populated group", ["empty", 0]]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Aggrobot
|
4
|
+
module QueryPlanner
|
5
|
+
|
6
|
+
describe DefaultQueryPlanner do
|
7
|
+
let(:collection) { double }
|
8
|
+
let(:group) { 'group_col' }
|
9
|
+
subject(:query_planner) { DefaultQueryPlanner.new(collection, group) }
|
10
|
+
|
11
|
+
describe '#sub_query' do
|
12
|
+
context 'when group was specified' do
|
13
|
+
before do
|
14
|
+
collection.should_receive(:where).with('group_col' => 'group').and_return('collection')
|
15
|
+
end
|
16
|
+
it 'returns the correct subquery' do
|
17
|
+
expect(query_planner.sub_query('group')).to eq 'collection'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'when default group' do
|
22
|
+
let(:query_planner) { DefaultQueryPlanner.new(collection, DEFAULT_GROUP_BY) }
|
23
|
+
it 'returns the correct subquery' do
|
24
|
+
expect(query_planner.sub_query('group')).to eq collection
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#query_results' do
|
30
|
+
context 'collection is none' do
|
31
|
+
before { query_planner.stub(:collection_is_none? => true) }
|
32
|
+
it { expect(query_planner.query_results).to be_empty }
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'collection is not none' do
|
36
|
+
let(:grouped_relation) { double }
|
37
|
+
before do
|
38
|
+
query_planner.stub(:collection_is_none? => false)
|
39
|
+
|
40
|
+
collection.should_receive(:group).with('group_col').and_return(grouped_relation)
|
41
|
+
grouped_relation.should_receive(:pluck)
|
42
|
+
.with('group_col', SqlFunctions.count, 'extra_col1', 'extra_col2')
|
43
|
+
.and_return(:results)
|
44
|
+
end
|
45
|
+
it 'returns results for columns' do
|
46
|
+
expect(query_planner.query_results(['extra_col1', 'extra_col2'])).to eq :results
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Aggrobot
|
4
|
+
module QueryPlanner
|
5
|
+
describe GroupLimitQueryPlanner do
|
6
|
+
let(:collection) { double }
|
7
|
+
let(:group) { 'group_col' }
|
8
|
+
|
9
|
+
context 'initialization' do
|
10
|
+
before do
|
11
|
+
GroupLimitQueryPlanner.any_instance.stub(:process_top_groups_options)
|
12
|
+
end
|
13
|
+
it 'requires explicit parameters' do
|
14
|
+
expect { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2) }.to raise_error
|
15
|
+
expect { GroupLimitQueryPlanner.new(collection, group, :sort_by => 2) }.to raise_error
|
16
|
+
expect { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2, :sort_by => 2) }.to_not raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#sub_query' do
|
21
|
+
|
22
|
+
context 'with only default options' do
|
23
|
+
before do
|
24
|
+
should_receive_queries(collection, :group => group, :order => 'order_col desc',
|
25
|
+
:limit => 2, :pluck => group).and_return(['group1', 'group2'])
|
26
|
+
end
|
27
|
+
subject(:query_planner) { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2,
|
28
|
+
:sort_by => :order_col, :other_group => 'others') }
|
29
|
+
context 'for one of the top groups' do
|
30
|
+
it 'gives query with only that group in condition' do
|
31
|
+
should_receive_queries(collection, :where => {'group_col' => 'group1'}).and_return(:sub_query1)
|
32
|
+
expect(query_planner.sub_query('group1')).to eq :sub_query1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'for others group' do
|
37
|
+
it 'gives query with only that group in condition' do
|
38
|
+
should_receive_queries(collection, :where => nil, not: {'group_col' => ['group1', 'group2']}).and_return(:sub_query_other)
|
39
|
+
expect(query_planner.sub_query('others')).to eq :sub_query_other
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with always include option' do
|
45
|
+
before do
|
46
|
+
should_receive_queries(collection, :group => group, :order => 'order_col desc',
|
47
|
+
:limit => 2, :pluck => group).and_return(['group1', 'group2'])
|
48
|
+
end
|
49
|
+
subject(:query_planner) { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2,
|
50
|
+
:sort_by => :order_col, :other_group => 'others', :always_include => 'always') }
|
51
|
+
context 'for one of the top groups' do
|
52
|
+
it 'gives query with only that group in condition' do
|
53
|
+
should_receive_queries(collection, :where => {'group_col' => 'always'}).and_return(:sub_query_always)
|
54
|
+
expect(query_planner.sub_query('always')).to eq :sub_query_always
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#query_results' do
|
62
|
+
let(:columns) { ['group_col', 'COUNT(*)', 'col1', 'col2'] }
|
63
|
+
let(:other_columns) { ["'others'", 'COUNT(*)', 'col1', 'col2'] }
|
64
|
+
subject(:query_planner) { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2,
|
65
|
+
:sort_by => :order_col, :other_group => 'others') }
|
66
|
+
|
67
|
+
|
68
|
+
context 'when collection is none' do
|
69
|
+
before do
|
70
|
+
should_receive_queries(collection, :group => group, :order => 'order_col desc',
|
71
|
+
:limit => 2, :pluck => group).and_return(['group1', 'group2'])
|
72
|
+
query_planner.stub(:collection_is_none? => true)
|
73
|
+
end
|
74
|
+
it 'returns empty result' do
|
75
|
+
expect(query_planner.query_results).to be_empty
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'with reverse sort order' do
|
80
|
+
let(:conditions) { {'group_col' => ['group2', 'group1']} }
|
81
|
+
subject(:query_planner) { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2, :order => 'asc',
|
82
|
+
:sort_by => :order_col, :other_group => 'others') }
|
83
|
+
before do
|
84
|
+
should_receive_queries(collection, :group => group, :order => 'order_col asc',
|
85
|
+
:limit => 2, :pluck => group).and_return(['group2', 'group1'])
|
86
|
+
should_receive_queries(collection, :where => conditions,
|
87
|
+
:group => group, :pluck => columns).and_return([:group2_results, :group1_results])
|
88
|
+
should_receive_queries(collection, :where => nil, :not => conditions,
|
89
|
+
:group => "'others'", :pluck => other_columns).and_return([:others])
|
90
|
+
query_planner.stub(:collection_is_none? => false)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'returns results for reverse order' do
|
94
|
+
expect(query_planner.query_results(['col1', 'col2'])).to eq [:group2_results, :group1_results, :others]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when collection is not none and default sort order' do
|
99
|
+
let(:conditions) { {'group_col' => ['group1', 'group2']} }
|
100
|
+
|
101
|
+
before do
|
102
|
+
should_receive_queries(collection, :group => group, :order => 'order_col desc',
|
103
|
+
:limit => 2, :pluck => group).and_return(['group1', 'group2'])
|
104
|
+
query_planner.stub(:collection_is_none? => false)
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'with default options' do
|
108
|
+
it 'returns top group results' do
|
109
|
+
should_receive_queries(collection, :group => group, :where => conditions, :pluck => columns).and_return([:group1_results, :group2_results])
|
110
|
+
should_receive_queries(collection, :where => nil, :not => conditions,
|
111
|
+
:group => "'others'", :pluck => other_columns).and_return([:others])
|
112
|
+
expect(query_planner.query_results(['col1', 'col2'])).to eq [:group1_results, :group2_results, :others]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
context 'with always_include option added' do
|
118
|
+
subject(:query_planner) { GroupLimitQueryPlanner.new(collection, group, :limit_to => 2,
|
119
|
+
:sort_by => :order_col, :always_include => 'always', :other_group => 'others') }
|
120
|
+
let(:conditions) { {'group_col' => ['group1', 'always']} }
|
121
|
+
before do
|
122
|
+
query_planner.stub(:collection_is_none? => false)
|
123
|
+
end
|
124
|
+
it 'returns top group results inlcuding the always_include group' do
|
125
|
+
should_receive_queries(collection, :group => group, :where => conditions, :pluck => columns).and_return([:group1_results, :group2_results])
|
126
|
+
should_receive_queries(collection, :where => nil, :not => conditions,
|
127
|
+
:group => "'others'", :pluck => other_columns).and_return([:others])
|
128
|
+
expect(query_planner.query_results(['col1', 'col2'])).to eq [:group1_results, :group2_results, :others]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'aggrobot/sql_functions'
|
3
|
+
|
4
|
+
module Aggrobot
|
5
|
+
|
6
|
+
describe SqlFunctions do
|
7
|
+
before do
|
8
|
+
module SqlFunctions
|
9
|
+
ROUNDING_DIGITS = 2
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '.sql_attr' do
|
14
|
+
it 'returns an escaped sql attribute' do
|
15
|
+
expect(SqlFunctions.desc('attr')).to eq 'attr desc'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '.count' do
|
20
|
+
it 'get SQL Sum' do
|
21
|
+
expect(SqlFunctions.count('attr')).to eq 'COUNT(attr)'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '.unique_count' do
|
26
|
+
it 'gets distinct COUNT' do
|
27
|
+
expect(SqlFunctions.unique_count('attr')).to eq 'COUNT(DISTINCT attr)'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '.max' do
|
32
|
+
it 'gets max' do
|
33
|
+
expect(SqlFunctions.max('attr')).to eq 'MAX(attr)'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '.max' do
|
38
|
+
it 'gets min' do
|
39
|
+
expect(SqlFunctions.min('attr')).to eq 'MIN(attr)'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.sum' do
|
44
|
+
it 'gets sum' do
|
45
|
+
expect(SqlFunctions.sum('attr')).to eq 'SUM(attr)'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '.avg' do
|
50
|
+
it 'gets avg' do
|
51
|
+
expect(SqlFunctions.avg('attr')).to eq "ROUND(AVG(attr), #{SqlFunctions::ROUNDING_DIGITS})"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '.group_collect' do
|
56
|
+
it 'does group concat' do
|
57
|
+
expect(SqlFunctions.group_collect('attr')).to eq 'GROUP_CONCAT(DISTINCT attr)'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '.percent' do
|
62
|
+
it 'calculate percent' do
|
63
|
+
expect(SqlFunctions.percent('attr', 100)).to eq "ROUND((100*100.0)/attr, #{SqlFunctions::ROUNDING_DIGITS})"
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'calculate percent with default params' do
|
67
|
+
expect(SqlFunctions.percent('attr')).to eq "ROUND((COUNT(*)*100.0)/attr, #{SqlFunctions::ROUNDING_DIGITS})"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '.mysql' do
|
72
|
+
it 'multiplies' do
|
73
|
+
expect(SqlFunctions.multiply('attr', 100, 2)).to eq 'ROUND(attr*100, 2)'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '.divide' do
|
78
|
+
it 'divides' do
|
79
|
+
expect(SqlFunctions.divide('attr', 100, 2)).to eq 'ROUND(attr/100, 2)'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aggrobot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shadab Ahmed
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-12-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +52,34 @@ dependencies:
|
|
38
52
|
- - ! '>='
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.14.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.14.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rdoc
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ! '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
41
83
|
description: Easy and performant aggregation for rails
|
42
84
|
email:
|
43
85
|
- shadab.ansari@gmail.com
|
@@ -46,13 +88,33 @@ extensions: []
|
|
46
88
|
extra_rdoc_files: []
|
47
89
|
files:
|
48
90
|
- .gitignore
|
91
|
+
- .rspec
|
92
|
+
- .travis.yml
|
49
93
|
- Gemfile
|
94
|
+
- Guardfile
|
50
95
|
- LICENSE.txt
|
51
96
|
- README.md
|
52
97
|
- Rakefile
|
53
98
|
- aggrobot.gemspec
|
54
99
|
- lib/aggrobot.rb
|
100
|
+
- lib/aggrobot/aggregator.rb
|
101
|
+
- lib/aggrobot/aggrobot.rb
|
102
|
+
- lib/aggrobot/aggrobot_error.rb
|
103
|
+
- lib/aggrobot/helper.rb
|
104
|
+
- lib/aggrobot/query_planner.rb
|
105
|
+
- lib/aggrobot/query_planner/bucketed_groups_query_planner.rb
|
106
|
+
- lib/aggrobot/query_planner/default_query_planner.rb
|
107
|
+
- lib/aggrobot/query_planner/group_limit_query_planner.rb
|
108
|
+
- lib/aggrobot/railtie.rb
|
109
|
+
- lib/aggrobot/sql_functions.rb
|
55
110
|
- lib/aggrobot/version.rb
|
111
|
+
- spec/spec_helper.rb
|
112
|
+
- spec/support/chain_query.rb
|
113
|
+
- spec/unit/aggrobot/query_planners/bucketed_groups_query_planner_spec.rb
|
114
|
+
- spec/unit/aggrobot/query_planners/default_query_planner_spec.rb
|
115
|
+
- spec/unit/aggrobot/query_planners/group_limit_query_planner_spec.rb
|
116
|
+
- spec/unit/aggrobot/sql_functions_spec.rb
|
117
|
+
- spec/unit/aggrobot_spec.rb
|
56
118
|
homepage: ''
|
57
119
|
licenses:
|
58
120
|
- MIT
|
@@ -77,4 +139,11 @@ rubygems_version: 2.1.4
|
|
77
139
|
signing_key:
|
78
140
|
specification_version: 4
|
79
141
|
summary: Rails aggregation library
|
80
|
-
test_files:
|
142
|
+
test_files:
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
- spec/support/chain_query.rb
|
145
|
+
- spec/unit/aggrobot/query_planners/bucketed_groups_query_planner_spec.rb
|
146
|
+
- spec/unit/aggrobot/query_planners/default_query_planner_spec.rb
|
147
|
+
- spec/unit/aggrobot/query_planners/group_limit_query_planner_spec.rb
|
148
|
+
- spec/unit/aggrobot/sql_functions_spec.rb
|
149
|
+
- spec/unit/aggrobot_spec.rb
|