aub-record_filter 0.2.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,205 @@
1
+ = record_filter
2
+
3
+ record_filter is a DSL for specifying criteria for ActiveRecord queries in pure Ruby.
4
+ It has support for filters created on the fly and for named filters that are associated with object types.
5
+ record_filter has the following top-level features:
6
+
7
+ * Pure ruby API eliminates the need for hard-coded SQL in most cases.
8
+ * Works seamlessly with existing ActiveRecord APIs, including named scopes.
9
+ * Supports creation of ad-hoc filters as well as named filters that can be associated with object types.
10
+ * Allows chaining of filters with each other and with named scopes to create complex queries.
11
+ * Takes advantage of the associations in your ActiveRecord objects for a clean implicit join API.
12
+
13
+ == Installation
14
+
15
+ gem install outoftime-record_filter --source=http://gems.github.com
16
+
17
+ == Usage
18
+
19
+ === Ad-hoc filters
20
+
21
+ Post.filter do
22
+ with(:permalink, 'blog-post')
23
+ having(:blog).with(:name, 'Blog')
24
+ end
25
+
26
+ This could be expressed in ActiveRecord as:
27
+
28
+ Post.find(:all, :joins => :blog, :conditions => ['posts.permalink = ? AND blogs.name = ?', 'blog-post', 'Blog')
29
+
30
+ and it returns the same result, a list of Post objects that are returned from the query.
31
+
32
+ === Named filters
33
+
34
+ class Post < ActiveRecord::Base
35
+ named_filter(:with_title) do |title|
36
+ with(:title, title)
37
+ end
38
+ end
39
+
40
+ Post.with_title('posted')
41
+
42
+ This is the same as the following code using named scopes, and returns the same result:
43
+
44
+ class Post < ActiveRecord::Base
45
+ named_scope :with_title, lambda { |title| { :conditions => ['title = ?', title] }}
46
+ end
47
+
48
+ Post.with_title('scoped')
49
+
50
+ === Restrictions
51
+
52
+ Restrictions are specified through the API using the 'with' function. The first argument to 'with' should be the
53
+ name of the field that the restriction applies to. All restriction types can be negated by chaining the 'with'
54
+ method with a call to 'not', as seen in some examples below.
55
+
56
+ ==== Equality
57
+
58
+ If a second argument is supplied, it is assumed that you are
59
+ expressing an equality condition and that argument is used as the value.
60
+
61
+ with(:title, 'abc') # :conditions => ['title = ?', 'abc']
62
+
63
+ Which can be negated with:
64
+
65
+ with(:title, 'abc').not # :conditions => ['title <> ?', 'abc']
66
+
67
+ For the more verbose among us, this can also be specified as:
68
+
69
+ with(:title).equal_to('abc') # :conditions => ['title = ?', 'abc']
70
+
71
+ Other types of restrictions are specified by omitting the second argument to 'with' and chaining it with one of the
72
+ restriction methods.
73
+
74
+ ==== Comparison operators
75
+
76
+ with(:price).greater_than(10) # :conditions => ['price > ?', 10]
77
+
78
+ with(:created_at).less_than(2.days.ago) # :conditions => ['created_at < ?', 2.days.ago]
79
+
80
+ These methods can also have _or_equal_to tagged onto the end, to obvious affect, and all of the comparison operators are aliased to
81
+ their standard single-character variants:
82
+
83
+ gt, gte, lt and lte
84
+
85
+ ==== IS NULL
86
+
87
+ with(:price, nil) # :conditions => ['price IS NULL']
88
+
89
+ This short form can also be made explicit by using the is_null, null, or nil functions on with:
90
+
91
+ with(:price).is_null # :conditions => ['price IS NULL']
92
+
93
+ It can be negated either by chaining with the 'not' function or by using is_not_null:
94
+
95
+ with(:price).is_not_null # :conditions => ['price IS NOT NULL']
96
+ with(:price).is_null.not # "
97
+
98
+ ==== IN
99
+
100
+ with(:id).in([1, 2, 3]) # :conditions => ['id IN (?)', [1, 2, 3]]
101
+
102
+ ==== BETWEEN
103
+
104
+ with(:id).between(1, 5) # :conditions => ['id BETWEEN ? AND ?', 1, 5]
105
+
106
+ The argument to between can also be either a tuple or a range
107
+
108
+ with(:created_at).between([Time.now, 3.days.ago]) # :conditions => ['created_at BETWEEN ? AND ?', Time.now, 3.days.ago]
109
+
110
+ with(:price).between(1..5) # :conditions => ['price BETWEEN ? AND ?', 1, 5]
111
+
112
+ ==== LIKE
113
+
114
+ with(:title).like('%help%') # :conditions => ['title LIKE ?', '%help%']
115
+
116
+
117
+ === Implicit Joins
118
+
119
+ Implicit joins are specified using the 'having' function, which takes as its argument the name of an association to join on.
120
+
121
+ having(:comments) # :joins => :comments
122
+
123
+ This function can be chained with calls to 'with' in order to specify conditions on the joined table:
124
+
125
+ having(:comments).with(:created_at).gt(2.days.ago) # :joins => :comments, :conditions => ['comments.created_at > ?', 2.days.ago]
126
+
127
+ It can also take a block that can have any number of conditions or other clauses (including other joins) in it:
128
+
129
+ having(:comments) do
130
+ with(:created_at).gt(2.days.ago)
131
+ having(:author).with(:name, 'Bubba')
132
+ end
133
+
134
+ The 'having' function can also take :inner, :left or :right as its second argument in order to specify that a particular join type
135
+ should be used.
136
+
137
+ === Explicit joins
138
+
139
+ In cases where there is no ActiveRecord association that can be used to specify an implicit join, explicit joins are also
140
+ supported, using the 'join' function. Its arguments are the class to be joined against, the join type (:inner, left or :right) and
141
+ an optional alias for the join table. A block should also be supplied in order to specify the columns to use for the join using the
142
+ 'on' method.
143
+
144
+ Post.filter do
145
+ join(Comment, :inner, :posts__comments_alias) do
146
+ on(:id => :commentable_id)
147
+ on(:commentable_type, 'Post')
148
+ end
149
+ end
150
+
151
+ === Conjunctions
152
+
153
+ The following conjunction types are supported:
154
+
155
+ * any_of
156
+ * all_of
157
+ * none_of
158
+ * not_all_of
159
+
160
+ Each takes a block that can contain conditions or joins and will apply the expected boolean logic to the combination.
161
+
162
+ any_of
163
+ with(:price, 2)
164
+ with(:price).gt(100)
165
+ end
166
+
167
+ # :conditions => ['price = ? OR price > ?', 2, 100]
168
+
169
+ === Limits and ordering
170
+
171
+ limit(20)
172
+
173
+ order(:id, :desc)
174
+
175
+ Multiple order clauses can be specified, and they will be applied in the order in which they are specified.
176
+ When joins are used, order can also take a hash as the first argument that leads to the column to order on through the joins:
177
+
178
+ having(:comments).with(:offensive, true)
179
+ order(:comments => :id, :desc)
180
+
181
+
182
+ == LICENSE:
183
+
184
+ (The MIT License)
185
+
186
+ Copyright (c) 2008 Mat Brown
187
+
188
+ Permission is hereby granted, free of charge, to any person obtaining
189
+ a copy of this software and associated documentation files (the
190
+ 'Software'), to deal in the Software without restriction, including
191
+ without limitation the rights to use, copy, modify, merge, publish,
192
+ distribute, sublicense, and/or sell copies of the Software, and to
193
+ permit persons to whom the Software is furnished to do so, subject to
194
+ the following conditions:
195
+
196
+ The above copyright notice and this permission notice shall be
197
+ included in all copies or substantial portions of the Software.
198
+
199
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
200
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
201
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
202
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
203
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
204
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
205
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  :patch: 0
3
3
  :major: 0
4
- :minor: 2
4
+ :minor: 6
data/lib/record_filter.rb CHANGED
@@ -7,7 +7,9 @@ require 'active_record'
7
7
  end
8
8
 
9
9
  module RecordFilter
10
- class AssociationNotFoundException < StandardError; end
11
- class ColumnNotFoundException < StandardError; end
12
- class InvalidJoinException < StandardError; end
10
+ AssociationNotFoundException = Class.new(StandardError)
11
+ ColumnNotFoundException = Class.new(StandardError)
12
+ InvalidFilterException = Class.new(StandardError)
13
+ InvalidJoinException = Class.new(StandardError)
14
+ NamedFilterNotFoundException = Class.new(StandardError)
13
15
  end
@@ -1,14 +1,33 @@
1
1
  module RecordFilter
2
+ # The ActiveRecordExtension module is mixed in to ActiveRecord::Base to form the
3
+ # top-level API for interacting with record_filter. It adds public methods for
4
+ # executing ad-hoc filters as well as for creating and querying named filters.
2
5
  module ActiveRecordExtension
3
6
  module ClassMethods
4
7
 
8
+ # Execute an ad-hoc filter
9
+ #
10
+ # ==== Parameters
11
+ # block::
12
+ # A block that specifies the contents of the filter.
13
+ #
14
+ # ==== Returns
15
+ # Filter:: The Filter object resulting from the query, which can be
16
+ # treated as an array of the results.
17
+ #
18
+ # ==== Example
19
+ # Blog.filter do
20
+ # having(:posts).with(:name, nil)
21
+ # end
22
+ #—
23
+ # @public
5
24
  def filter(&block)
6
25
  Filter.new(self, nil, &block)
7
26
  end
8
27
 
9
28
  def named_filter(name, &block)
10
29
  return if named_filters.include?(name.to_sym)
11
- named_filters << name.to_sym
30
+ local_named_filters << name.to_sym
12
31
  DSL::DSL::subclass(self).module_eval do
13
32
  define_method(name, &block)
14
33
  end
@@ -21,10 +40,19 @@ module RecordFilter
21
40
  end
22
41
 
23
42
  def named_filters
24
- read_inheritable_attribute(:named_filters) || write_inheritable_attribute(:named_filters, [])
43
+ result = local_named_filters.dup
44
+ result.concat(superclass.named_filters) if (superclass && superclass.respond_to?(:named_filters))
45
+ result
46
+ end
47
+
48
+ protected
49
+
50
+ def local_named_filters
51
+ @local_named_filters ||= []
25
52
  end
26
53
  end
27
54
  end
28
55
  end
29
56
 
30
57
  ActiveRecord::Base.send(:extend, RecordFilter::ActiveRecordExtension::ClassMethods)
58
+
@@ -30,6 +30,8 @@ module RecordFilter
30
30
  result.add_order(step.column, step.direction)
31
31
  when DSL::GroupBy
32
32
  result.add_group_by(step.column)
33
+ when DSL::NamedFilter
34
+ result.add_named_filter(step.name, step.args)
33
35
  end
34
36
  end
35
37
  result
@@ -81,6 +83,14 @@ module RecordFilter
81
83
  @limit, @offset = limit, offset
82
84
  end
83
85
 
86
+ def add_named_filter(name, args)
87
+ unless @table.model_class.named_filters.include?(name.to_sym)
88
+ raise NamedFilterNotFoundException.new("The named filter #{name} was not found in #{@table.model_class}")
89
+ end
90
+ query = Query.new(@table.model_class, name, *args)
91
+ self << self.class.create_from(query.dsl_conjunction, @table)
92
+ end
93
+
84
94
  def <<(restriction)
85
95
  @restrictions << restriction
86
96
  end
@@ -1,4 +1,4 @@
1
- %w(class_join conjunction conjunction_dsl dsl group_by join join_dsl join_condition limit order restriction).each { |file| require File.join(File.dirname(__FILE__), 'dsl', file) }
1
+ %w(class_join conjunction conjunction_dsl dsl group_by join join_dsl join_condition limit named_filter order restriction).each { |file| require File.join(File.dirname(__FILE__), 'dsl', file) }
2
2
 
3
3
  module RecordFilter
4
4
  module DSL
@@ -44,6 +44,10 @@ module RecordFilter
44
44
  def add_group_by(column)
45
45
  @steps << GroupBy.new(column)
46
46
  end
47
+
48
+ def add_named_filter(method, *args)
49
+ @steps << NamedFilter.new(method, *args)
50
+ end
47
51
  end
48
52
  end
49
53
  end
@@ -53,6 +53,26 @@ module RecordFilter
53
53
  def filter_class
54
54
  @model_class
55
55
  end
56
+
57
+ def limit(offset_or_limit, limit=nil)
58
+ raise InvalidFilterException.new('Calls to limit can only be made in the outer block of a filter.')
59
+ end
60
+
61
+ def order(column, direction=:asc)
62
+ raise InvalidFilterException.new('Calls to order can only be made in the outer block of a filter.')
63
+ end
64
+
65
+ def group_by(column)
66
+ raise InvalidFilterException.new('Calls to group_by can only be made in the outer block of a filter.')
67
+ end
68
+
69
+ def on(column, value=Restriction::DEFAULT_VALUE)
70
+ raise InvalidFilterException.new('Calls to on can only be made in the block of a call to join.')
71
+ end
72
+
73
+ def method_missing(method, *args)
74
+ @conjunction.add_named_filter(method, *args)
75
+ end
56
76
  end
57
77
  end
58
78
  end
@@ -12,7 +12,7 @@ module RecordFilter
12
12
  end
13
13
 
14
14
  def subclass(clazz)
15
- SUBCLASSES[clazz.name.to_sym]
15
+ SUBCLASSES[clazz.object_id]
16
16
  end
17
17
  end
18
18
 
@@ -0,0 +1,12 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class NamedFilter
4
+
5
+ attr_reader :name, :args
6
+
7
+ def initialize(name, *args)
8
+ @name, @args = name, args
9
+ end
10
+ end
11
+ end
12
+ end
@@ -35,6 +35,11 @@ module RecordFilter
35
35
  end
36
36
  end
37
37
 
38
+ def is_not_null
39
+ @operator = :is_null
40
+ @negated = true
41
+ end
42
+
38
43
  alias_method :gt, :greater_than
39
44
  alias_method :gte, :greater_than_or_equal_to
40
45
  alias_method :lt, :less_than
@@ -12,10 +12,7 @@ module RecordFilter
12
12
  @current_scoped_methods = clazz.send(:current_scoped_methods)
13
13
  @clazz = clazz
14
14
 
15
- @dsl = dsl_for_named_filter(@clazz, named_filter)
16
- @dsl.instance_eval(&block) if block
17
- @dsl.send(named_filter, *args) if named_filter && @dsl.respond_to?(named_filter)
18
- @query = Query.new(@clazz, @dsl.conjunction)
15
+ @query = Query.new(@clazz, named_filter, *args, &block)
19
16
  end
20
17
 
21
18
  def first(*args)
@@ -90,16 +87,6 @@ module RecordFilter
90
87
  end
91
88
  end
92
89
 
93
- def dsl_for_named_filter(clazz, named_filter)
94
- return DSL::DSL.create(clazz) if named_filter.blank?
95
- while (clazz)
96
- dsl = DSL::DSL::SUBCLASSES.has_key?(clazz.name.to_sym) ? DSL::DSL::SUBCLASSES[clazz.name.to_sym] : nil
97
- return DSL::DSL.create(clazz) if dsl && dsl.instance_methods(false).include?(named_filter.to_s)
98
- clazz = clazz.superclass
99
- end
100
- nil
101
- end
102
-
103
90
  def loaded_data
104
91
  @loaded_data ||= do_with_scope do
105
92
  @clazz.find(:all)
@@ -22,7 +22,7 @@ module RecordFilter
22
22
  end
23
23
 
24
24
  def requires_distinct_select?
25
- [:left, :outer, :left_outer].include?(@join_type)
25
+ [:right, :left].include?(@join_type)
26
26
  end
27
27
 
28
28
  protected
@@ -55,9 +55,8 @@ module RecordFilter
55
55
  def join_type_string
56
56
  @join_type_string ||= case(@join_type)
57
57
  when :inner then 'INNER'
58
- when :left then 'LEFT'
59
- when :left_outer then 'LEFT OUTER'
60
- when :outer then 'OUTER'
58
+ when :left then 'LEFT OUTER'
59
+ when :right then 'RIGHT OUTER'
61
60
  else nil
62
61
  end
63
62
  end
@@ -1,31 +1,54 @@
1
1
  module RecordFilter
2
2
  class Query
3
3
 
4
- def initialize(clazz, dsl_conjunction)
4
+ attr_reader :dsl_conjunction
5
+ attr_reader :conjunction
6
+
7
+ def initialize(clazz, named_filter, *args, &block)
8
+ dsl = dsl_for_named_filter(clazz, named_filter)
9
+ dsl.instance_eval(&block) if block
10
+ dsl.send(named_filter, *args) if named_filter && dsl.respond_to?(named_filter)
11
+
5
12
  @table = RecordFilter::Table.new(clazz)
6
- @conjunction = RecordFilter::Conjunctions::Base.create_from(dsl_conjunction, @table)
13
+ @dsl_conjunction = dsl.conjunction
14
+ @conjunction = RecordFilter::Conjunctions::Base.create_from(@dsl_conjunction, @table)
7
15
  end
8
16
 
9
17
  def to_find_params(count_query=false)
10
- params = {}
11
- conditions = @conjunction.to_conditions
12
- params = { :conditions => conditions } if conditions
13
- joins = @table.all_joins
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}.*"
18
+ @params_cache ||= {}
19
+ @params_cache[count_query] ||= begin
20
+ params = {}
21
+ conditions = @conjunction.to_conditions
22
+ params = { :conditions => conditions } if conditions
23
+ joins = @table.all_joins
24
+ params[:joins] = joins.map { |join| join.to_sql } * ' ' unless joins.empty?
25
+ if (joins.any? { |j| j.requires_distinct_select? })
26
+ if count_query
27
+ params[:select] = "DISTINCT #{@table.model_class.quoted_table_name}.#{@table.model_class.primary_key}"
28
+ else
29
+ params[:select] = "DISTINCT #{@table.model_class.quoted_table_name}.*"
30
+ end
20
31
  end
