aub-record_filter 0.6.0 → 0.8.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.
Files changed (39) hide show
  1. data/README.rdoc +1 -1
  2. data/Rakefile +35 -1
  3. data/VERSION.yml +2 -2
  4. data/lib/record_filter/active_record.rb +55 -12
  5. data/lib/record_filter/conjunctions.rb +1 -1
  6. data/lib/record_filter/dsl/class_join.rb +1 -1
  7. data/lib/record_filter/dsl/conjunction.rb +1 -1
  8. data/lib/record_filter/dsl/conjunction_dsl.rb +244 -18
  9. data/lib/record_filter/dsl/dsl.rb +90 -25
  10. data/lib/record_filter/dsl/dsl_factory.rb +19 -0
  11. data/lib/record_filter/dsl/group_by.rb +1 -1
  12. data/lib/record_filter/dsl/join.rb +1 -1
  13. data/lib/record_filter/dsl/join_condition.rb +1 -1
  14. data/lib/record_filter/dsl/join_dsl.rb +36 -1
  15. data/lib/record_filter/dsl/limit.rb +1 -1
  16. data/lib/record_filter/dsl/named_filter.rb +1 -1
  17. data/lib/record_filter/dsl/order.rb +1 -1
  18. data/lib/record_filter/dsl/restriction.rb +218 -22
  19. data/lib/record_filter/dsl.rb +16 -1
  20. data/lib/record_filter/filter.rb +15 -21
  21. data/lib/record_filter/group_by.rb +1 -1
  22. data/lib/record_filter/join.rb +1 -1
  23. data/lib/record_filter/order.rb +1 -1
  24. data/lib/record_filter/query.rb +4 -5
  25. data/lib/record_filter/restrictions.rb +1 -1
  26. data/lib/record_filter/table.rb +27 -13
  27. data/lib/record_filter.rb +26 -5
  28. data/spec/active_record_spec.rb +144 -0
  29. data/spec/exception_spec.rb +41 -0
  30. data/spec/explicit_join_spec.rb +5 -4
  31. data/spec/limits_and_ordering_spec.rb +6 -5
  32. data/spec/models.rb +19 -0
  33. data/spec/named_filter_spec.rb +5 -2
  34. data/spec/proxying_spec.rb +26 -12
  35. data/spec/restrictions_spec.rb +40 -0
  36. data/spec/test.db +0 -0
  37. data/test/performance_test.rb +36 -0
  38. data/{lib/record_filter/join_table.rb → test/test.db} +0 -0
  39. metadata +8 -3
data/README.rdoc CHANGED
@@ -12,7 +12,7 @@ record_filter has the following top-level features:
12
12
 
13
13
  == Installation
14
14
 
15
- gem install outoftime-record_filter --source=http://gems.github.com
15
+ gem install outoftime-record_filter --source=http://gems.github.com
16
16
 
17
17
  == Usage
18
18
 
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- ENV['RUBYOPT'] = '-W1'
1
+ # ENV['RUBYOPT'] = '-W1'
2
2
 
3
3
  require 'rubygems'
4
4
  require 'rake'
@@ -22,3 +22,37 @@ rescue LoadError
22
22
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
23
23
  end
24
24
 
25
+ # Try to use hanna to create spiffier docs.
26
+ begin
27
+ require 'hanna/rdoctask'
28
+ rescue LoadError
29
+ require 'rake/rdoctask'
30
+ end
31
+
32
+ Rake::RDocTask.new do |rdoc|
33
+ if File.exist?('VERSION.yml')
34
+ config = YAML.load(File.read('VERSION.yml'))
35
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
36
+ else
37
+ version = ""
38
+ end
39
+
40
+ rdoc.rdoc_dir = 'rdoc'
41
+ rdoc.title = "record_filter #{version}"
42
+ rdoc.rdoc_files.include('README*')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
45
+
46
+ begin
47
+ require 'ruby-prof/task'
48
+
49
+ RubyProf::ProfileTask.new do |t|
50
+ t.test_files = FileList['test/performance_test.rb']
51
+ t.output_dir = 'perf'
52
+ t.printer = :graph_html
53
+ t.min_percent = 5
54
+ end
55
+ rescue LoadError
56
+ puts 'Ruby-prof not available. Profiling tests are disabled.'
57
+ end
58
+
data/VERSION.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 0
3
2
  :major: 0
