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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NTYyNjYxMGE2OGY1MjU4ZWNmM2NiNmQwZDIwMWM2MjkzYjc4N2RmMw==
4
+ YjhjNWE3NWU3M2IxOWQxZWIxMjIwYTVjMmQ5Mjc5ZDFmYTRhZTI5Yg==
5
5
  data.tar.gz: !binary |-
6
- YjBlM2MxNTRlNjk1OGY3YjMyMmVjMTM4YTU1YjRkM2RhNzE4ODFiZA==
6
+ OTc4YjYzNjlmMjdlMjIzNzY4MTcwZTA5YWUwNjk2MTkyMzM4M2Q5MQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NDQ4ODhiYzNkODRjZDdjM2Y5NGRiZDMyNThkMTE1YzRjZTJjNzgxNTA3YWFk
10
- M2M1Njk4NjJlMzA0NDBjNmE5NjI2MzNjNWVjZjFlYTYwYzgxYmJmM2MyMjE2
11
- NzEwMTJiOTg2OGQ0NjExMjQyMGNmYzUzZGY1YjA2NDI5NWNlYjM=
9
+ ODk4NjY1NGJjZTIxMWZjMWZhODk4OTI4YTBhYmFiMzRjMDM1NjkzOTM4YTJi
10
+ OTdkYTA1MjEwNTBhODk2OGQwODU0NDIyNzA5YzI5OWQ3OTJlN2I4MWI4ZjMw
11
+ NzczYjY1OWRlYzY4ZDI5ZjE1MDcwMmZkZTdkYTYyMzAwYzgyNTE=
12
12
  data.tar.gz: !binary |-
13
- MTA5NDYxMTMxZTNlZWNmMGUwZDAxZGMzYmUzOWYyMjEzZjhjMGRmNzQ0MzEw
14
- ZjkwNTU0OTQ5MjQ0NGVmN2MzMzg1ZGFkNzNmM2Y5YTg5OGVjMzZiMzY2NDMw
15
- Nzg2YzBkMTJjNThiZGE3MGM2YWMxNmRlZDVhYTBjNGE3NjRmYjI=
13
+ YTA2MjhlMzI0ZjU1YzY4NWRlYWE3Y2VhMjRiOGJlNzA5YjljMWJhYWM1MGY4
14
+ MjZmZTA0MjEwOGNkNmZiMDc4ZWJmMmIwOWNjNTZkMjQ5MTgxYTFkMWRmZTcx
15
+ NjZjNDA1YzcxODgzYTRkZmY2N2VkZjU4OGJlMDI3YTU3YWI4ZTM=
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+
6
+ env:
7
+ - "RAILS_VERSION=4.0"
8
+ - "RAILS_VERSION=3.2"
9
+ - "RAILS_VERSION=3.1"
10
+ - "RAILS_VERSION=3.0"
11
+
12
+ script: bundle exec rspec
data/Gemfile CHANGED
@@ -2,3 +2,9 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in aggrobot.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem 'guard-rspec', require: false
8
+ gem 'growl', require: false
9
+ gem 'simplecov', require: false
10
+ end
@@ -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 "bundler/gem_tasks"
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
@@ -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 = "aggrobot"
7
+ spec.name = 'aggrobot'
8
8
  spec.version = Aggrobot::VERSION
9
- spec.authors = ["Shadab Ahmed"]
10
- spec.email = ["shadab.ansari@gmail.com"]
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 = "MIT"
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 = ["lib"]
19
+ spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
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
@@ -1,5 +1,36 @@
1
- require "aggrobot/version"
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
- # Your code goes here...
5
- end
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,6 @@
1
+ module Aggrobot
2
+ class AggrobotError < StandardError
3
+ end
4
+ end
5
+
6
+
@@ -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,11 @@
1
+ require 'rails/railtie'
2
+
3
+ module LogStasher
4
+ class Railtie < Rails::Railtie
5
+ config.aggrobot = ActiveSupport::OrderedOptions.new
6
+
7
+ initializer :aggrobot do |app|
8
+ Aggrobot.setup(app)
9
+ end
10
+ end
11
+ 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
@@ -1,3 +1,3 @@
1
1
  module Aggrobot
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+
3
+ describe Aggrobot do
4
+ 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.1
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-03 00:00:00.000000000 Z
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