32
+ orders = @table.orders
33
+ params[:order] = orders.map { |order| order.to_sql } * ', ' unless orders.empty?
34
+ group_bys = @table.group_bys
35
+ params[:group] = group_bys.map { |group_by| group_by.to_sql } * ', ' unless group_bys.empty?
36
+ params[:limit] = @conjunction.limit if @conjunction.limit
37
+ params[:offset] = @conjunction.offset if @conjunction.offset
38
+ params
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def dsl_for_named_filter(clazz, named_filter)
45
+ return DSL::DSL.create(clazz) if named_filter.blank?
46
+ while (clazz)
47
+ dsl = DSL::DSL.subclass(clazz)
48
+ return DSL::DSL.create(clazz) if dsl && dsl.instance_methods(false).include?(named_filter.to_s)
49
+ clazz = clazz.superclass
21
50
  end
22
- orders = @table.orders
23
- params[:order] = orders.map { |order| order.to_sql } * ', ' unless orders.empty?
24
- group_bys = @table.group_bys
25
- params[:group] = group_bys.map { |group_by| group_by.to_sql } * ', ' unless group_bys.empty?
26
- params[:limit] = @conjunction.limit if @conjunction.limit
27
- params[:offset] = @conjunction.offset if @conjunction.offset
28
- params
51
+ nil
29
52
  end
