aub-record_filter 0.1.4 → 0.2.0

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.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 4
2
+ :patch: 0
3
3
  :major: 0
4
- :minor: 1
4
+ :minor: 2
@@ -3,7 +3,7 @@ module RecordFilter
3
3
  module ClassMethods
4
4
 
5
5
  def filter(&block)
6
- Filter.new(self, nil, nil, &block)
6
+ Filter.new(self, nil, &block)
7
7
  end
8
8
 
9
9
  def named_filter(name, &block)
@@ -15,7 +15,7 @@ module RecordFilter
15
15
 
16
16
  (class << self; self; end).instance_eval do
17
17
  define_method(name.to_s) do |*args|
18
- Filter.new(self, name, nil, *args)
18
+ Filter.new(self, name, *args)
19
19
  end
20
20
  end
21
21
  end
@@ -18,7 +18,7 @@ module RecordFilter
18
18
  when DSL::Conjunction
19
19
  result.add_conjunction(create_from(step, table))
20
20
  when DSL::Join
21
- join = result.add_join_on_association(step.association)
21
+ join = result.add_join_on_association(step.association, step.join_type)
22
22
  result.add_conjunction(create_from(step.conjunction, join.right_table))
23
23
  when DSL::ClassJoin
24
24
  join = result.add_join_on_class(
@@ -55,14 +55,14 @@ module RecordFilter
55
55
  conjunction
56
56
  end
57
57
 
58
- def add_join_on_association(association_name)
58
+ def add_join_on_association(association_name, join_type)
59
59
  table = @table
60
60
  while association_name.is_a?(Hash)
61
- result = table.join_association(association_name.keys[0])
61
+ result = table.join_association(association_name.keys[0], join_type)
62
62
  table = result.right_table
63
63
  association_name = association_name.values[0]
64
64
  end
65
- table.join_association(association_name)
65
+ table.join_association(association_name, join_type)
66
66
  end
67
67
 
68
68
  def add_join_on_class(join_class, join_type, table_alias, conditions)
@@ -94,16 +94,20 @@ module RecordFilter
94
94
  else
95
95
  @restrictions.map do |restriction|
96
96
  conditions = restriction.to_conditions
97
- conditions[0] = "(#{conditions[0]})"
98
- conditions
99
- end.inject do |conditions, new_conditions|
97
+ if conditions
98
+ conditions[0] = "(#{conditions[0]})"
99
+ conditions
100
+ else
101
+ nil
102
+ end
103
+ end.compact.inject do |conditions, new_conditions|
100
104
  conditions.first << " #{conjunctor} #{new_conditions.shift}"
101
105
  conditions.concat(new_conditions)
102
106
  conditions
103
107
  end
104
108
  end
105
109
  end
106
- result[0] = "!(#{result[0]})" if (negated && !result.nil? && !result[0].nil?)
110
+ result[0] = "NOT (#{result[0]})" if (negated && !result.nil? && !result[0].nil?)
107
111
  result
108
112
  end
109
113
 
@@ -19,10 +19,10 @@ module RecordFilter
19
19
  @steps << dsl.conjunction
20
20
  end
21
21
 
22
- def add_join(association, &block)
22
+ def add_join(association, join_type, &block)
23
23
  dsl = ConjunctionDSL.new(@model_class, Conjunction.new(@model_class, :all_of))
24
24
  dsl.instance_eval(&block) if block
25
- @steps << Join.new(association, dsl.conjunction)
25
+ @steps << Join.new(association, join_type, dsl.conjunction)
26
26
  dsl
27
27
  end
28
28
 
@@ -37,8 +37,13 @@ module RecordFilter
37
37
  end
38
38
 
39
39
  # join
40
- def having(association, &block)
41
- @conjunction.add_join(association, &block)
40
+ def having(join_type_or_association, association=nil, &block)
41
+ if association.nil?
42
+ association, join_type = join_type_or_association, nil
43
+ else
44
+ join_type = join_type_or_association
45
+ end
46
+ @conjunction.add_join(association, join_type, &block)
42
47
  end
43
48
 
44
49
  def join(clazz, join_type, table_alias=nil, &block)
@@ -2,10 +2,10 @@ module RecordFilter
2
2
  module DSL
3
3
  class Join
4
4
 
5
- attr_reader :association, :conjunction
5
+ attr_reader :association, :join_type, :conjunction
6
6
 
7
- def initialize(association, conjunction)
8
- @association, @conjunction = association, conjunction
7
+ def initialize(association, join_type, conjunction)
8
+ @association, @join_type, @conjunction = association, join_type, conjunction
9
9
  end
10
10
  end
11
11
  end
@@ -1,35 +1,92 @@
1
1
  module RecordFilter
2
2
  class Filter
3
3
 
4
- delegate :inspect, :to => :loaded_data
4
+ NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?)
5
+ [].methods.each do |m|
6
+ unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
7
+ delegate m, :to => :loaded_data
8
+ end
9
+ end
5
10
 
6
- def initialize(clazz, named_filter, combine_conjunction, *args, &block)
11
+ def initialize(clazz, named_filter, *args, &block)
12
+ @current_scoped_methods = clazz.send(:current_scoped_methods)
7
13
  @clazz = clazz