4
- :minor: 6
3
+ :minor: 8
4
+ :patch: 0
@@ -2,33 +2,68 @@ module RecordFilter
2
2
  # The ActiveRecordExtension module is mixed in to ActiveRecord::Base to form the
3
3
  # top-level API for interacting with record_filter. It adds public methods for
4
4
  # executing ad-hoc filters as well as for creating and querying named filters.
5
+ # See RecordFilter::ActiveRecordExtension::ClassMethods for more detail on the
6
+ # API.
5
7
  module ActiveRecordExtension
6
8
  module ClassMethods
7
9
 
8
- # Execute an ad-hoc filter
10
+ # Create a filter on the fly to find a set of results that matches the given criteria.
11
+ # This method, which can be called on any ActiveRecord::Base subclass, accepts a block
12
+ # that defines the contents of the filter and returns a Filter object that contains a list
13
+ # of the objects resulting from the query. See the documentation for RecordFilter::DSL
14
+ # for more information on how to specify the filter.
9
15
  #
10
16
  # ==== Parameters
11
- # block::
17
+ # block<Proc>::
12
18
  # A block that specifies the contents of the filter.
13
19
  #
14
20
  # ==== Returns
15
- # Filter:: The Filter object resulting from the query, which can be
16
- # treated as an array of the results.
21
+ # Filter::
22
+ # The Filter object resulting from the query, which can be treated as an array of the results.
17
23
  #
18
24
  # ==== Example
19
- # Blog.filter do
20
- # having(:posts).with(:name, nil)
21
- # end
22
- #—
25
+ # Blog.filter do
26
+ # having(:posts).with(:name, nil)
27
+ # end
28
+ #
23
29
  # @public
24
30
  def filter(&block)
25
31
  Filter.new(self, nil, &block)
26
32
  end
27
33
 
34
+ # Create a new named filter, which defines a function on the callee class that provides easy
35
+ # access to the query defined by the filter. Any number of named filters can be created on a
36
+ # class, and they can also be chained to create complex queries out of simple building blocks.
37
+ # In addition, named filters can accept any number of arguments in order to allow customization
38
+ # of their behavior when used. For more details on how to specify the contents of named filters,
39
+ # see the documentation for RecordFilter::DSL.
40
+ #
41
+ # Post.named_filter(:without_permalink) do
42
+ # with(:permalink, nil)
43
+ # end
44
+ #
45
+ # Post.named_filter(:created_after) do |time|
46
+ # with(:created_at).gt(time)
47
+ # end
48
+ #
49
+ # Post.without_permalink # :conditions => ['permalink IS NULL']
50
+ # Post.created_after(3.hours.ago) # :conditions => ['created_at > ?', 3.hours.ago]
51
+ # Post.without_permalink.created_after(3.hours.ago) # :conditions => ['permalink IS NULL AND created_at > ?', 3.hours.ago]
52
+ #
53
+ # ==== Raises
54
+ # InvalidFilterNameException::
55
+ # There is already a named filter with the given name on this class or one of its superclasses.
56
+ #
57
+ # ==== Returns
58
+ # nil
59
+ #
60
+ # @public
28
61
  def named_filter(name, &block)
29
- return if named_filters.include?(name.to_sym)
62
+ if named_filters.include?(name.to_sym)
63
+ raise InvalidFilterNameException.new("A named filter with the name #{name} already exists on the class #{self.name}.")
64
+ end
30
65
  local_named_filters << name.to_sym
31
- DSL::DSL::subclass(self).module_eval do
66
+ DSL::DSLFactory::subclass(self).module_eval do
32
67
  define_method(name, &block)
33
68
  end
34
69
 
@@ -37,8 +72,16 @@ module RecordFilter
37
72
  Filter.new(self, name, *args)
38
73
  end
39
74
  end
75
+ nil
40
76
  end
41
77
 
78
+ # Retreive a list of named filters that apply to a specific class, including ones
79
+ # that were defined in its superclasses.
80
+ #
81
+ # ==== Returns
82
+ # Array:: A list of the names of the named filters, as symbols.
83
+ #
84
+ # @public
42
85
  def named_filters