30
53
  end
31
54
  end
@@ -75,6 +75,11 @@ module RecordFilter
75
75
  def to_negative_sql
76
76
  "#{@column_name} NOT IN (?)"
77
77
  end
78
+
79
+ def to_conditions
80
+ # Need to put in the value even if it's null in this case.
81
+ [to_sql, @value]
82
+ end
78
83
  end
79
84
 
80
85
  class Between < Base
@@ -102,4 +102,66 @@ describe 'raising exceptions' do
102
102
  }.should raise_error(RecordFilter::InvalidJoinException)
103
103
  end
104
104
  end
105
+
106
+ describe 'limiting methods within joins and conjunctions' do
107
+ it 'should not allow calls to limit within joins' do
108
+ lambda {
109
+ Post.filter do
110
+ having(:photo) do
111
+ limit 2
112
+ end
113
+ end
114
+ }.should raise_error(RecordFilter::InvalidFilterException)
115
+ end
116
+
117
+ it 'should not allow calls to order within joins' do
118
+ lambda {
119
+ Post.filter do
120
+ having(:photo) do
121
+ order :id
122
+ end
123
+ end
124
+ }.should raise_error(RecordFilter::InvalidFilterException)
125
+ end
126
+
127
+ it 'should not allow calls to limit within conjunctions' do
128
+ lambda {
129
+ Post.filter do
130
+ all_of do
131
+ limit 2
132
+ end
133
+ end
134
+ }.should raise_error(RecordFilter::InvalidFilterException)
135
+ end
136
+
137
+ it 'should not allow calls to order within joins' do
138
+ lambda {
139
+ Post.filter do
140
+ all_of do
141
+ order :id
142
+ end
143
+ end
144
+ }.should raise_error(RecordFilter::InvalidFilterException)
145
+ end
146
+ end
147
+
148
+ describe 'limiting calls to on' do
149
+ it 'should not allow calls to on in the outer scope' do
150
+ lambda {
151
+ Post.filter do
152
+ on(:a => :b)
153
+ end
154
+ }.should raise_error(RecordFilter::InvalidFilterException)
155
+ end
156
+ end
157
+
158
+ describe 'calling named filters within filters' do
159
+ it 'should raise an excpetion if the named filter does not exist' do
160
+ lambda {
161
+ Post.filter do
162
+ having(:comments).does_not_exist
163
+ end
164
+ }.should raise_error(RecordFilter::NamedFilterNotFoundException)
165
+ end
166
+ end
105
167
  end