8
14
 
9
15
  @dsl = dsl_for_named_filter(@clazz, named_filter)
10
16
  @dsl.instance_eval(&block) if block
11
17
  @dsl.send(named_filter, *args) if named_filter && @dsl.respond_to?(named_filter)
12
- @dsl.conjunction.steps.unshift(combine_conjunction.steps).flatten! if combine_conjunction
18
+ @query = Query.new(@clazz, @dsl.conjunction)
19
+ end
20
+
21
+ def first(*args)
22
+ if args.first.kind_of?(Integer)
23
+ loaded_data.first(*args)
24
+ else
25
+ do_with_scope do
26
+ @clazz.find(:first, *args)
27
+ end
28
+ end
29
+ end
30
+
31
+ def last(*args)
32
+ if args.first.kind_of?(Integer)
33
+ loaded_data.last(*args)
34
+ else
35
+ do_with_scope do
36
+ @clazz.find(:last, *args)
37
+ end
38
+ end
39
+ end
40
+
41
+ def size
42
+ @loaded_data ? @loaded_data.length : count
43
+ end
44
+
45
+ def empty?
46
+ @loaded_data ? @loaded_data.empty? : count.zero?
47
+ end
48
+
49
+ def any?
50
+ if block_given?
51
+ loaded_data.any? { |*block_args| yield(*block_args) }
52
+ else
53
+ !empty?
54
+ end
13
55
  end
14
56
 
15
57
  def filter(&block)
16
- Filter.new(@clazz, nil, @dsl.conjunction, &block)
58
+ do_with_scope do
59
+ Filter.new(@clazz, nil, &block)
60
+ end
17
61
  end
18
62
 
63
+ def proxy_options(count_query=false)
64
+ @query.to_find_params(count_query)
65
+ end
66
+
67
+ protected
68
+
19
69
  def method_missing(method, *args, &block)
20
70
  if @clazz.named_filters.include?(method)
21
- Filter.new(@clazz, method, @dsl.conjunction, *args)
71
+ do_with_scope do
72
+ Filter.new(@clazz, method, *args)
73
+ end
22
74
  else
23
- loaded_data.send(method, *args, &block)
75
+ do_with_scope(method == :count) do
76
+ @clazz.send(method, *args, &block)
77
+ end
24
78
  end
25
79
  end
26
80
 
27
- protected
28
-
29
- def loaded_data
30
- @loaded_data ||= begin
31
- query = Query.new(@clazz, @dsl.conjunction)
32
- @clazz.scoped(query.to_find_params)
81
+ def do_with_scope(count_query=false, &block)
82
+ @clazz.send(:with_scope, { :find => proxy_options(count_query), :create => proxy_options(count_query) }, :reverse_merge) do
83
+ if @current_scoped_methods
84
+ @clazz.send(:with_scope, @current_scoped_methods) do
85
+ block.call
86
+ end
87
+ else
88
+ block.call
89
+ end
33
90
  end
34
91
  end
35
92
 
@@ -42,5 +99,11 @@ module RecordFilter
42
99
  end
43
100
  nil
44
101
  end
102
+
103
+ def loaded_data
104
+ @loaded_data ||= do_with_scope do
105
+ @clazz.find(:all)
106
+ end
107
+ end
45
108
  end
46
109
  end
@@ -17,7 +17,7 @@ module RecordFilter
17
17
  raise ColumnNotFoundException.new("The column #{column} was not found in #{table.table_name}.")
18
18
  end
19
19
 
20
- "#{table.table_name}.#{column}"
20
+ "#{table.table_alias}.#{column}"
21
21
  end
22
22
  end
23
23
  end
@@ -2,9 +2,10 @@ module RecordFilter
2
2
  class Join
3
3
  attr_reader :left_table, :right_table
4
4
 
5
- def initialize(left_table, right_table, join_conditions, join_type=:inner)
5
+ def initialize(left_table, right_table, join_conditions, join_type=nil)
6
6
  @left_table, @right_table, @join_conditions, @join_type =
7
7
  left_table, right_table, join_conditions, join_type
8
+ @join_type ||= :inner
8
9
  end
9
10
 
10
11
  def to_sql
@@ -20,6 +21,10 @@ module RecordFilter
20
21
  "#{join_type_string} JOIN #{@right_table.table_name} AS #{@right_table.table_alias} ON #{predicate_sql}"
21
22
  end
22
23
 
24
+ def requires_distinct_select?
25
+ [:left, :outer, :left_outer].include?(@join_type)
26
+ end
27
+
23
28
  protected
24
29
 
25
30
  def condition_to_predicate_part(condition)
@@ -51,6 +56,8 @@ module RecordFilter
51
56
  @join_type_string ||= case(@join_type)
52
57
  when :inner then 'INNER'
53
58
  when :left then 'LEFT'
59
+ when :left_outer then 'LEFT OUTER'
60
+ when :outer then 'OUTER'
54
61
  else nil
55
62
  end
56
63
  end
@@ -22,7 +22,7 @@ module RecordFilter
22
22
  raise ColumnNotFoundException.new("The column #{column} was not found in #{table.table_name}.")