43
86
  result = local_named_filters.dup
44
87
  result.concat(superclass.named_filters) if (superclass && superclass.respond_to?(:named_filters))
@@ -46,8 +89,8 @@ module RecordFilter
46
89
  end
47
90
 
48
91
  protected
49
-
50
- def local_named_filters
92
+
93
+ def local_named_filters # :nodoc:
51
94
  @local_named_filters ||= []
52
95
  end
53
96
  end
@@ -1,5 +1,5 @@
1
1
  module RecordFilter
2
- module Conjunctions
2
+ module Conjunctions # :nodoc: all
3
3
  class Base
4
4
  attr_reader :table_name, :limit, :offset
5
5
 
@@ -1,6 +1,6 @@
1
1
  module RecordFilter
2
2
  module DSL
3
- class ClassJoin
3
+ class ClassJoin # :nodoc: all
4
4
  attr_reader :join_class, :join_type, :table_alias, :conjunction
5
5
 
6
6
  def initialize(join_class, join_type, table_alias, conjunction, join_conditions)
@@ -1,6 +1,6 @@
1
1
  module RecordFilter
2
2
  module DSL
3
- class Conjunction
3
+ class Conjunction # :nodoc: all
4
4
 
5
5
  attr_reader :type, :steps
6
6
 
@@ -1,78 +1,304 @@
1
1
  module RecordFilter
2
2
  module DSL
3
+ # The ConjunctionDSL is used for specifying restrictions, conjunctions and joins, with methods that
4
+ # can be accessed from any point in a filter declaration. The where method is used for creating
5
+ # restrictions, conjunctions are specified through any_of, all_of, none_of and not_all_of, and joins
6
+ # are described by having and join.
3
7
  class ConjunctionDSL
4
8
 
5
- attr_reader :conjunction
9
+ attr_reader :conjunction # :nodoc:
6
10
 
7
- def initialize(model_class, conjunction)
11
+ def initialize(model_class, conjunction) # :nodoc:
8
12
  @model_class = model_class
9
13
  @conjunction = conjunction
10
14
  end
11
15
 
12
- # restriction
16
+ # Specify a condition on the given column, which will be added to the WHERE clause
17
+ # of the resulting query. This method returns a Restriction object, which can be called
18
+ # with any of the specific restriction methods described there in order to create many
19
+ # types of conditions. If both a column name and a value are passed, this will automatically
20
+ # create an equality condition, so the following two examples are equal:
21
+ # with(:permalink, 'junk')
22
+ # with(:permalink).equal_to('junk')
23
+ # If nil is passed as the second argument, an is_null restriction will automatically be
24
+ # created, so these two examples are equal as well:
25
+ # with(:permalink, nil)
26
+ # with(:permalink).is_null
27
+ # This method can be called at any point in the filter specification, and the appropriate
28
+ # clauses will be created if it is called within or other conjunctions.
29
+ #
30
+ # ==== Parameters
31
+ # column<Symbol>::
32
+ # The name of the column to restrict. The column is assumed to exist in the table that is
33
+ # currently in scope. In the outer block of a filter, that would be the table being filtered,
34
+ # and within joins it would be the table being joined.
35
+ # value<value, optional>::
36
+ # If specified, the value will be used to automatically create either an equality restriction
37
+ # or an IS NULL test, as described above.
38
+ #
39
+ # ==== Returns
40
+ # Restriction::
41
+ # A restriction object that can be used to create a specific condition. See the API in
42
+ # Restriction for options.
43
+ #
44
+ # ==== Alternatives
45
+ # The value parameter is optional, as described above.
46
+ #
47
+ # @public
13
48
  def with(column, value=Restriction::DEFAULT_VALUE)
14
49
  return @conjunction.add_restriction(column, value)
15
50
  end
16
51
 