@@ -16,7 +16,7 @@ describe 'explicit joins' do
16
16
  end
17
17
 
18
18
  it 'should add correct join' do
19
- Post.last_find[:joins].should == %q(LEFT JOIN "blogs" AS posts_blogs ON "posts".blog_id = posts_blogs.id)
19
+ Post.last_find[:joins].should == %q(LEFT OUTER JOIN "blogs" AS posts_blogs ON "posts".blog_id = posts_blogs.id)
20
20
  end
21
21
 
22
22
  it 'should query against condition on join table' do
@@ -36,7 +36,7 @@ describe 'explicit joins' do
36
36
  end
37
37
 
38
38
  it 'should add correct join' do
39
- Review.last_find[:joins].should == %q(LEFT JOIN "features" AS reviews_features ON "reviews".reviewable_id = reviews_features.featurable_id AND "reviews".reviewable_type = reviews_features.featurable_type)
39
+ Review.last_find[:joins].should == %q(LEFT OUTER JOIN "features" AS reviews_features ON "reviews".reviewable_id = reviews_features.featurable_id AND "reviews".reviewable_type = reviews_features.featurable_type)
40
40
  end
41
41
 
42
42
  it 'should query against condition on join table' do
@@ -57,7 +57,7 @@ describe 'explicit joins' do
57
57
  end