23
23
  end
24
24
 
25
- "#{table.table_name}.#{column} #{dir}"
25
+ "#{table.table_alias}.#{column} #{dir}"
26
26
  end
27
27
  end
28
28
  end
@@ -6,10 +6,19 @@ module RecordFilter
6
6
  @conjunction = RecordFilter::Conjunctions::Base.create_from(dsl_conjunction, @table)
7
7
  end
8
8
 
9
- def to_find_params
10
- params = { :conditions => @conjunction.to_conditions }
9
+ def to_find_params(count_query=false)
10
+ params = {}
11
+ conditions = @conjunction.to_conditions
12
+ params = { :conditions => conditions } if conditions
11
13
  joins = @table.all_joins
12
14
  params[:joins] = joins.map { |join| join.to_sql } * ' ' unless joins.empty?
15
+ if (joins.any? { |j| j.requires_distinct_select? })
16
+ if count_query
17
+ params[:select] = "DISTINCT #{@table.model_class.quoted_table_name}.#{@table.model_class.primary_key}"
18
+ else
19
+ params[:select] = "DISTINCT #{@table.model_class.quoted_table_name}.*"
20
+ end
21
+ end
13
22
  orders = @table.orders
14
23
  params[:order] = orders.map { |order| order.to_sql } * ', ' unless orders.empty?
15
24
  group_bys = @table.group_bys
@@ -15,7 +15,7 @@ module RecordFilter
15
15
  end
16
16
 
17
17
  def to_negative_sql
18
- "!(#{to_positive_sql})"
18
+ "NOT (#{to_positive_sql})"
19
19
  end
20
20
 
21
21
  def self.class_from_operator(operator)
@@ -29,7 +29,7 @@ module RecordFilter
29
29
  end
30
30
 
31
31
  def to_negative_sql
32
- "#{@column_name} != ?"
32
+ "#{@column_name} <> ?"
33
33
  end
34
34
  end
35
35
 
@@ -16,18 +16,25 @@ module RecordFilter
16
16
  @model_class.quoted_table_name
17
17
  end
18
18
 
19
- def join_association(association_name)
19
+ def join_association(association_name, join_type=nil)
20
20
  @joins_cache[association_name] ||=
21
21
  begin
22
22
  association = @model_class.reflect_on_association(association_name)
23
23
  if association.nil?
24
24
  raise AssociationNotFoundException.new("The association #{association_name} was not found on #{@model_class.name}.")
25
25
  end
26
- case association.macro
27
- when :belongs_to, :has_many, :has_one
28
- simple_join(association)
29
- when :has_and_belongs_to_many
30
- compound_join(association)
26
+
27
+ if (association.options[:through])
28
+ through_association = @model_class.reflect_on_association(association.options[:through])
29
+ through_join = join_association(association.options[:through], join_type)
30
+ through_join.right_table.join_association(association_name, join_type)
31
+ else
32
+ case association.macro
33
+ when :belongs_to, :has_many, :has_one
34
+ simple_join(association, join_type)
35
+ when :has_and_belongs_to_many
36
+ compound_join(association, join_type)
37
+ end
31
38
  end
32
39
  end
33
40
  end
@@ -62,7 +69,7 @@ module RecordFilter
62
69
 
63
70
  private
64
71
 
65
- def simple_join(association)
72
+ def simple_join(association, join_type)
66
73
  join_predicate =
67
74
  case association.macro
68
75
  when :belongs_to
@@ -70,19 +77,23 @@ module RecordFilter
70
77
  when :has_many, :has_one
71
78
  [{ :id => association.primary_key_name.to_sym }]
72
79
  end
80
+
81
+ if association.options[:as]
82
+ join_predicate << DSL::Restriction.new(association.options[:as].to_s + '_type').equal_to(association.active_record.base_class.name)
83
+ end
73
84
  join_table = Table.new(association.klass, alias_for_association(association))
74
- @joins << join = Join.new(self, join_table, join_predicate)
85
+ @joins << join = Join.new(self, join_table, join_predicate, join_type)
75
86
  join
76
87
  end
77
88
 
78
- def compound_join(association)
89
+ def compound_join(association, join_type)
79
90
  pivot_join_predicate = [{ :id => association.primary_key_name.to_sym }]
80
91
  table_name = @model_class.connection.quote_table_name(association.options[:join_table])
81
92
  pivot_table = PivotTable.new(table_name, association, "__#{alias_for_association(association)}")
82
- pivot_join = Join.new(self, pivot_table, pivot_join_predicate)
93
+ pivot_join = Join.new(self, pivot_table, pivot_join_predicate, join_type)
83
94
  join_predicate = [{ association.association_foreign_key.to_sym => :id }]
84
95
  join_table = Table.new(association.klass, alias_for_association(association))
85
- pivot_table.joins << join = Join.new(pivot_table, join_table, join_predicate)
96
+ pivot_table.joins << join = Join.new(pivot_table, join_table, join_predicate, join_type)
86
97
  @joins << pivot_join
87
98
  join
88
99
  end
@@ -1,7 +1,10 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper')
2
2
 
3
3
  describe 'raising exceptions' do
