aggrobot 0.0.1 → 0.0.2
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 +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
|