58
58
 
59
59
  it 'should add correct join' do
60
- Review.last_find[:joins].should == %q(LEFT JOIN "features" AS reviews__Feature ON "reviews".reviewable_id = reviews__Feature.featurable_id AND "reviews".reviewable_type = reviews__Feature.featurable_type AND (reviews__Feature.featurable_type = 'SomeType'))
60
+ Review.last_find[:joins].should == %q(LEFT OUTER JOIN "features" AS reviews__Feature ON "reviews".reviewable_id = reviews__Feature.featurable_id AND "reviews".reviewable_type = reviews__Feature.featurable_type AND (reviews__Feature.featurable_type = 'SomeType'))
61
61
  end
62
62
  end
63
63
 
@@ -73,7 +73,7 @@ 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 OUTER JOIN "features" AS reviews__Feature ON (reviews__Feature.featurable_type IS NULL) AND (reviews__Feature.featurable_id >= 12) AND (reviews__Feature.priority <> 6))
77
77
  end
78
78
  end
79
79
 
@@ -100,7 +100,7 @@ describe 'explicit joins' do
100
100
  end
101
101
 
102
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'))
103
+ Blog.last_find[:joins].should == %q(INNER JOIN "ads" AS blogs__ads ON "blogs".id = blogs__ads.blog_id LEFT OUTER 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'))
104
104
  end
105
105
  end
106
106
  end
@@ -224,7 +224,7 @@ describe 'implicit joins' do
224
224
  describe 'passing the join type to having' do
225
225
  before do
226
226
  Blog.filter do
227
- having(:left_outer, :posts) do
227
+ having(:left, :posts) do
228
228
  with(:permalink, 'ack')
229
229
  end
230
230
  end.inspect
@@ -242,7 +242,7 @@ describe 'implicit joins' do
242
242
  describe 'passing the join type to having with multiple joins' do
243
243
  before do
244
244
  Blog.filter do
245
- having(:left_outer, :posts => :comments) do
245
+ having(:left, :posts => :comments) do
246
246
  with(:offensive, true)
247
247
  end
248
248
  end.inspect
@@ -118,48 +118,6 @@ describe 'filter qualifiers' do
118
118
  Post.last_find[:order].should == %q(posts__photo.path DESC, "posts".permalink ASC)
119
119
  end
120
120
  end
121
-
122
- describe 'limiting methods within joins and conjunctions' do
123
- it 'should not allow calls to limit within joins' do
124
- lambda {
125
- Post.filter do
126
- having(:photo) do
127
- limit 2
128
- end
129
- end
130
- }.should raise_error(NoMethodError)
131
- end
132
-
133
- it 'should not allow calls to order within joins' do
134
- lambda {
135
- Post.filter do
136
- having(:photo) do
137
- order :id
138
- end
139
- end
140
- }.should raise_error(NoMethodError)
141
- end
142
-
143
- it 'should not allow calls to limit within conjunctions' do
144
- lambda {
145
- Post.filter do
146
- all_of do
147
- limit 2
148
- end
149
- end
150
- }.should raise_error(NoMethodError)
151
- end
152
-
153
- it 'should not allow calls to order within joins' do
154
- lambda {
155
- Post.filter do
156
- all_of do
157
- order :id
158
- end
159
- end
160
- }.should raise_error(NoMethodError)
161
- end
162
- end
163
121
  end
164
122
 
165
123
  describe 'group_by' do
@@ -7,40 +7,43 @@ describe 'named filters' do
7
7
 
8
8
  describe 'defining a simple filter' do
9
9
  before do
10
- Blog.named_filter(:with_test_name) do
10
+ @blog = Class.new(Blog)
11
+ @blog.named_filter(:with_test_name) do
11
12
  with :name, 'Test Name'
12
13
  end
13
14
  end
14
15
 
15
16
  it 'should call the filter through the filter method' do
16
- Blog.with_test_name.inspect
17
- Blog.last_find[:conditions].should == [%q("blogs".name = ?), 'Test Name']
17
+ @blog.with_test_name.inspect
18
+ @blog.last_find[:conditions].should == [%q("blogs".name = ?), 'Test Name']
18
19
  end
19
20
 
20
21
  it 'should call the filter within the block' do
21
- Blog.filter do
22
+ @blog.filter do
22
23
  with_test_name
23
24
  end.inspect
24
- Blog.last_find[:conditions].should == [%q("blogs".name = ?), 'Test Name']
25
+ @blog.last_find[:conditions].should == [%q("blogs".name = ?), 'Test Name']
25
26
  end
26
27
  end
27
28
 
28
29
  describe 'defining a filter with arguments' do
29
30
  before do
30
- Blog.named_filter(:with_name) do |name|
31
+ @blog = Class.new(Blog)
32
+ @blog.named_filter(:with_name) do |name|
31
33
  with :name, name
32
34
  end
33
35
  end
34
36
 
35
37
  it 'should call the filter with the passed argument' do
36
- Blog.with_name('nice name').inspect
37
- Blog.last_find[:conditions].should == [%q("blogs".name = ?), 'nice name']
38
+ @blog.with_name('nice name').inspect
39
+ @blog.last_find[:conditions].should == [%q("blogs".name = ?), 'nice name']
38
40
  end
39
41
  end
40
42
 
41
43
  describe 'defining a filter that passes arguments down several levels' do
42
44
  before do
43
- Blog.named_filter(:with_name_and_post_with_permalink) do |name, permalink|
45
+ @blog = Class.new(Blog)
46
+ @blog.named_filter(:with_name_and_post_with_permalink) do |name, permalink|
44
47
  with :name, name
45
48
  having :posts do
46
49
  with :permalink, permalink
@@ -49,29 +52,31 @@ describe 'named filters' do
49
52
  end
50
53
 
51
54
  it 'should call the filter passing all of the arguments' do
52
- Blog.with_name_and_post_with_permalink('booya', 'ftw').inspect
53
- Blog.last_find[:conditions].should ==
55
+ @blog.with_name_and_post_with_permalink('booya', 'ftw').inspect
56
+ @blog.last_find[:conditions].should ==
54
57
  [%q(("blogs".name = ?) AND (blogs__posts.permalink = ?)), 'booya', 'ftw']
55
58
  end
56
59
  end
57
60
 
58
61
  describe 'taking active_record objects as arguments' do
59
62
  it 'should use the id of the object as the actual parameter' do
60
- Post.named_filter(:with_ar_arg) do |blog|
63
+ post = Class.new(Post)
64
+ post.named_filter(:with_ar_arg) do |blog|
61
65
  with(:blog_id, blog)
62
66
  end
63
67
  blog = Blog.create
64
- Post.with_ar_arg(blog).inspect
65
- Post.last_find[:conditions].should == [%q("posts".blog_id = ?), blog.id]
68
+ post.with_ar_arg(blog).inspect
69
+ post.last_find[:conditions].should == [%q("posts".blog_id = ?), blog.id]
66
70
  end
67
71
  end
68
72
 
69
73
  describe 'using filters in subclasses' do
70
74
  before do
71
- Comment.named_filter(:with_contents) do |*args|
75
+ @comment = Class.new(Comment)
76
+ @comment.named_filter(:with_contents) do |*args|
72
77
  with :contents, args[0]
73
78
  end
74
- class NiceComment < Comment
79
+ @nice_comment = Class.new(@comment) do
75
80
  extend TestModel
76
81
 
77
82
  named_filter(:offensive) do
@@ -81,95 +86,110 @@ describe 'named filters' do
81
86
  end
82
87
 
83
88
  it 'should execute the parent class filters correctly' do
84
- NiceComment.with_contents('test contents').inspect
85
- NiceComment.last_find[:conditions].should ==
89
+ @nice_comment.with_contents('test contents').inspect
90
+ @nice_comment.last_find[:conditions].should ==
86
91
  [%q("comments".contents = ?), 'test contents']
87
92
  end
88
93
 
89
94
  it 'should not have the subclass filters in the parent class' do
90
- Comment.respond_to?(:offensive).should == false
95
+ @comment.respond_to?(:offensive).should == false
91
96
  end
92
97
 
93
98
  it 'should have parent class filters in the subclass' do
94
- NiceComment.offensive.with_contents('something').inspect
95
- NiceComment.last_find[:conditions].should ==
99
+ @nice_comment.offensive.with_contents('something').inspect
100
+ @nice_comment.last_find[:conditions].should ==
96
101
  %q(("comments".contents = 'something') AND ("comments".offensive = 't'))
97
102
  end
98
103
 
99
104
  it 'should provide access to the named filters' do
100
- Comment.named_filters.should == [:with_contents]
101
- NiceComment.named_filters.sort_by { |i| i.to_s }.should == [:offensive, :with_contents]
105
+ @comment.named_filters.should == [:with_contents]
106
+ @nice_comment.named_filters.sort_by { |i| i.to_s }.should == [:offensive, :with_contents]
102
107
  end
103
108
  end
104
109
 
105
110
  describe 'using compound filters' do
106
- it 'should concatenate the filters correctly' do
107
- pending 'nested chaining'
108
- Post.named_filter(:with_offensive_comments) do
109
- having(:comments).offensive(true)
111
+ before do
112
+ Comment.named_filter(:offensive_or_not) do |state|
113
+ with(:offensive, state)
110
114
  end
111
- Post.with_offensive_comments.inspect
115
+ end
116
+
117
+ it 'should concatenate the filters correctly' do
118
+ Post.filter do
119
+ having(:comments).offensive_or_not(true)
120
+ end.inspect
112
121
  Post.last_find[:conditions].should == [%q(posts__comments.offensive = ?), true]
113
- Post.last_find[:joins].should == %q(INNER JOIN "comments" AS posts__comments ON "comments".post_id = posts__blog.id)
122
+ Post.last_find[:joins].should == %q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
123
+ end
124
+
125
+ it 'should work correctly with the named filter called within the having block' do
126
+ Post.filter do
127
+ having(:comments) do
128
+ offensive_or_not(false)
129
+ end
130
+ end.inspect
131
+ Post.last_find[:conditions].should == [%q(posts__comments.offensive = ?), false]
132
+ Post.last_find[:joins].should == %q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
114
133
  end
115
134
  end
116
135
 
117
136
  describe 'chaining filters' do
118
137
  before do
119
- Post.named_filter(:for_blog) do |*args|
138
+ @post = Class.new(Post)
139
+ @post.named_filter(:for_blog) do |*args|
120
140
  having(:blog).with :id, args[0]
121
141
  end
122
- Post.named_filter(:with_offensive_comments) do
142
+ @post.named_filter(:with_offensive_comments) do
123
143
  having(:comments).with :offensive, true
124
144
  end
125
- Post.named_filter(:with_interesting_comments) do
145
+ @post.named_filter(:with_interesting_comments) do
126
146
  having(:comments).with :offensive, false
127
147
  end
128
148
  end
129
149
 
130
150
  it 'should chain the filters into a single query' do
131
- Post.for_blog(1).with_offensive_comments.inspect
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)]
151
+ @post.for_blog(1).with_offensive_comments.inspect
152
+ @post.last_find[:conditions].should == %q((posts__comments.offensive = 't') AND (posts__blog.id = 1))
153
+ @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)]
134
154
  end