4
-
4
+ before do
5
+ TestModel.extended_models.each { |model| model.last_find = {} }
6
+ end
7
+
5
8
  describe 'on missing associations' do
6
9
  it 'should get AssociationNotFoundException' do
7
10
  lambda {
@@ -73,7 +73,34 @@ describe 'explicit joins' do
73
73
  end
74
74
 
75
75
  it 'should add the correct join' do
76
- Review.last_find[:joins].should == %q(LEFT JOIN "features" AS reviews__Feature ON (reviews__Feature.featurable_type IS NULL) AND (reviews__Feature.featurable_id >= 12) AND (reviews__Feature.priority != 6))
76
+ Review.last_find[:joins].should == %q(LEFT JOIN "features" AS reviews__Feature ON (reviews__Feature.featurable_type IS NULL) AND (reviews__Feature.featurable_id >= 12) AND (reviews__Feature.priority <> 6))
77
+ end
78
+ end
79
+
80
+ describe 'using implicit and explicit joins together with conditions' do
81
+ before do
82
+ Blog.named_filter :somethings do
83
+ having(:ads) do
84
+ with(:content, nil)
85
+ end
86
+ join(Post, :left) do
87
+ on(:id => :blog_id)
88
+ join(Comment, :inner) do
89
+ on(:id => :post_id)
90
+ on(:offensive, true)
91
+ end
92
+ end
93
+ group_by(:id)
94
+ end
95
+ Blog.somethings.inspect
96
+ end
97
+
98
+ it 'should produce the correct conditions' do
99
+ Blog.last_find[:conditions].should == [%q((blogs__ads.content IS NULL))]
100
+ end
101
+
102
+ it 'should produce the correct join' do
103
+ Blog.last_find[:joins].should == %q(INNER JOIN "ads" AS blogs__ads ON "blogs".id = blogs__ads.blog_id LEFT JOIN "posts" AS blogs__Post ON "blogs".id = blogs__Post.blog_id INNER JOIN "comments" AS blogs__Post__Comment ON blogs__Post.id = blogs__Post__Comment.post_id AND (blogs__Post__Comment.offensive = 't'))
77
104
  end
78
105
  end
79
106
  end
@@ -191,7 +191,7 @@ describe 'implicit joins' do
191
191
  end
192
192
 
193
193
  it 'should create the correct condition' do
194
- Comment.last_find[:conditions].should == [%q("comments".offensive != ?), false]
194
+ Comment.last_find[:conditions].should == [%q("comments".offensive <> ?), false]
195
195
  end
196
196
  end
197
197
 
@@ -220,4 +220,94 @@ describe 'implicit joins' do
220
220
  Comment.last_find[:conditions].should == [%q(("comments".contents IS NOT NULL) AND ("comments".offensive = ?)), true]
221
221
  end
222
222
  end
223
+
224
+ describe 'passing the join type to having' do
225
+ before do
226
+ Blog.filter do
227
+ having(:left_outer, :posts) do
228
+ with(:permalink, 'ack')
229
+ end
230
+ end.inspect
231
+ end
232
+
233
+ it 'should create the correct condition' do
234
+ Blog.last_find[:conditions].should == [%q(blogs__posts.permalink = ?), 'ack']
235
+ end
236
+
237
+ it 'should create the correct join' do
238
+ Blog.last_find[:joins].should == %q(LEFT OUTER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id)
239
+ end
240
+ end
241
+
242
+ describe 'passing the join type to having with multiple joins' do
243
+ before do
244
+ Blog.filter do
245
+ having(:left_outer, :posts => :comments) do
246
+ with(:offensive, true)
247
+ end
248
+ end.inspect
249
+ end
250
+
251
+ it 'should create the correct condition' do
252
+ Blog.last_find[:conditions].should == [%q(blogs__posts__comments.offensive = ?), true]
253
+ end
254
+
255
+ it 'should create the correct join' do
256
+ Blog.last_find[:joins].should == %q(LEFT OUTER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id LEFT OUTER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)
257
+ end
258
+ end
259
+
260
+ describe 'on polymorphic associations' do
261
+ before do
262
+ PublicPost.filter do
263
+ having(:reviews) do
264
+ with(:stars_count, 3)
265
+ end
266
+ end.inspect
267
+ end
268
+
269
+ it 'should create the correct condition' do
270
+ PublicPost.last_find[:conditions].should == [%q(posts__reviews.stars_count = ?), 3]
271
+ end
272
+
273
+ it 'should create the correct join' do
274
+ PublicPost.last_find[:joins].should == %q(INNER JOIN "reviews" AS posts__reviews ON "posts".id = posts__reviews.reviewable_id AND (posts__reviews.reviewable_type = 'Post'))
275
+ end
276
+ end
277
+
278
+ describe 'on has_many_through associations' do
279
+ before do
280
+ Blog.filter do
281
+ having(:comments) do
282
+ with(:offensive, true)
283
+ end
284
+ end.inspect
285
+ end
286
+
287
+ it 'should create the correct condition' do
288
+ Blog.last_find[:conditions].should == [%q(blogs__posts__comments.offensive = ?), true]
289
+ end
290
+
291
+ it 'should create the correct join' do
292
+ Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)
293
+ end
294
+ end
295
+
296
+ describe 'on has_one_through associations' do
297
+ before do
298
+ Post.filter do
299
+ having(:user) do
300
+ with(:first_name, 'Joe')
301
+ end
302
+ end.inspect
303
+ end
304
+
305
+ it 'should create the correct condition' do
306
+ Post.last_find[:conditions].should == [%q(posts__author__user.first_name = ?), 'Joe']
307
+ end
308
+
309
+ it 'should create the correct join' do
310
+ Post.last_find[:joins].should == %q(INNER JOIN "authors" AS posts__author ON "posts".id = posts__author.post_id INNER JOIN "users" AS posts__author__user ON posts__author.user_id = posts__author__user.id)
311
+ end
312
+ end
223
313
  end