17
- # conjunction
52
+ # Add a where clause that will pass if any of the conditions specified within it
53
+ # are true. Any restrictions created inside the given block are OR'ed together
54
+ # in the final query, and the block can contain any number of joins, restrictions, or
55
+ # other conjunctions.
56
+ # Blog.filter do
57
+ # any_of do
58
+ # with(:created_at, nil)
59
+ # with(:created_at).greater_than(3.days.ago)
60
+ # end
61
+ # end
62
+ #
63
+ # # :conditions => { ['blogs.created_at IS NULL OR blogs.created_at > ?', 3.days.ago] }
64
+ #
65
+ # ==== Parameters
66
+ # block<Proc>::
67
+ # The block can contain any sequence of calls, and the conditions that it contains will be
68
+ # OR'ed together to create a where clause.
69
+ #
70
+ # ==== Returns
71
+ # nil
72
+ #
73
+ # @public
18
74
  def any_of(&block)
19
75
  @conjunction.add_conjunction(:any_of, &block)
20
76
  nil
21
77
  end
22
78
 
23
- # conjunction
79
+ # Add a where clause that will pass only if all of the conditions specified within it
80
+ # are true. Any restrictions created inside the given block are AND'ed together
81
+ # in the final query, and the block can contain any number of joins, restrictions, or
82
+ # other conjunctions.
83
+ # Blog.filter do
84
+ # all_of do
85
+ # with(:created_at, nil)
86
+ # with(:created_at).greater_than(3.days.ago)
87
+ # end
88
+ # end
89
+ #
90
+ # # :conditions => { ['blogs.created_at IS NULL AND blogs.created_at > ?', 3.days.ago] }
91
+ #
92
+ # ==== Parameters
93
+ # block<Proc>::
94
+ # The block can contain any sequence of calls, and the conditions that it contains will be
95
+ # AND'ed together to create a where clause.
96
+ #
97
+ # ==== Returns
98
+ # nil
99
+ #
100
+ # @public
24
101
  def all_of(&block)
25
102
  @conjunction.add_conjunction(:all_of, &block)
26
103
  nil
27
104
  end
28
105
 
106
+ # Add a where clause that will pass only if none of the conditions specified within it
107
+ # are true. Any restrictions created inside the given block are OR'ed together
108
+ # in the final query and the result is negated. The block can contain any number of joins,
109
+ # restrictions, or other conjunctions.
110
+ # Blog.filter do
111
+ # none_of do
112
+ # with(:created_at, nil)
113
+ # with(:created_at).greater_than(3.days.ago)
114
+ # end
115
+ # end
116
+ #
117
+ # # :conditions => { ['NOT (blogs.created_at IS NULL OR blogs.created_at > ?)', 3.days.ago] }
118
+ #
119
+ # ==== Parameters
120
+ # block<Proc>::
121
+ # The block can contain any sequence of calls, and the conditions that it contains will be
122
+ # OR'ed together and then negated to create a where clause.
123
+ #
124
+ # ==== Returns
125
+ # nil
126
+ #
127
+ # @public
29
128
  def none_of(&block)
30
129
  @conjunction.add_conjunction(:none_of, &block)
31
130
  nil
32
131
  end
33
132
 
133
+ # Add a where clause that will pass unless all of the conditions specified within it
134
+ # are true. Any restrictions created inside the given block are AND'ed together
135
+ # in the final query and the result is negated. The block can contain any number of joins,
136
+ # restrictions, or other conjunctions.
137
+ # Blog.filter do
138
+ # none_of do
139
+ # with(:created_at, nil)
140
+ # with(:created_at).greater_than(3.days.ago)
141
+ # end
142
+ # end
143
+ #
144
+ # # :conditions => { ['NOT (blogs.created_at IS NULL AND blogs.created_at > ?)', 3.days.ago] }
145
+ #
146
+ # ==== Parameters
147
+ # block<Proc>::
148
+ # The block can contain any sequence of calls, and the conditions that it contains will be
149
+ # AND'ed together and then negated to create a where clause.
150
+ #
151
+ # ==== Returns
152
+ # nil
153
+ #
154
+ # @public
34
155
  def not_all_of(&block)
35
156
  @conjunction.add_conjunction(:not_all_of, &block)
36
157
  nil
37
158
  end
38
159
 