135
155
 
136
156
  it 'should remove duplicate joins' do
137
- Post.for_blog(1).with_offensive_comments.with_interesting_comments.inspect
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)]
157
+ @post.for_blog(1).with_offensive_comments.with_interesting_comments.inspect
158
+ @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)]
139
159
  end
140
160
 
141
161
  it 'should allow for filtering a named_filter' do
142
- Post.for_blog(1).filter { having(:comments).with :offensive, true }.inspect
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)]
162
+ @post.for_blog(1).filter { having(:comments).with :offensive, true }.inspect
163
+ @post.last_find[:conditions].should == %q((posts__comments.offensive = 't') AND (posts__blog.id = 1))
164
+ @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)]
145
165
  end
146
166
 
147
167
  it 'should allow for applying a named filter to a filter' do
148
- Post.filter { having(:comments).with :offensive, false }.for_blog(1).inspect
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)]
168
+ @post.filter { having(:comments).with :offensive, false }.for_blog(1).inspect
169
+ @post.last_find[:conditions].should == %q((posts__blog.id = 1) AND (posts__comments.offensive = 'f'))
170
+ @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)]
151
171
  end
152
172
 
153
173
  it 'should not change the inner filter conditions when chaining filters' do
154
- base = Post.for_blog(1)
174
+ base = @post.for_blog(1)
155
175
  base.with_offensive_comments