@@ -21,7 +21,7 @@ describe 'filter qualifiers' do
21
21
 
22
22
  describe 'with multiple calls to limit' do
23
23
  before do
24
- Post.filter do
24
+ Post.filter do
25
25
  limit 5
26
26
  with :published, true
27
27
  limit 6
@@ -115,7 +115,7 @@ describe 'filter qualifiers' do
115
115
  end
116
116
 
117
117
  it 'should add the limit to the parameters' do
118
- Post.last_find[:order].should == %q("photos".path DESC, "posts".permalink ASC)
118
+ Post.last_find[:order].should == %q(posts__photo.path DESC, "posts".permalink ASC)
119
119
  end
120
120
  end
121
121
 
@@ -184,7 +184,7 @@ describe 'filter qualifiers' do
184
184
  group_by(:created_at)
185
185
  group_by(:photo => :format)
186
186
  end.inspect
187
- Post.last_find[:group].should == %q("posts".created_at, "photos".format)
187
+ Post.last_find[:group].should == %q("posts".created_at, posts__photo.format)
188
188
  end
189
189
  end
190
190
  end
data/spec/models.rb ADDED
@@ -0,0 +1,70 @@
1
+ class Ad < ActiveRecord::Base
2
+ extend TestModel
3
+ belongs_to :blog
4
+ end
5
+
6
+
7
+ class Author < ActiveRecord::Base
8
+ extend TestModel
9
+ belongs_to :user
10
+ belongs_to :post
11
+ end
12
+
13
+
14
+ class Blog < ActiveRecord::Base
15
+ extend TestModel
16
+ has_many :posts
17
+ has_many :comments, :through => :posts
18
+ has_many :ads
19
+ end
20
+
21
+
22
+ class Comment < ActiveRecord::Base
23
+ extend TestModel
24
+ belongs_to :post
25
+ belongs_to :user
26
+ end
27
+
28
+
29
+ class Feature < ActiveRecord::Base
30
+ extend TestModel
31
+ belongs_to :featurable, :polymorphic => true
32
+ end
33
+
34
+
35
+ class Photo < ActiveRecord::Base
36
+ belongs_to :post
37
+ end
38
+
39
+
40
+ class Post < ActiveRecord::Base
41
+ extend TestModel
42
+ belongs_to :blog
43
+ has_many :comments
44
+ has_one :photo
45
+ has_and_belongs_to_many :tags
46
+ has_many :features, :as => :featurable
47
+ has_many :reviews, :as => :reviewable
48
+ has_one :author
49
+ has_one :user, :through => :author
50
+ end
51
+
52
+ class PublicPost < Post
53
+ end
54
+
55
+ class Review < ActiveRecord::Base
56
+ extend TestModel
57
+ belongs_to :reviewable, :polymorphic => true
58
+ end
59
+
60
+
61
+ class Tag < ActiveRecord::Base
62
+ has_and_belongs_to_many :posts
63
+ end
64
+
65
+
66
+ class User < ActiveRecord::Base
67
+ extend TestModel
68
+ has_one :author
69
+ has_many :comments
70
+ end
@@ -50,7 +50,8 @@ describe 'named filters' do
50
50
 
51
51
  it 'should call the filter passing all of the arguments' do
52
52
  Blog.with_name_and_post_with_permalink('booya', 'ftw').inspect
53
- Blog.last_find[:conditions].should == [%q(("blogs".name = ?) AND (blogs__posts.permalink = ?)), 'booya', 'ftw']
53
+ Blog.last_find[:conditions].should ==
54
+ [%q(("blogs".name = ?) AND (blogs__posts.permalink = ?)), 'booya', 'ftw']
54
55
  end
55
56
  end
56
57
 
@@ -81,7 +82,8 @@ describe 'named filters' do
81
82
 
82
83
  it 'should execute the parent class filters correctly' do
83
84
  NiceComment.with_contents('test contents').inspect
84
- NiceComment.last_find[:conditions].should == [%q("comments".contents = ?), 'test contents']
85
+ NiceComment.last_find[:conditions].should ==
86
+ [%q("comments".contents = ?), 'test contents']
85
87
  end
86
88
 
87
89
  it 'should not have the subclass filters in the parent class' do
@@ -90,7 +92,8 @@ describe 'named filters' do
90
92
 
91
93
  it 'should have parent class filters in the subclass' do