39
- # join
40
- def having(join_type_or_association, association=nil, &block)
160
+ # Create an implicit join using an association as the target. This method allows you to
161
+ # easily specify a join without specifying the columns to use by taking any needed data
162
+ # from the specified ActiveRecord association. If given, the block will be evaluated in
163
+ # the context of the table that has been joined, so any restrictions or other joins will
164
+ # be performed using its columns and associations. For example, if a Post has_many comments
165
+ # then the following code will join to the comments table and restrict the comments based
166
+ # on their created_at field:
167
+ # Post.filter do
168
+ # having(:comments) do
169
+ # with(:created_at).greater_than(3.days.ago)
170
+ # end
171
+ # end
172
+ # If one argument is given, it is assumed to be a symbol representing the name of the association
173
+ # that will be used for the join and a join type of :inner will be used. If two arguments are
174
+ # provided, the first one is assumed to be the join type, which can be one of :inner, :left or
175
+ # :right and the second one is the association name. An alias will automatically be created
176
+ # for the joined table named "#{left_table}__#{association_name}", so in the above example, the
177
+ # alias would be posts__comments.
178
+ #
179
+ # ==== Parameters
180
+ # join_type<Symbol>::
181
+ # Specifies the type of join to perform, and can be one of :inner, :left or :right. :left
182
+ # and :right will create left and right outer joins, respectively.
183
+ # association<Symbol>::
184
+ # The name of the association to use as a base for the join.
185
+ #
186
+ # ==== Returns
187
+ # ConjunctionDSL::
188
+ # A DSL object is returned in order to allow constructs like: having(:comments).with(:offensive, true)
189
+ #
190
+ # ==== Alternatives
191
+ # If only one argument is given, the join type will default to :inner and the first argument will
192
+ # be used as the association name.
193
+ #
194
+ # @public
195
+ def having(join_type, association=nil, &block)
41
196
  if association.nil?
42
- association, join_type = join_type_or_association, nil
43
- else
44
- join_type = join_type_or_association
197
+ association, join_type = join_type, nil
45
198
  end
46
199
  @conjunction.add_join(association, join_type, &block)
47
200
  end
48
201
 
202
+ # Create an explicit join on the table of the given class. This method allows more complex
203
+ # joins to be speficied than can be created using having, including jump joins and ones that
204
+ # include conditions on column values. The method accepts a block that can contain any sequence
205
+ # of conjunctions, restrictions, or other joins, but it must also contain at least one call to
206
+ # JoinDSL.on to specify the conditions for the join.
207
+ #
208
+ # ==== Parameters
209
+ # clazz<Class>::
210
+ # The class that is being joined to.
211
+ # join_type<Symbol>::
212
+ # Indicates the type of join to use and must be one of :inner, :left or :right, where :left
213
+ # or :right will create a LEFT or RIGHT OUTER join respectively.
214
+ # table_alias<String, optional>::
215
+ # If provided, will specify an alias to use in the SQL when referring to the joined table.
216
+ # If the argument is not given, the alias will be "#{left_table}__#{clazz.name}"
217
+ # block<Proc>
218
+ # The contents of the join block can contain any sequence of conjunctions, restrictions, or joins.
219
+ #
220
+ # ==== Returns
221
+ # JoinDSL::
222
+ # A DSL object that can be used to specify the contents of the join. Returning this value allows
223
+ # for constructions like: join(Comment, :inner).on(:id => :post_id)
224
+ #
225
+ # @public
49
226
  def join(clazz, join_type, table_alias=nil, &block)
50
227
  @conjunction.add_class_join(clazz, join_type, table_alias, &block)
51
228
  end
52
229
 
230
+ # Access the class that the current filter is being applied to. This is necessary
231
+ # because the filter is evaluated in the context of the DSL object, so self will
232
+ # not give access to any methods that need to be called on the filtered class.
233
+ # It is especially useful in named filters that may be defined in a way that allows
234
+ # them to apply to multiple classes.
235
+ #
236
+ # ==== Returns
237
+ # Class::
238
+ # The class that is currently being filtered.
239
+ #
240
+ # @public
53
241
  def filter_class
54
242
  @model_class
55
243
  end