156
176
  base.inspect
157
- Post.last_find[:conditions].should == [%q(posts__blog.id = ?), 1]
177
+ @post.last_find[:conditions].should == [%q(posts__blog.id = ?), 1]
158
178
  end
159
179
 
160
180
  it 'should not change the inner filter joins when chaining filters' do
161
- base = Post.for_blog(1)
181
+ base = @post.for_blog(1)
162
182
  base.with_offensive_comments
163
183
  base.inspect
164
- Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)
184
+ @post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)
165
185
  end
166
186
 
167
187
  it 'should not change an original filter when reusing it' do
168
- base = Post.for_blog(1)
188
+ base = @post.for_blog(1)
169
189
  level1 = base.with_offensive_comments.inspect
170
190
  level2 = base.with_interesting_comments
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)]
191
+ @post.last_find[:conditions].should == %q((posts__comments.offensive = 't') AND (posts__blog.id = 1))
192
+ @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
193
  end
174
194
  end
175
195
 
@@ -220,20 +240,22 @@ describe 'named filters' do
220
240
 
221
241
  describe 'chaining multiple named filters with different joins' do
222
242
  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) }
243
+ @blog = Class.new(Blog)
244
+ @blog.named_filter(:with_offensive_comments) { having(:comments).with(:offensive, true) }
245
+ @blog.named_filter(:with_ads_with_content) { |content| having(:ads).with(:content, content) }
225
246
  end