92
94
  NiceComment.offensive.with_contents('something').inspect
93
- NiceComment.last_find[:conditions].should == [%q(("comments".offensive = ?) AND ("comments".contents = ?)), true, 'something']
95
+ NiceComment.last_find[:conditions].should ==
96
+ %q(("comments".contents = 'something') AND ("comments".offensive = 't'))
94
97
  end
95
98
 
96
99
  it 'should provide access to the named filters' do
@@ -126,25 +129,25 @@ describe 'named filters' do
126
129
 
127
130
  it 'should chain the filters into a single query' do
128
131
  Post.for_blog(1).with_offensive_comments.inspect
129
- Post.last_find[:conditions].should == [%q((posts__blog.id = ?) AND (posts__comments.offensive = ?)), 1, true]
130
- Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
132
+ Post.last_find[:conditions].should == %q((posts__comments.offensive = 't') AND (posts__blog.id = 1))
133
+ Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id), %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)]
131
134
  end
132
135
 
133
136
  it 'should remove duplicate joins' do
134
137
  Post.for_blog(1).with_offensive_comments.with_interesting_comments.inspect
135
- Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
138
+ Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id), %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)]
136
139
  end
137
140
 
138
141
  it 'should allow for filtering a named_filter' do
139
142
  Post.for_blog(1).filter { having(:comments).with :offensive, true }.inspect
140
- Post.last_find[:conditions].should == [%q((posts__blog.id = ?) AND (posts__comments.offensive = ?)), 1, true]
141
- Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
143
+ Post.last_find[:conditions].should == %q((posts__comments.offensive = 't') AND (posts__blog.id = 1))
144
+ Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id), %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)]
142
145
  end
143
146
 
144
147
  it 'should allow for applying a named filter to a filter' do
145
148
  Post.filter { having(:comments).with :offensive, false }.for_blog(1).inspect
146
- Post.last_find[:conditions].should == [%q((posts__comments.offensive = ?) AND (posts__blog.id = ?)), false, 1]
147
- Post.last_find[:joins].should == %q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)
149
+ Post.last_find[:conditions].should == %q((posts__blog.id = 1) AND (posts__comments.offensive = 'f'))
150
+ Post.last_find[:joins].should == [%q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id), %q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)]
148
151
  end
149
152
 
150
153
  it 'should not change the inner filter conditions when chaining filters' do
@@ -163,11 +166,74 @@ describe 'named filters' do
163
166
 
164
167
  it 'should not change an original filter when reusing it' do
165
168
  base = Post.for_blog(1)
166
- level1 = base.with_offensive_comments
169
+ level1 = base.with_offensive_comments.inspect
167
170
  level2 = base.with_interesting_comments
168
- level1.inspect
169
- Post.last_find[:conditions].should == [%q((posts__blog.id = ?) AND (posts__comments.offensive = ?)), 1, true]
170
- Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
171
+ Post.last_find[:conditions].should == %q((posts__comments.offensive = 't') AND (posts__blog.id = 1))
172
+ Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id), %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)]
173
+ end
174
+ end
175
+
176
+ describe 'chaining named filters with regular AR associations' do
177
+ before do
178
+ Post.named_filter(:published) do
179
+ with(:published, true)
180
+ end
181
+ @blog = Blog.create
182
+ @blog.posts.published.inspect
183
+ end
184
+
185
+ it 'should combine the conditions from the association with the named filter' do
186
+ Post.last_find[:conditions].should == "(\"posts\".published = 't') AND (\"posts\".blog_id = #{@blog.id})"
187
+ end
188
+ end
189
+
190
+ describe 'chaining named filters with AR associations that involve joins' do
191
+ before do
192
+ Comment.named_filter(:with_user_named) do |name|
193
+ having(:user).with(:first_name, name)
194
+ end
195
+ @blog = Blog.create
196
+ @blog.comments.with_user_named('Bob').inspect
197
+ end
198
+
199
+ it 'should combine the joins from the association with the named filter' do
200
+ Comment.last_find[:joins].should == [%q(INNER JOIN "users" AS comments__user ON "comments".user_id = comments__user.id), %q(INNER JOIN "posts" ON "comments".post_id = "posts".id)]
201
+ end
202
+
203
+ it 'should combine the conditions from the association with the named filter' do
204
+ Comment.last_find[:conditions].should == "(comments__user.first_name = 'Bob') AND ((\"posts\".blog_id = #{@blog.id}))"
205
+ end
206
+ end
207
+
208
+ describe 'chaining multiple named filters with an AR association' do
209
+ before do
210
+ Comment.named_filter(:offensive) { with(:offensive, true) }
211
+ Comment.named_filter(:with_fun_in_contents) { with(:contents).like('%fun%') }
212
+ @post = Post.create
213
+ @post.comments.offensive.with_fun_in_contents.inspect
214
+ end
215
+
216
+ it 'should combine the conditions correctly' do
217
+ Comment.last_find[:conditions].should == "(\"comments\".contents LIKE '%fun%') AND ((\"comments\".offensive = 't') AND (\"comments\".post_id = #{@post.id}))"
218
+ end
219
+ end
220
+
221
+ describe 'chaining multiple named filters with different joins' do
222
+ before do
223
+ Blog.named_filter(:with_offensive_comments) { having(:comments).with(:offensive, true) }
224
+ Blog.named_filter(:with_ads_with_content) { |content| having(:ads).with(:content, content) }
225
+ end
226
+
227
+ it 'compile the joins correctly' do
228
+ Blog.with_offensive_comments.with_ads_with_content('ack').inspect
229
+ Blog.last_find[:joins].should == [%q(INNER JOIN "ads" AS blogs__ads ON "blogs".id = blogs__ads.blog_id), %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)]
230
+ end
231
+ end
232
+
233
+ describe 'with named filters that only include orders' do
234
+ it 'should have an empty conditions hash' do
235
+ Blog.named_filter(:ordered_by_id) { order(:id, :desc) }
236
+ Blog.ordered_by_id.proxy_options.should == { :order => %q("blogs".id DESC) }
171
237
  end