244
+
245
+ # Enable calling of named filters from within other filters by catching unknown calls
246
+ # and assuming that they are to named filters. This enables the following examples:
247
+ # class Post < ActiveRecord::Base
248
+ # has_many :comments
249
+ # named_filter(:empty) { with(:contents).nil }
250
+ # end
251
+ #
252
+ # class Comment < ActiveRecord::Base
253
+ # belongs_to :post
254
+ # named_filter(:offensive) { |value| with(:offensive, value) }
255
+ # end
256
+ #
257
+ # Post.filter do
258
+ # with(:created_at).less_than(1.hour.ago)
259
+ # empty
260
+ # end
261
+ #
262
+ # # Results in:
263
+ # # :conditions => { ['posts.created_at < ? AND posts.contents IS NULL', 1.hour.ago] }
264
+ # # And even cooler:
265
+ #
266
+ # Post.filter do
267
+ # having(:comments).offensive(true)
268
+ # end
269
+ #
270
+ # # Results in:
271
+ # # :conditions => { ['posts__comments.offensive = ?', true] }
272
+ # # :joins => { 'INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id' }
273
+ #
274
+ # ==== Parameters
275
+ # args<Array>::
276
+ # The arguments to pass to the named filter when called.
277
+ #
278
+ # @public
279
+ def method_missing(method, *args)
280
+ @conjunction.add_named_filter(method, *args)
281
+ end
56
282
 
57
- def limit(offset_or_limit, limit=nil)
283
+ #
284
+ # Define these_methods here just so that we can throw exceptions when they are called. They should not
285
+ # be callable in the scope of a conjunction_dsl.
286
+ #
287
+ def limit(offset_or_limit, limit=nil) # :nodoc:
58
288
  raise InvalidFilterException.new('Calls to limit can only be made in the outer block of a filter.')
59
289
  end
60
290
 
61
- def order(column, direction=:asc)
291
+ def order(column, direction=:asc) # :nodoc:
62
292
  raise InvalidFilterException.new('Calls to order can only be made in the outer block of a filter.')
63
293
  end
64
294
 
65
- def group_by(column)
295
+ def group_by(column) # :nodoc:
66
296
  raise InvalidFilterException.new('Calls to group_by can only be made in the outer block of a filter.')
67
297
  end
68
298
 
69
- def on(column, value=Restriction::DEFAULT_VALUE)
299
+ def on(column, value=Restriction::DEFAULT_VALUE) # :nodoc:
70
300
  raise InvalidFilterException.new('Calls to on can only be made in the block of a call to join.')
71
301
  end
72
-
73
- def method_missing(method, *args)
74
- @conjunction.add_named_filter(method, *args)
75
- end
76
302
  end
77
303
  end
78
304
  end
@@ -2,42 +2,107 @@ module RecordFilter
2
2
  module DSL
3
3
  class DSL < ConjunctionDSL
4
4
 
5
- SUBCLASSES = Hash.new do |h, k|
6
- h[k] = Class.new(DSL)
7
- end
8
-
9
- class << self
10
- def create(clazz)
11
- subclass(clazz).new(clazz, Conjunction.new(clazz, :all_of))
12
- end
13
-
14
- def subclass(clazz)
15
- SUBCLASSES[clazz.object_id]
16
- end
17
- end
18
-
19
- # This method can take two forms:
20
- # limit(offset, limit), or
21
- # limit(limit)
22
- def limit(offset_or_limit, limit=nil)
5
+ # Define an limit and/or offset for the results returned from the current
6
+ # filter. This method can only be called from the outermost scope of a filter
7
+ # (i.e. not inside of a having block, etc.). If it is called multiple times, the
8
+ # last one will override any others.
9
+ #
10
+ # ==== Parameters
11
+ # offset<Integer>::
12
+ # Used for the offset of the query.
13
+ # limit<Integer::
14
+ # Used as the limit for the query.
15
+ #
16
+ # ==== Returns
17
+ # nil
18
+ #
19
+ # ==== Alternatives
20
+ # If called with a single argument, it is assumed to represent the limit, and
21
+ # no offset will be specified.
22
+ #
23
+ # @public
24
+ def limit(offset, limit=nil)
23
25
  if limit
24
- @conjunction.add_limit(limit, offset_or_limit)
26
+ @conjunction.add_limit(limit, offset)
25
27
  else
26
- @conjunction.add_limit(offset_or_limit, nil)
28
+ @conjunction.add_limit(offset, nil)
27
29
  end
28
30
  nil
29
31
  end
30
32
 