226
247
 
227
248
  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)]
249
+ @blog.with_offensive_comments.with_ads_with_content('ack').inspect
250
+ @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
251
  end
231
252
  end
232
253
 
233
254
  describe 'with named filters that only include orders' do
234
255
  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) }
256
+ blog = Class.new(Blog)
257
+ blog.named_filter(:ordered_by_id) { order(:id, :desc) }
258
+ blog.ordered_by_id.proxy_options.should == { :order => %q("blogs".id DESC) }
237
259
  end
238
260
  end
239
261
  end
@@ -42,6 +42,20 @@ describe 'RecordFilter restrictions' do
42
42
  Post.last_find.should == { :conditions => [%q{"posts".blog_id IN (?)}, [1, 3, 5]] }
43
43
  end
44
44
 
45
+ it 'should do the right thing for IN filters with empty arrays' do
46
+ Post.filter do
47
+ with(:blog_id).in([])
48
+ end.inspect
49
+ Post.last_find.should == { :conditions => [%q("posts".blog_id IN (?)), []] }
50
+ end
51
+
52
+ it 'should do the right thing for IN filters with nil' do
53
+ Post.filter do
54
+ with(:blog_id).in(nil)
55
+ end.inspect
56
+ Post.last_find.should == { :conditions => [%q("posts".blog_id IN (?)), nil] }
57
+ end
58
+
45
59
  it 'should filter for between' do
46
60
  time1 = Time.parse('2008-01-03 23:23:00')
47
61
  time2 = Time.parse('2008-01-03 23:36:00')
@@ -119,6 +133,13 @@ describe 'RecordFilter restrictions' do
119
133
  end
120
134
  end
121
135
 
136
+ it 'should filter for is_not_null' do
137
+ Post.filter do
138
+ with(:permalink).is_not_null
139
+ end.inspect
140
+ Post.last_find.should == { :conditions => [%q("posts".permalink IS NOT NULL)] }
141
+ end
142
+
122
143
  it 'should support like' do
123
144
  Post.filter do
124
145
  with(:permalink).like('%piglets%')
data/spec/select_spec.rb CHANGED
@@ -16,10 +16,10 @@ describe 'with custom selects for cases where DISTINCT is required' do
16
16
 
17
17
  describe 'with join types that require distinct' do
18
18
  it 'should put the distinct clause in the select' do
19
- [:left, :outer, :left_outer].each do |join_type|
19
+ [:left, :right].each do |join_type|
20
20
  Post.filter do
21
21
  having(join_type, :comments).with(:offensive, true)
22
- end.inspect
22
+ end.inspect rescue nil # required because sqlite doesn't support right joins
23
23
  Post.last_find[:select].should == %q(DISTINCT "posts".*)
24
24
  end
25
25
  end
@@ -40,7 +40,7 @@ describe 'with custom selects for cases where DISTINCT is required' do
40
40
  it 'should put the distinct clause in the select' do
41
41
  Blog.filter do
42
42
  having(:posts) do
43
- having(:left_outer, :comments).with(:offensive, true)
43
+ having(:left, :comments).with(:offensive, true)
44
44
  end
45
45
  end.inspect
46
46
  Blog.last_find[:select].should == %q(DISTINCT "blogs".*)
@@ -50,7 +50,7 @@ describe 'with custom selects for cases where DISTINCT is required' do
50
50
  describe 'on a filter that requires distinct with a count call' do
51
51
  it 'should put the distinct clause in the select' do
52
52
  Post.filter do
53
- having(:left_outer, :comments).with(:offensive, true)
53
+ having(:left, :comments).with(:offensive, true)
54
54
  end.count
55
55
  Post.last_find[:select].should == %q(DISTINCT "posts".id)
56
56
  end
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.2.0
4
+ version: 0.6.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-27 00:00:00 -07:00
13
+ date: 2009-05-01 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -20,9 +20,10 @@ executables: []
20
20
 
21
21
  extensions: []
22
22
 
23
- extra_rdoc_files: []
24
-
23
+ extra_rdoc_files:
24
+ - README.rdoc
25
25
  files:
26
+ - README.rdoc
26
27
  - Rakefile
27
28
  - VERSION.yml
28
29
  - lib/record_filter.rb
@@ -38,6 +39,7 @@ files:
38
39
  - lib/record_filter/dsl/join_condition.rb
39
40
  - lib/record_filter/dsl/join_dsl.rb
40
41
  - lib/record_filter/dsl/limit.rb
42
+ - lib/record_filter/dsl/named_filter.rb
41
43
  - lib/record_filter/dsl/order.rb
42
44
  - lib/record_filter/dsl/restriction.rb
43
45
  - lib/record_filter/filter.rb