172
238
  end
173
239
  end
@@ -0,0 +1,49 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'proxying to the found data' do
4
+ before do
5
+ TestModel.extended_models.each { |model| model.last_find = {} }
6
+ end
7
+
8
+ describe 'calling first and last on a filter result' do
9
+ before do
10
+ Blog.all.each { |b| b.destroy }
11
+ Blog.named_filter(:by_name) do
12
+ order(:name)
13
+ end
14
+ @blog1 = Blog.create(:name => 'a')
15
+ @blog2 = Blog.create(:name => 'b')
16
+ @blog3 = Blog.create(:name => 'c')
17
+ end
18
+
19
+ it 'should return the first element in the result correctly' do
20
+ Blog.by_name.first.should == @blog1
21
+ end
22
+
23
+ it 'should return the last element in the result correctly' do
24
+ Blog.by_name.last.should == @blog3
25
+ end
26
+
27
+ it 'should proxy arguments through' do
28
+ Blog.by_name.first(:conditions => ['name = ?', 'b']).should == @blog2
29
+ end
30
+ end
31
+
32
+ describe 'calling reject! on a filter result' do
33
+ before do
34
+ Blog.all.each { |blog| blog.destroy }
35
+ Blog.named_filter(:by_name) do
36
+ order(:name)
37
+ end
38
+ @blog1 = Blog.create
39
+ @blog2 = Blog.create
40
+ end
41
+
42
+ it 'should remove the rejected items from the list' do
43
+ items = Blog.by_name
44
+ items.size.should == 2
45
+ items.reject! { |i| true } # should reject them all
46
+ items.size.should == 0
47
+ end
48
+ end
49
+ end
@@ -72,7 +72,7 @@ describe 'RecordFilter restrictions' do
72
72
  with(:permalink, 'eek')
73
73
  end
74
74
  end.inspect
75
- Post.last_find.should == { :conditions => [%q{!(("posts".blog_id = ?) OR ("posts".permalink = ?))}, 1, 'eek'] }
75
+ Post.last_find.should == { :conditions => [%q{NOT (("posts".blog_id = ?) OR ("posts".permalink = ?))}, 1, 'eek'] }
76
76
  end
77
77
 
78
78
  it 'should filter by not_all_of' do
@@ -82,7 +82,7 @@ describe 'RecordFilter restrictions' do
82
82
  with(:permalink, 'eek')
83
83
  end
84
84
  end.inspect
85
- Post.last_find.should == { :conditions => [%q{!(("posts".blog_id = ?) AND ("posts".permalink = ?))}, 1, 'eek'] }
85
+ Post.last_find.should == { :conditions => [%q{NOT (("posts".blog_id = ?) AND ("posts".permalink = ?))}, 1, 'eek'] }
86
86
  end
87
87
 
88
88
  it 'should filter by disjunction' do
@@ -137,6 +137,6 @@ describe 'RecordFilter restrictions' do
137
137
  Post.filter do
138
138
  with(:permalink).not.equal_to(filter_class.name)
139
139
  end.inspect
140
- Post.last_find.should == { :conditions => [%q("posts".permalink != ?), 'Post'] }
140
+ Post.last_find.should == { :conditions => [%q("posts".permalink <> ?), 'Post'] }
141
141
  end
142
142
  end
