record_filter 0.9.12
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +9 -0
- data/CHANGELOG +232 -0
- data/README.rdoc +354 -0
- data/Rakefile +92 -0
- data/TODO +3 -0
- data/VERSION.yml +4 -0
- data/config/roodi.yml +14 -0
- data/lib/record_filter/active_record.rb +108 -0
- data/lib/record_filter/column_parser.rb +14 -0
- data/lib/record_filter/conjunctions.rb +169 -0
- data/lib/record_filter/dsl/class_join.rb +16 -0
- data/lib/record_filter/dsl/conjunction.rb +57 -0
- data/lib/record_filter/dsl/conjunction_dsl.rb +317 -0
- data/lib/record_filter/dsl/dsl.rb +143 -0
- data/lib/record_filter/dsl/dsl_factory.rb +19 -0
- data/lib/record_filter/dsl/group_by.rb +11 -0
- data/lib/record_filter/dsl/join.rb +12 -0
- data/lib/record_filter/dsl/join_condition.rb +21 -0
- data/lib/record_filter/dsl/join_dsl.rb +49 -0
- data/lib/record_filter/dsl/limit.rb +12 -0
- data/lib/record_filter/dsl/named_filter.rb +12 -0
- data/lib/record_filter/dsl/order.rb +12 -0
- data/lib/record_filter/dsl/restriction.rb +314 -0
- data/lib/record_filter/dsl.rb +21 -0
- data/lib/record_filter/filter.rb +105 -0
- data/lib/record_filter/group_by.rb +21 -0
- data/lib/record_filter/join.rb +66 -0
- data/lib/record_filter/order.rb +27 -0
- data/lib/record_filter/query.rb +60 -0
- data/lib/record_filter/restriction_factory.rb +21 -0
- data/lib/record_filter/restrictions.rb +97 -0
- data/lib/record_filter/table.rb +172 -0
- data/lib/record_filter.rb +35 -0
- data/record_filter.gemspec +108 -0
- data/script/console +8 -0
- data/spec/active_record_spec.rb +211 -0
- data/spec/exception_spec.rb +208 -0
- data/spec/explicit_join_spec.rb +132 -0
- data/spec/implicit_join_spec.rb +403 -0
- data/spec/limits_and_ordering_spec.rb +230 -0
- data/spec/models.rb +109 -0
- data/spec/named_filter_spec.rb +264 -0
- data/spec/proxying_spec.rb +63 -0
- data/spec/restrictions_spec.rb +251 -0
- data/spec/select_spec.rb +79 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/test.db +0 -0
- data/tasks/db.rake +106 -0
- data/tasks/rcov.rake +9 -0
- data/tasks/spec.rake +10 -0
- data/test/performance_test.rb +39 -0
- data/test/test.db +0 -0
- metadata +137 -0
@@ -0,0 +1,317 @@
|
|
1
|
+
module RecordFilter
|
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.
|
7
|
+
class ConjunctionDSL
|
8
|
+
|
9
|
+
attr_reader :conjunction # :nodoc:
|
10
|
+
|
11
|
+
def initialize(model_class, conjunction) # :nodoc:
|
12
|
+
@model_class = model_class
|
13
|
+
@conjunction = conjunction
|
14
|
+
end
|
15
|
+
|
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
|
48
|
+
def with(column, value=Restriction::DEFAULT_VALUE)
|
49
|
+
return @conjunction.add_restriction(column, value)
|
50
|
+
end
|
51
|
+
|
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
|
74
|
+
def any_of(&block)
|
75
|
+
@conjunction.add_conjunction(:any_of, &block)
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
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
|
101
|
+
def all_of(&block)
|
102
|
+
@conjunction.add_conjunction(:all_of, &block)
|
103
|
+
nil
|
104
|
+
end
|
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
|
128
|
+
def none_of(&block)
|
129
|
+
@conjunction.add_conjunction(:none_of, &block)
|
130
|
+
nil
|
131
|
+
end
|
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
|
155
|
+
def not_all_of(&block)
|
156
|
+
@conjunction.add_conjunction(:not_all_of, &block)
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
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 given ActiveRecord association. If provided, 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
|
+
# Options can be passed to provide a custom join type or table alias through the options
|
173
|
+
# hash. The :join_type option can be either :inner, :left or :right and will default to
|
174
|
+
# :inner if not provided. The :alias option allows you to provide an alias for use in joining
|
175
|
+
# the table. If the same association is joined twice with different aliases, it will be treated
|
176
|
+
# as two separate joins. By default an alias will automatically be created
|
177
|
+
# for the joined table named "#{left_table}__#{association_name}", so in the above example, the
|
178
|
+
# alias would be posts__comments. It is also possible to provide a hash as the association
|
179
|
+
# name, in which case a trail of associations can be joined in one statment.
|
180
|
+
#
|
181
|
+
# ==== Parameters
|
182
|
+
# association<Symbol>::
|
183
|
+
# The name of the association to use as a base for the join.
|
184
|
+
# options<Hash>::
|
185
|
+
# An options hash (see below)
|
186
|
+
#
|
187
|
+
# ==== Options (options)
|
188
|
+
# :join_type<Symbol>::
|
189
|
+
# The type of join to use. Available options are :inner, :left and :right. Defaults to :inner.
|
190
|
+
# :alias<String>::
|
191
|
+
# An alias to use for the table name in the join. If provided, will create a unique name for
|
192
|
+
# the join and allow the same association to be joined multiple times. By default, the alias
|
193
|
+
# will be "#{left_table}__#{association_name}".
|
194
|
+
#
|
195
|
+
# ==== Returns
|
196
|
+
# ConjunctionDSL::
|
197
|
+
# A DSL object is returned in order to allow constructs like: having(:comments).with(:offensive, true)
|
198
|
+
#
|
199
|
+
# ==== Alternatives
|
200
|
+
# If only one argument is given, the join type will default to :inner and the first argument will
|
201
|
+
# be used as the association name.
|
202
|
+
#
|
203
|
+
# @public
|
204
|
+
def having(association, options={}, &block)
|
205
|
+
@conjunction.add_join(association, options[:join_type], options[:alias], &block)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create an explicit join on the table of the given class. This method allows more complex
|
209
|
+
# joins to be speficied than can be created using having, including jump joins and ones that
|
210
|
+
# include conditions on column values. The method accepts a block that can contain any sequence
|
211
|
+
# of conjunctions, restrictions, or other joins, but it must also contain at least one call to
|
212
|
+
# JoinDSL.on to specify the conditions for the join. The options hash accepts :join_type and
|
213
|
+
# :alias parameters. The :join_type parameter can be either :inner, :left or :right and defaults
|
214
|
+
# to :inner. The :alias parameter allows you to specify an alias for the table in the join and
|
215
|
+
# defaults to "#{left_table}__#{clazz.name}".
|
216
|
+
#
|
217
|
+
# ==== Parameters
|
218
|
+
# clazz<Class>::
|
219
|
+
# The class that is being joined to.
|
220
|
+
# options<Hash>::
|
221
|
+
# An options hash (see below)
|
222
|
+
# block<Proc>
|
223
|
+
# The contents of the join block can contain any sequence of conjunctions, restrictions, or joins.
|
224
|
+
#
|
225
|
+
# ==== Options(options)
|
226
|
+
# :join_type<Symbol>::
|
227
|
+
# Indicates the type of join to use and must be one of :inner, :left or :right, where :left
|
228
|
+
# or :right will create a LEFT or RIGHT OUTER join respectively.
|
229
|
+
# :alias<String>::
|
230
|
+
# If provided, will specify an alias to use in the SQL when referring to the joined table.
|
231
|
+
# If the argument is not given, the alias will be "#{left_table}__#{clazz.name}"
|
232
|
+
#
|
233
|
+
# ==== Returns
|
234
|
+
# JoinDSL::
|
235
|
+
# A DSL object that can be used to specify the contents of the join. Returning this value allows
|
236
|
+
# for constructions like: join(Comment, :inner).on(:id => :post_id)
|
237
|
+
#
|
238
|
+
# @public
|
239
|
+
def join(clazz, options={}, &block)
|
240
|
+
@conjunction.add_class_join(clazz, options[:join_type], options[:alias], &block)
|
241
|
+
end
|
242
|
+
|
243
|
+
# Access the class that the current filter is being applied to. This is necessary
|
244
|
+
# because the filter is evaluated in the context of the DSL object, so self will
|
245
|
+
# not give access to any methods that need to be called on the filtered class.
|
246
|
+
# It is especially useful in named filters that may be defined in a way that allows
|
247
|
+
# them to apply to multiple classes.
|
248
|
+
#
|
249
|
+
# ==== Returns
|
250
|
+
# Class::
|
251
|
+
# The class that is currently being filtered.
|
252
|
+
#
|
253
|
+
# @public
|
254
|
+
def filter_class
|
255
|
+
@model_class
|
256
|
+
end
|
257
|
+
|
258
|
+
# Enable calling of named filters from within other filters by catching unknown calls
|
259
|
+
# and assuming that they are to named filters. This enables the following examples:
|
260
|
+
# class Post < ActiveRecord::Base
|
261
|
+
# has_many :comments
|
262
|
+
# named_filter(:empty) { with(:contents).nil }
|
263
|
+
# end
|
264
|
+
#
|
265
|
+
# class Comment < ActiveRecord::Base
|
266
|
+
# belongs_to :post
|
267
|
+
# named_filter(:offensive) { |value| with(:offensive, value) }
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# Post.filter do
|
271
|
+
# with(:created_at).less_than(1.hour.ago)
|
272
|
+
# empty
|
273
|
+
# end
|
274
|
+
#
|
275
|
+
# # Results in:
|
276
|
+
# # :conditions => { ['posts.created_at < ? AND posts.contents IS NULL', 1.hour.ago] }
|
277
|
+
# # And even cooler:
|
278
|
+
#
|
279
|
+
# Post.filter do
|
280
|
+
# having(:comments).offensive(true)
|
281
|
+
# end
|
282
|
+
#
|
283
|
+
# # Results in:
|
284
|
+
# # :conditions => { ['posts__comments.offensive = ?', true] }
|
285
|
+
# # :joins => { 'INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id' }
|
286
|
+
#
|
287
|
+
# ==== Parameters
|
288
|
+
# args<Array>::
|
289
|
+
# The arguments to pass to the named filter when called.
|
290
|
+
#
|
291
|
+
# @public
|
292
|
+
def method_missing(method, *args)
|
293
|
+
@conjunction.add_named_filter(method, *args)
|
294
|
+
end
|
295
|
+
|
296
|
+
#
|
297
|
+
# Define these_methods here just so that we can throw exceptions when they are called. They should not
|
298
|
+
# be callable in the scope of a conjunction_dsl.
|
299
|
+
#
|
300
|
+
def limit(limit, offset=nil) # :nodoc:
|
301
|
+
raise InvalidFilterException.new('Calls to limit can only be made in the outer block of a filter.')
|
302
|
+
end
|
303
|
+
|
304
|
+
def order(column, direction=:asc) # :nodoc:
|
305
|
+
raise InvalidFilterException.new('Calls to order can only be made in the outer block of a filter.')
|
306
|
+
end
|
307
|
+
|
308
|
+
def group_by(column) # :nodoc:
|
309
|
+
raise InvalidFilterException.new('Calls to group_by can only be made in the outer block of a filter.')
|
310
|
+
end
|
311
|
+
|
312
|
+
def on(column, value=Restriction::DEFAULT_VALUE) # :nodoc:
|
313
|
+
raise InvalidFilterException.new('Calls to on can only be made in the block of a call to join.')
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class DSL < ConjunctionDSL
|
4
|
+
|
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
|
+
# limit<Integer>::
|
12
|
+
# Used for the limit of the query.
|
13
|
+
# offset<Integer>::
|
14
|
+
# Used as the offset for the query. This argument is optional, with the default
|
15
|
+
# being no offset.
|
16
|
+
#
|
17
|
+
# ==== Returns
|
18
|
+
# nil
|
19
|
+
#
|
20
|
+
# @public
|
21
|
+
def limit(limit, offset=nil)
|
22
|
+
@conjunction.add_limit(limit, offset)
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Define an offset for the results returned from the current
|
27
|
+
# filter. This method can only be called from the outermost scope of a filter
|
28
|
+
# (i.e. not inside of a having block, etc.). If it is called multiple times, the
|
29
|
+
# last one will override any others.
|
30
|
+
#
|
31
|
+
# ==== Parameters
|
32
|
+
# offset<Integer>::
|
33
|
+
# The offset of the query.
|
34
|
+
#
|
35
|
+
# ==== Returns
|
36
|
+
# nil
|
37
|
+
#
|
38
|
+
# @public
|
39
|
+
def offset(offset)
|
40
|
+
@conjunction.add_limit(nil, offset)
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Define an order clause for the current query, with options for specifying
|
45
|
+
# both the column to use as well as the direction. This method can only be called
|
46
|
+
# in the outermost scope of a filter (i.e. not inside of a having block, etc.).
|
47
|
+
# Multiple calls will create multiple order clauses in the resulting query, and
|
48
|
+
# they will be added in the order in which they were called in the filter. In order
|
49
|
+
# to specify ordering on columns added through joins, a hash can be passed as the
|
50
|
+
# first argument, specifying a path through the joins to the column, as in this
|
51
|
+
# example:
|
52
|
+
#
|
53
|
+
# Blog.filter do
|
54
|
+
# having(:posts) do
|
55
|
+
# having(:comments).with(:created_at).greater_than(3.days.ago)
|
56
|
+
# end
|
57
|
+
# order(:posts => :comments => :created_at, :desc)
|
58
|
+
# order(:id, :asc)
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# ==== Parameters
|
62
|
+
# column<Symbol, Hash>::
|
63
|
+
# Specify the column for the ordering. If a symbol is given, it is assumed to represent
|
64
|
+
# a column in the class that is being filtered. With a hash argument, it is possible
|
65
|
+
# to specify a path to a column in one of the joined tables, as seen above. If a string
|
66
|
+
# is given and it doesn't match up with a column name, it is used as a literal string
|
67
|
+
# for ordering.
|
68
|
+
# direction<Symbol>::
|
69
|
+
# Specifies the direction of the order. Should be either :asc or :desc and defaults to :asc.
|
70
|
+
#
|
71
|
+
# ==== Returns
|
72
|
+
# nil
|
73
|
+
#
|
74
|
+
# ==== Raises
|
75
|
+
# InvalidFilterException::
|
76
|
+
# If the direction is neither :asc nor :desc.
|
77
|
+
#
|
78
|
+
# ==== Alternatives
|
79
|
+
# As described above, it is possible to pass a symbol, a hash or a string as the first
|
80
|
+
# argument.
|
81
|
+
#
|
82
|
+
# @public
|
83
|
+
def order(column, direction=:asc)
|
84
|
+
unless [:asc, :desc].include?(direction)
|
85
|
+
raise InvalidFilterException.new("The direction for orders must be either :asc or :desc but was #{direction}")
|
86
|
+
end
|
87
|
+
@conjunction.add_order(column, direction)
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
|
91
|
+
# Specify a group_by clause for the resulting query. This method can only be called
|
92
|
+
# in the outermost scope of a filter (i.e. not inside of a having block, etc.).
|
93
|
+
# Multiple calls will create multiple group_by clauses in the resulting query, and
|
94
|
+
# they will be added in the order in which they were called in the filter. In order
|
95
|
+
# to specify grouping on columns added through joins, a hash can be passed as the
|
96
|
+
# argument, specifying a path through the joins to the column, as in this example:
|
97
|
+
#
|
98
|
+
# Blog.filter do
|
99
|
+
# having(:posts) do
|
100
|
+
# having(:comments).with(:created_at).greater_than(3.days.ago)
|
101
|
+
# end
|
102
|
+
# group_by(:posts => :comments => :offensive)
|
103
|
+
# group_by(:id)
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# ==== Parameters
|
107
|
+
# column<Symbol, Hash>::
|
108
|
+
# If a symbol is specified, it is taken to represent the name of a column on the
|
109
|
+
# class being filtered. If a hash is given, it should represent a path through the
|
110
|
+
# joins to a column in one of the joined tables. If a string is given, it is used
|
111
|
+
# without modification as the grouping parameter.
|
112
|
+
#
|
113
|
+
# ==== Returns
|
114
|
+
# nil
|
115
|
+
#
|
116
|
+
# ==== Alternatives
|
117
|
+
# As described above, it is possible to pass either a symbol, a hash, or a string
|
118
|
+
# as the argument.
|
119
|
+
#
|
120
|
+
# @public
|
121
|
+
def group_by(column)
|
122
|
+
@conjunction.add_group_by(column)
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
# Specify that the resulting query should select distinct results. This method
|
127
|
+
# can only be called in the outermost scope of a filter (i.e. not inside of a having
|
128
|
+
# block, etc.).
|
129
|
+
#
|
130
|
+
# ==== Parameters
|
131
|
+
# none
|
132
|
+
#
|
133
|
+
# ==== Returns
|
134
|
+
# nil
|
135
|
+
#
|
136
|
+
# @public
|
137
|
+
def distinct
|
138
|
+
@conjunction.set_distinct
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -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
|
+
get_subclass(clazz).new(clazz, Conjunction.new(clazz, :all_of))
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_subclass(clazz)
|
14
|
+
SUBCLASSES[clazz.object_id]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class Join # :nodoc: all
|
4
|
+
|
5
|
+
attr_reader :association, :join_type, :conjunction, :aliaz
|
6
|
+
|
7
|
+
def initialize(association, join_type, conjunction, aliaz)
|
8
|
+
@association, @join_type, @conjunction, @aliaz = association, join_type, conjunction, aliaz
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
class JoinCondition # :nodoc: all
|
4
|
+
|
5
|
+
attr_reader :restriction
|
6
|
+
|
7
|
+
def initialize(column, value)
|
8
|
+
@column = column
|
9
|
+
if column.is_a?(Hash) && value == Restriction::DEFAULT_VALUE
|
10
|
+
@condition = column
|
11
|
+
else
|
12
|
+
@restriction = Restriction.new(column, value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def condition
|
17
|
+
@condition || restriction
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module RecordFilter
|
2
|
+
module DSL
|
3
|
+
# This class is used as the active DSL when executing the block provided to explicit
|
4
|
+
# joins created with ConjunctionDSL.join. It is a subclass of ConjunctionDSL that
|
5
|
+
# adds a method for specifying how the join is to be performed by giving either a pair
|
6
|
+
# of columns or a restriction that is applied to a column of the right table.
|
7
|
+
class JoinDSL < ConjunctionDSL
|
8
|
+
|
9
|
+
attr_reader :conditions # :nodoc:
|
10
|
+
|
11
|
+
# Specify parameters for explicit joins. This method should be called at least once
|
12
|
+
# within the block of a call to ConjunctionDSL.join and accepts various combinations
|
13
|
+
# of arguments to determine how to join the two tables. The possible options are:
|
14
|
+
#
|
15
|
+
# * Pass a hash where the key is a symbol that represents a column in the left table and the value is a symbol that represents a column in the right table.
|
16
|
+
# * Pass a symbol and a value, in which case an equality condition will be created on the column in the right table with the given column name.
|
17
|
+
# * Pass only the name of a column in the right table, in which case the method can be chained with any of the calls in the Restriction API to create generic restrictions on the column.
|
18
|
+
# Post.filter do
|
19
|
+
# join(Comment, :inner) do
|
20
|
+
# on(:id => :post_id)
|
21
|
+
# on(:offensive, false)
|
22
|
+
# on(:id).greater_than(12)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # Results in:
|
26
|
+
# # :joins => "INNER JOIN "comments" AS posts__Comment ON "posts".id = posts__Comment.post_id AND posts__Comment.offensive = false AND posts__Comment.id > 12"
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# ==== Parameters
|
30
|
+
# column<Hash, Symbol>::
|
31
|
+
# Either a hash representing the column pair to join or a symbol representing the column in the
|
32
|
+
# right table to add a condition to.
|
33
|
+
# value<value, optional>::
|
34
|
+
# If provided along with a symbol for the column argument, creates an equality condition on that
|
35
|
+
# column.
|
36
|
+
#
|
37
|
+
# ==== Returns
|
38
|
+
# Restriction::
|
39
|
+
# A restriction that can be used to limit the column when a symbol is passed as the column name.
|
40
|
+
#
|
41
|
+
# @public
|
42
|
+
def on(column, value=Restriction::DEFAULT_VALUE)
|
43
|
+
@conditions ||= []
|
44
|
+
@conditions << (condition = JoinCondition.new(column, value))
|
45
|
+
return condition.restriction
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|