31
- # This method can take two forms, as shown below.
32
- # order :permalink
33
- # order :permalink, :desc
34
- # order :photo => :path, :desc
35
- # order :photo => { :comment => :id }, :asc
33
+ # Define an order clause for the current query, with options for specifying
34
+ # both the column to use as well as the direction. This method can only be called
35
+ # in the outermost scope of a filter (i.e. not inside of a having block, etc.).
36
+ # Multiple calls will create multiple order clauses in the resulting query, and
37
+ # they will be added in the order in which they were called in the filter. In order
38
+ # to specify ordering on columns added through joins, a hash can be passed as the
39
+ # first argument, specifying a path through the joins to the column, as in this
40
+ # example:
41
+ #
42
+ # Blog.filter do
43
+ # having(:posts) do
44
+ # having(:comments).with(:created_at).greater_than(3.days.ago)
45
+ # end
46
+ # order(:posts => :comments => :created_at, :desc)
47
+ # order(:id, :asc)
48
+ # end
49
+ #
50
+ # ==== Parameters
51
+ # column<Symbol, Hash>::
52
+ # Specify the column for the ordering. If a symbol is given, it is assumed to represent
53
+ # a column in the class that is being filtered. With a hash argument, it is possible
54
+ # to specify a path to a column in one of the joined tables, as seen above.
55
+ # direction<Symbol>::
56
+ # Specifies the direction of the join. Should be either :asc or :desc.
57
+ #
58
+ # ==== Returns
59
+ # nil
60
+ #
61
+ # ==== Raises
62
+ # InvalidFilterException::
63
+ # If the direction is neither :asc nor :desc.
64
+ #
65
+ # ==== Alternatives
66
+ # As described above, it is possible to pass either a symbol or a hash as the first
67
+ # argument.
68
+ #
69
+ # @public
36
70
  def order(column, direction=:asc)
71
+ unless [:asc, :desc].include?(direction)
72
+ raise InvalidFilterException.new("The direction for orders must be either :asc or :desc but was #{direction}")
73
+ end
37
74
  @conjunction.add_order(column, direction)
38
75
  nil
39
76
  end
40
77
 
78
+ # Specify a group_by clause for the resulting query. This method can only be called
79
+ # in the outermost scope of a filter (i.e. not inside of a having block, etc.).
80
+ # Multiple calls will create multiple group_by clauses in the resulting query, and
81
+ # they will be added in the order in which they were called in the filter. In order
82
+ # to specify grouping on columns added through joins, a hash can be passed as the
83
+ # argument, specifying a path through the joins to the column, as in this example:
84
+ #
85
+ # Blog.filter do
86
+ # having(:posts) do
87
+ # having(:comments).with(:created_at).greater_than(3.days.ago)
88
+ # end
89
+ # group_by(:posts => :comments => :offensive)
90
+ # group_by(:id)
91
+ # end
92
+ #
93
+ # ==== Parameters
94
+ # column<Symbol, Hash>::
95
+ # If a symbol is specified, it is taken to represent the name of a column on the
96
+ # class being filtered. If a hash is given, it should represent a path through the
97
+ # joins to a column in one of the joined tables.
98
+ #
99
+ # ==== Returns
100
+ # nil
101
+ #
102
+ # ==== Alternatives
103
+ # As described above, it is possible to pass either a symbol or a hash as the argument.
104
+ #
105
+ # @public
41
106
  def group_by(column)
42
107
  @conjunction.add_group_by(column)
43
108
  nil
@@ -0,0 +1,19 @@
1
+ module RecordFilter
2
+ module DSL
3
+ class DSLFactory # :nodoc: all
4
+ SUBCLASSES = Hash.new do |h, k|
5
+ h[k] = Class.new(RecordFilter::DSL::DSL)
6
+ end
7
+
8
+ class << self
9
+ def create(clazz)
10
+ subclass(clazz).new(clazz, Conjunction.new(clazz, :all_of))
11
+ end
12
+
13
+ def subclass(clazz)
14
+ SUBCLASSES[clazz.object_id]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,6 @@
1
1
  module RecordFilter
2
2
  module DSL
3
- class GroupBy
3
+ class GroupBy # :nodoc: all
4
4
  attr_reader :column
5
5
 
6
6
  def initialize(column)