@@ -0,0 +1,58 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe 'with custom selects for cases where DISTINCT is required' do
4
+ before do
5
+ TestModel.extended_models.each { |model| model.last_find = {} }
6
+ end
7
+
8
+ describe 'on a standard filter' do
9
+ it 'should put nothing in the select' do
10
+ Post.filter do
11
+ having(:comments).with(:offensive, true)
12
+ end.inspect
13
+ Post.last_find[:select].should be_nil
14
+ end
15
+ end
16
+
17
+ describe 'with join types that require distinct' do
18
+ it 'should put the distinct clause in the select' do
19
+ [:left, :outer, :left_outer].each do |join_type|
20
+ Post.filter do
21
+ having(join_type, :comments).with(:offensive, true)
22
+ end.inspect
23
+ Post.last_find[:select].should == %q(DISTINCT "posts".*)
24
+ end
25
+ end
26
+ end
27
+
28
+ describe 'with join types that do not require distinct' do
29
+ it 'should not put the distinct clause in the select' do
30
+ [:inner].each do |join_type|
31
+ Post.filter do
32
+ having(join_type, :comments).with(:offensive, true)
33
+ end.inspect
34
+ Post.last_find[:select].should be_nil
35
+ end
36
+ end
37
+ end
38
+
39
+ describe 'on a filter with nested joins that require distinct' do
40
+ it 'should put the distinct clause in the select' do
41
+ Blog.filter do
42
+ having(:posts) do
43
+ having(:left_outer, :comments).with(:offensive, true)
44
+ end
45
+ end.inspect
46
+ Blog.last_find[:select].should == %q(DISTINCT "blogs".*)
47
+ end
48
+ end
49
+
50
+ describe 'on a filter that requires distinct with a count call' do
51
+ it 'should put the distinct clause in the select' do
52
+ Post.filter do
53
+ having(:left_outer, :comments).with(:offensive, true)
54
+ end.count
55
+ Post.last_find[:select].should == %q(DISTINCT "posts".id)
56
+ end
57
+ end
58
+ end
data/spec/spec_helper.rb CHANGED
@@ -13,8 +13,14 @@ module TestModel
13
13
 
14
14
  attr_accessor :last_find
15
15
 
16
- def scoped(params = {})
17
- @last_find = params
16
+ def find(*args)
17
+ @last_find = current_scoped_methods[:find] if current_scoped_methods
18
+ super
19
+ end
20
+
21
+ def count(*args)
22
+ @last_find = current_scoped_methods[:find] if current_scoped_methods
23
+ super
18
24
  end
19
25
 
20
26
  def self.extended(base)
@@ -22,7 +28,7 @@ module TestModel
22
28
  end
23
29
  end
24
30
 
25
- Dir.glob(File.join(File.dirname(__FILE__), 'models', '*.rb')).each { |file| require file }
31
+ require File.join(File.dirname(__FILE__), 'models')
26
32
 
27
33
  ActiveRecord::Base.establish_connection(
28
34
  :adapter => 'sqlite3',
data/spec/test.db CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aub-record_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mat Brown
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-04-22 00:00:00 -07:00
13
+ date: 2009-04-27 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -52,15 +52,11 @@ files:
52
52
  - spec/explicit_join_spec.rb
53
53
  - spec/implicit_join_spec.rb
54
54
  - spec/limits_and_ordering_spec.rb
55
- - spec/models/blog.rb
56
- - spec/models/comment.rb
57
- - spec/models/feature.rb
58
- - spec/models/photo.rb
59
- - spec/models/post.rb
60
- - spec/models/review.rb
61
- - spec/models/tag.rb
55
+ - spec/models.rb
62
56
  - spec/named_filter_spec.rb
57
+ - spec/proxying_spec.rb
63
58
  - spec/restrictions_spec.rb
59
+ - spec/select_spec.rb
64
60
  - spec/spec_helper.rb
65
61
  - spec/test.db
66
62
  has_rdoc: true
@@ -94,13 +90,9 @@ test_files:
94
90
  - spec/explicit_join_spec.rb
95
91
  - spec/implicit_join_spec.rb
96
92
  - spec/limits_and_ordering_spec.rb
97
- - spec/models/blog.rb
98
- - spec/models/comment.rb
99
- - spec/models/feature.rb
100
- - spec/models/photo.rb
101
- - spec/models/post.rb
102
- - spec/models/review.rb
103
- - spec/models/tag.rb
93
+ - spec/models.rb
104
94
  - spec/named_filter_spec.rb
95
+ - spec/proxying_spec.rb
105
96
  - spec/restrictions_spec.rb
97
+ - spec/select_spec.rb
106
98
  - spec/spec_helper.rb
data/spec/models/blog.rb DELETED
@@ -1,5 +0,0 @@
1
- class Blog < ActiveRecord::Base
2
- extend TestModel
3
-
4
- has_many :posts
5
- end
@@ -1,5 +0,0 @@
1
- class Comment < ActiveRecord::Base
2
- extend TestModel
3
-
4
- belongs_to :post
5
- end
@@ -1,5 +0,0 @@
1
- class Feature < ActiveRecord::Base
2
- extend TestModel
3
-
4
- belongs_to :featurable, :polymorphic => true
5
- end
data/spec/models/photo.rb DELETED
@@ -1,3 +0,0 @@
1
- class Photo < ActiveRecord::Base
2
- belongs_to :post
3
- end
data/spec/models/post.rb DELETED
@@ -1,10 +0,0 @@
1
- class Post < ActiveRecord::Base
2
- extend TestModel
3
-
4
- belongs_to :blog
5
- has_many :comments
6
- has_one :photo
7
- has_and_belongs_to_many :tags
8
- has_many :features, :as => :featurable
9
- has_many :reviews, :as => :reviewable
10
- end
@@ -1,5 +0,0 @@
1
- class Review < ActiveRecord::Base
2
- extend TestModel
3
-
4
- belongs_to :reviewable, :polymorphic => true
5
- end
data/spec/models/tag.rb DELETED
@@ -1,3 +0,0 @@
1
- class Tag < ActiveRecord::Base
2
- has_and_belongs_to_many :posts
3
- end