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 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