aub-record_filter 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +163 -97
- data/VERSION.yml +1 -1
- data/lib/record_filter/dsl/conjunction_dsl.rb +6 -5
- data/lib/record_filter/dsl/dsl.rb +1 -1
- data/lib/record_filter/dsl/restriction.rb +15 -0
- data/lib/record_filter/filter.rb +1 -1
- data/lib/record_filter/query.rb +1 -1
- data/lib/record_filter/table.rb +2 -1
- data/spec/active_record_spec.rb +81 -28
- data/spec/explicit_join_spec.rb +5 -5
- data/spec/implicit_join_spec.rb +36 -17
- data/spec/models.rb +7 -0
- data/spec/named_filter_spec.rb +4 -4
- data/spec/restrictions_spec.rb +7 -0
- data/spec/test.db +0 -0
- metadata +2 -2
data/README.rdoc
CHANGED
@@ -14,176 +14,242 @@ record_filter has the following top-level features:
|
|
14
14
|
|
15
15
|
gem install outoftime-record_filter --source=http://gems.github.com
|
16
16
|
|
17
|
-
==
|
17
|
+
== Using Filters
|
18
18
|
|
19
|
-
|
19
|
+
Given a Blog model having a has_many relationship with a Post model, a simple
|
20
|
+
filter with conditions and joins might look like this.
|
20
21
|
|
21
|
-
|
22
|
-
with(:
|
23
|
-
having(:
|
22
|
+
Blog.filter do
|
23
|
+
with(:created_at).greater_than(1.day.ago)
|
24
|
+
having(:posts).with(:permalink, nil)
|
24
25
|
end
|
25
26
|
|
26
27
|
This could be expressed in ActiveRecord as:
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
Blog.find(
|
30
|
+
:all,
|
31
|
+
:joins => :posts,
|
32
|
+
:conditions => ['posts.permalink IS NULL AND blogs.created_at > ?', 'blog-post', 1.day.ago)
|
31
33
|
|
32
|
-
|
34
|
+
and it returns the same result, a list of Blog objects that are returned from the query. This
|
35
|
+
type of filter is designed to be created on the fly, but if you have a filter that you would like
|
36
|
+
to use in more than one place, it can be added to a class as a named filter. The following example
|
37
|
+
creates the same filter as above and executes it:
|
33
38
|
|
34
39
|
class Post < ActiveRecord::Base
|
35
|
-
named_filter(:
|
36
|
-
with(:
|
40
|
+
named_filter(:new_with_nil_permalink) do
|
41
|
+
with(:created_at).greater_than(1.day.ago)
|
42
|
+
having(:posts).with(:permalink, nil)
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
40
|
-
Post.
|
46
|
+
Post.new_with_nil_permalink
|
41
47
|
|
42
|
-
This
|
48
|
+
This returns the same result as the example above but with the advantages that it is
|
49
|
+
easily reusable and that it can be combined with other named filters to produce a more
|
50
|
+
complex query:
|
43
51
|
|
44
52
|
class Post < ActiveRecord::Base
|
45
|
-
|
53
|
+
named_filter(:title_is_monkeys) { with(:title, 'monkeys') }
|
54
|
+
named_filter(:permalink_is_donkeys) { with(:title, 'donkeys') }
|
46
55
|
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
|
|
56
|
-
|
57
|
+
Post.title_is_monkeys.permalink_is_donkeys
|
57
58
|
|
58
|
-
|
59
|
-
|
59
|
+
This example will return all of the posts that meet both animal-related conditions.
|
60
|
+
There is no limit to the number of filters that can be combined, and because record_filter works
|
61
|
+
seamlessly with named scopes, they can also be combined in this way as well.
|
60
62
|
|
61
|
-
|
63
|
+
Named filters can also be customized by taking any number of arguments. The example above can
|
64
|
+
be replicated with the following filter:
|
62
65
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
+
class Post < ActiveRecord::Base
|
67
|
+
named_filter(:with_title_and_permalink) do |title, permalink|
|
68
|
+
with(:title, title)
|
69
|
+
with(:permalink, permalink)
|
70
|
+
end
|
71
|
+
end
|
66
72
|
|
67
|
-
|
73
|
+
Post.with_title_and_permalink('monkeys', 'donkeys')
|
68
74
|
|
69
|
-
|
75
|
+
Named filters can also be called from other named filters and will be invoked on the correct
|
76
|
+
model even if called from a join.
|
70
77
|
|
71
|
-
|
72
|
-
|
78
|
+
class Comment < ActiveRecord::Base
|
79
|
+
named_filter(:offensive) { with(:offensive, true) }
|
80
|
+
end
|
73
81
|
|
74
|
-
|
82
|
+
class Post < ActiveRecord::Base
|
83
|
+
named_filter(:recursive_test) do
|
84
|
+
having(:comments) do
|
85
|
+
offensive
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
75
89
|
|
76
|
-
|
90
|
+
== Specifying Filters
|
77
91
|
|
78
|
-
|
92
|
+
record_filter supports all of SQL query abstractions provided by ActiveRecord, specifically:
|
79
93
|
|
80
|
-
|
81
|
-
|
94
|
+
* Conditions
|
95
|
+
* Boolean operations
|
96
|
+
* Joins
|
97
|
+
* Limits
|
98
|
+
* Offsets
|
99
|
+
* Ordering
|
100
|
+
* Grouping
|
82
101
|
|
83
|
-
|
102
|
+
The following example shows the use of each of these techniques:
|
84
103
|
|
85
|
-
|
104
|
+
Post.filter do
|
105
|
+
any_of do
|
106
|
+
with(:permalink).is_null
|
107
|
+
having(:comments) do
|
108
|
+
with(:offensive, true)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
limit(10, 100)
|
112
|
+
order(:created_at, :desc)
|
113
|
+
group_by(:comments => :offensive)
|
114
|
+
end
|
86
115
|
|
87
|
-
|
116
|
+
=== Conditions
|
88
117
|
|
89
|
-
|
118
|
+
Conditions are specified using the 'with' function, which takes as its first argument
|
119
|
+
the name of the column to restrict. If a second argument is given, it will automatically
|
120
|
+
be used as the value in an equality condition. The 'with' function will return a Restriction
|
121
|
+
object that has methods to specify a number of different conditions and to negate them:
|
90
122
|
|
91
|
-
with(:
|
123
|
+
with(:permalink, 'aardvarks') # :conditions => ['permalink = ?', 'aardvarks']
|
124
|
+
with(:permalink).equal_to('sheep') # :conditions => ['permalink = ?', 'sheep']
|
125
|
+
with(:permalink).not.equal_to('cats') # :conditions => ['permailnk <> ?', 'cats']
|
92
126
|
|
93
|
-
|
127
|
+
with(:permalink, nil) # :conditions => ['permalink IS NULL']
|
128
|
+
with(:permalink).is_null # :conditions => ['permalink IS NULL']
|
129
|
+
with(:permalink, nil).not # :conditions => ['permalink IS NOT NULL']
|
94
130
|
|
95
|
-
|
96
|
-
with(:price).is_null.not # "
|
131
|
+
The following condition types are supported through the Restriction API:
|
97
132
|
|
98
|
-
|
133
|
+
* Equality
|
134
|
+
* Comparisons (> >= < <=)
|
135
|
+
* Between
|
136
|
+
* In
|
137
|
+
* Is null
|
138
|
+
* Like
|
99
139
|
|
100
|
-
|
140
|
+
=== Boolean Operations
|
101
141
|
|
102
|
-
|
142
|
+
Conditions can be combined with boolean operators using the methods all_of, any_of, none_of
|
143
|
+
and not_all_of. These methods take a block where any conditions they contain will be combined
|
144
|
+
using AND, OR and NOT to create the correct condition. The block can also contain any number of
|
145
|
+
joins or other boolean operations. The default operator is all_of.
|
103
146
|
|
104
|
-
|
147
|
+
Post.filter do
|
148
|
+
with(:id, 4)
|
149
|
+
with(:permalink, 'ack')
|
150
|
+
end
|
105
151
|
|
106
|
-
|
152
|
+
:conditions => ['id = ? AND permalink = ?', 4, 'ack']
|
107
153
|
|
108
|
-
|
154
|
+
Post.filter do
|
155
|
+
any_of
|
156
|
+
with(:id, 3)
|
157
|
+
with(:permalink, 'booya')
|
158
|
+
end
|
159
|
+
end
|
109
160
|
|
110
|
-
|
161
|
+
:conditions => ['id = ? OR permalink = ?', 3, 'booya']
|
111
162
|
|
112
|
-
|
163
|
+
Post.filter do
|
164
|
+
none_of
|
165
|
+
with(:id, 2)
|
166
|
+
with(:permalink, 'ouch')
|
167
|
+
end
|
168
|
+
end
|
113
169
|
|
114
|
-
|
170
|
+
:conditions => ['NOT (id = ? OR permalink = ?', 2, 'ouch']
|
115
171
|
|
172
|
+
=== Joins
|
116
173
|
|
117
|
-
|
174
|
+
Joins in record_filter come in two varieties. Using the information in ActiveRecord associations,
|
175
|
+
it is possible to perform most joins easily using the 'having' method, which requires no specification
|
176
|
+
of the columns to use for the join. In cases where an association does not apply, it is also possible
|
177
|
+
to create an explicit join that can include both the columns to combine as well as restrictions on
|
178
|
+
the columns in the join table.
|
118
179
|
|
119
|
-
|
180
|
+
In a filter for a Post model that has_many comments, the following two examples are equivalent:
|
120
181
|
|
121
|
-
having(:comments)
|
182
|
+
having(:comments)
|
122
183
|
|
123
|
-
|
184
|
+
join(Comment, :inner) do
|
185
|
+
on(:id => :post_id)
|
186
|
+
end
|
124
187
|
|
125
|
-
|
188
|
+
With an explicit join, any number of columns can be matched in this way, and both join types
|
189
|
+
accept a block in which any number of conditions, boolean operations, or other joins can be
|
190
|
+
added. Explicit joins also allow conditions to be set on columns of the table being joined:
|
126
191
|
|
127
|
-
|
192
|
+
having(:comments).with(:offensive, true)
|
128
193
|
|
129
194
|
having(:comments) do
|
130
|
-
with(:created_at).
|
131
|
-
having(:author).with(:name, 'Bubba')
|
195
|
+
with(:created_at).greater_than(2.days.ago)
|
132
196
|
end
|
133
197
|
|
134
|
-
|
135
|
-
|
198
|
+
join(Comment, :inner) do
|
199
|
+
on(:id => :commentable_id)
|
200
|
+
on(:commentable_type).equal_to('Post')
|
201
|
+
end
|
136
202
|
|
137
|
-
|
203
|
+
With implicit joins, it is also possible to use a hash as the association name, in which case
|
204
|
+
multiple joins can be created with one statement. If the comment model has_one Author, this
|
205
|
+
example will join both tables and add a condition on the author.
|
138
206
|
|
139
|
-
|
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.
|
207
|
+
having(:comments => :author).with(:name, 'Bob')
|
143
208
|
|
144
|
-
|
145
|
-
join(Comment, :inner, :posts__comments_alias) do
|
146
|
-
on(:id => :commentable_id)
|
147
|
-
on(:commentable_type, 'Post')
|
148
|
-
end
|
149
|
-
end
|
209
|
+
=== Limits and Offsets
|
150
210
|
|
151
|
-
|
211
|
+
These are specified using the 'limit' method, which takes two arguments, the offset and the
|
212
|
+
limit. If only one argument is given, it is assumed to be the limit.
|
152
213
|
|
153
|
-
|
214
|
+
limit(10, 100) # :offset => 10, :limit => 100
|
215
|
+
limit(100) # :offset => 0, :limit => 100
|
154
216
|
|
155
|
-
|
156
|
-
* all_of
|
157
|
-
* none_of
|
158
|
-
* not_all_of
|
217
|
+
=== Ordering
|
159
218
|
|
160
|
-
|
219
|
+
Ordering is done through the 'order' method, which accepts arguments for the column and direction.
|
220
|
+
The column can either be passed as the name of a column in the class that is being filtered or as
|
221
|
+
a hash that represents a path through the joined associations to the correct column. The direction argument
|
222
|
+
should be either :asc or :desc and defaults to :asc if not given. Multiple calls to 'order' are
|
223
|
+
allowed and will be applied in the order in which they were given.
|
161
224
|
|
162
|
-
|
163
|
-
with(:
|
164
|
-
|
225
|
+
Post.filter do
|
226
|
+
having(:comments).with(:offensive, true)
|
227
|
+
order(:created_at, :desc)
|
228
|
+
order(:comments => :id)
|
165
229
|
end
|
166
230
|
|
167
|
-
# :
|
168
|
-
|
169
|
-
=== Limits and ordering
|
231
|
+
# :order => "'posts'.created_at DESC posts__comments.id ASC"
|
170
232
|
|
171
|
-
|
233
|
+
=== Grouping
|
172
234
|
|
173
|
-
|
235
|
+
Grouping is specified with the 'group_by' method, which accepts either the name of a column in the
|
236
|
+
class that is being filtered or a hash that represents a path through the joined associations. If
|
237
|
+
there are multiple calls to 'group_by' they will be combined in the final result, maintaining the
|
238
|
+
order in which they were given.
|
174
239
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
240
|
+
Post.filter do
|
241
|
+
having(:comments).with(:created_at).greater_than(1.hour.ago)
|
242
|
+
group_by(:permalink)
|
243
|
+
group_by(:comments => :offensive)
|
244
|
+
end
|
180
245
|
|
246
|
+
# :group => "'posts'.permalink, posts__comments.offensive'
|
181
247
|
|
182
248
|
== LICENSE:
|
183
249
|
|
184
250
|
(The MIT License)
|
185
251
|
|
186
|
-
Copyright (c) 2008 Mat Brown
|
252
|
+
Copyright (c) 2008 Mat Brown, Aubrey Holland
|
187
253
|
|
188
254
|
Permission is hereby granted, free of charge, to any person obtaining
|
189
255
|
a copy of this software and associated documentation files (the
|
data/VERSION.yml
CHANGED
@@ -159,7 +159,7 @@ module RecordFilter
|
|
159
159
|
|
160
160
|
# Create an implicit join using an association as the target. This method allows you to
|
161
161
|
# easily specify a join without specifying the columns to use by taking any needed data
|
162
|
-
# from the
|
162
|
+
# from the given ActiveRecord association. If provided, the block will be evaluated in
|
163
163
|
# the context of the table that has been joined, so any restrictions or other joins will
|
164
164
|
# be performed using its columns and associations. For example, if a Post has_many comments
|
165
165
|
# then the following code will join to the comments table and restrict the comments based
|
@@ -169,12 +169,13 @@ module RecordFilter
|
|
169
169
|
# with(:created_at).greater_than(3.days.ago)
|
170
170
|
# end
|
171
171
|
# end
|
172
|
-
# If one argument is given, it is assumed to
|
173
|
-
# that will be used for the join and a join type of :inner will be used. If two arguments
|
174
|
-
# provided, the first one is assumed to be the join type, which can be one of :inner, :left or
|
172
|
+
# If one argument is given, it is assumed to represent the name of the association
|
173
|
+
# that will be used for the join and a join type of :inner will be used by default. If two arguments
|
174
|
+
# are provided, the first one is assumed to be the join type, which can be one of :inner, :left or
|
175
175
|
# :right and the second one is the association name. An alias will automatically be created
|
176
176
|
# for the joined table named "#{left_table}__#{association_name}", so in the above example, the
|
177
|
-
# alias would be posts__comments.
|
177
|
+
# alias would be posts__comments. It is also possible to provide a hash as the association
|
178
|
+
# name, in which case a trail of associations can be joined in one statment.
|
178
179
|
#
|
179
180
|
# ==== Parameters
|
180
181
|
# join_type<Symbol>::
|
@@ -53,7 +53,7 @@ module RecordFilter
|
|
53
53
|
# a column in the class that is being filtered. With a hash argument, it is possible
|
54
54
|
# to specify a path to a column in one of the joined tables, as seen above.
|
55
55
|
# direction<Symbol>::
|
56
|
-
# Specifies the direction of the join. Should be either :asc or :desc.
|
56
|
+
# Specifies the direction of the join. Should be either :asc or :desc and defaults to :asc.
|
57
57
|
#
|
58
58
|
# ==== Returns
|
59
59
|
# nil
|
@@ -201,6 +201,21 @@ module RecordFilter
|
|
201
201
|
self
|
202
202
|
end
|
203
203
|
|
204
|
+
# Create a negated IN restriction of the form ['column NOT IN (?)', value]
|
205
|
+
#
|
206
|
+
# ==== Parameters
|
207
|
+
# value::
|
208
|
+
# Either a single item or an array of values to form the inclusion test.
|
209
|
+
#
|
210
|
+
# ==== Returns
|
211
|
+
# Restriction:: self
|
212
|
+
#
|
213
|
+
# @public
|
214
|
+
def not_in(value)
|
215
|
+
@value, @operator, @negated = value, :in, true
|
216
|
+
self
|
217
|
+
end
|
218
|
+
|
204
219
|
# Create a LIKE restriction of the form ['column LIKE ?', value]
|
205
220
|
#
|
206
221
|
# ==== Parameters
|
data/lib/record_filter/filter.rb
CHANGED
@@ -2,7 +2,7 @@ module RecordFilter
|
|
2
2
|
# This class is the value that is returned from the execution of a filter.
|
3
3
|
class Filter
|
4
4
|
|
5
|
-
NON_DELEGATE_METHODS = %w(nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?)
|
5
|
+
NON_DELEGATE_METHODS = %w(debugger nil? send object_id class extend find size count sum average maximum minimum paginate first last empty? any? respond_to?)
|
6
6
|
|
7
7
|
[].methods.each do |m|
|
8
8
|
unless m =~ /^__/ || NON_DELEGATE_METHODS.include?(m.to_s)
|
data/lib/record_filter/query.rb
CHANGED
@@ -21,7 +21,7 @@ module RecordFilter
|
|
21
21
|
conditions = @conjunction.to_conditions
|
22
22
|
params = { :conditions => conditions } if conditions
|
23
23
|
joins = @table.all_joins
|
24
|
-
params[:joins] = joins.map { |join| join.to_sql }
|
24
|
+
params[:joins] = joins.map { |join| join.to_sql } unless joins.empty?
|
25
25
|
if (joins.any? { |j| j.requires_distinct_select? })
|
26
26
|
if count_query
|
27
27
|
params[:select] = "DISTINCT #{@table.model_class.quoted_table_name}.#{@table.model_class.primary_key}"
|
data/lib/record_filter/table.rb
CHANGED
@@ -17,6 +17,7 @@ module RecordFilter
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def join_association(association_name, join_type=nil, options={})
|
20
|
+
association_name = association_name.to_sym
|
20
21
|
@joins_cache[association_name] ||=
|
21
22
|
begin
|
22
23
|
association = @model_class.reflect_on_association(association_name)
|
@@ -115,7 +116,7 @@ module RecordFilter
|
|
115
116
|
protected
|
116
117
|
|
117
118
|
def alias_for_association(association)
|
118
|
-
"#{@aliased ? @table_alias.to_s : @model_class.table_name}__#{association.name}"
|
119
|
+
"#{@aliased ? @table_alias.to_s : @model_class.table_name}__#{association.name.to_s.downcase}"
|
119
120
|
end
|
120
121
|
|
121
122
|
alias_method :alias_for_class, :alias_for_association
|
data/spec/active_record_spec.rb
CHANGED
@@ -18,7 +18,7 @@ describe 'active record options' do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'should create the correct join' do
|
21
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "news_stories" AS blogs__stories ON "blogs".id = blogs__stories.blog_id)
|
21
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "news_stories" AS blogs__stories ON "blogs".id = blogs__stories.blog_id)]
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
@@ -34,7 +34,7 @@ describe 'active record options' do
|
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'should create the correct join' do
|
37
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__special_posts ON "blogs".id = blogs__special_posts.special_blog_id)
|
37
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__special_posts ON "blogs".id = blogs__special_posts.special_blog_id)]
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
@@ -50,7 +50,7 @@ describe 'active record options' do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
it 'should create the correct join' do
|
53
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__special_public_posts ON "blogs".special_id = blogs__special_public_posts.blog_id)
|
53
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__special_public_posts ON "blogs".special_id = blogs__special_public_posts.blog_id)]
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
@@ -66,7 +66,7 @@ describe 'active record options' do
|
|
66
66
|
end
|
67
67
|
|
68
68
|
it 'should create the correct join' do
|
69
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id INNER JOIN "comments" AS blogs__posts__bad_comments ON blogs__posts.id = blogs__posts__bad_comments.post_id)
|
69
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id), %q(INNER JOIN "comments" AS blogs__posts__bad_comments ON blogs__posts.id = blogs__posts__bad_comments.post_id)]
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
@@ -82,22 +82,9 @@ describe 'active record options' do
|
|
82
82
|
end
|
83
83
|
|
84
84
|
it 'should create the correct join' do
|
85
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "features" AS blogs__features ON "blogs".id = blogs__features.blog_id AND (blogs__features.featurable_type = 'Post') INNER JOIN "posts" AS blogs__features__featurable ON blogs__features.featurable_id = blogs__features__featurable.id)
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
# :include
|
90
|
-
# :finder_sql
|
91
|
-
# :counter_sql
|
92
|
-
# :group
|
93
|
-
# :having
|
94
|
-
# :limit
|
95
|
-
# :offset
|
96
|
-
# :select
|
97
|
-
# :uniq
|
98
|
-
# :readonly
|
99
|
-
# :order
|
100
|
-
# :conditions
|
85
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "features" AS blogs__features ON "blogs".id = blogs__features.blog_id AND (blogs__features.featurable_type = 'Post')), %q(INNER JOIN "posts" AS blogs__features__featurable ON blogs__features.featurable_id = blogs__features__featurable.id)]
|
86
|
+
end
|
87
|
+
end
|
101
88
|
end
|
102
89
|
|
103
90
|
describe 'for belongs_to' do
|
@@ -114,7 +101,7 @@ describe 'active record options' do
|
|
114
101
|
end
|
115
102
|
|
116
103
|
it 'should create the correct join' do
|
117
|
-
Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__publication ON "posts".blog_id = posts__publication.id)
|
104
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "blogs" AS posts__publication ON "posts".blog_id = posts__publication.id)]
|
118
105
|
end
|
119
106
|
end
|
120
107
|
|
@@ -130,15 +117,81 @@ describe 'active record options' do
|
|
130
117
|
end
|
131
118
|
|
132
119
|
it 'should create the correct join' do
|
133
|
-
Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__special_blog ON "posts".special_blog_id = posts__special_blog.id)
|
120
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "blogs" AS posts__special_blog ON "posts".special_blog_id = posts__special_blog.id)]
|
134
121
|
end
|
135
122
|
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe 'working with named scopes' do
|
126
|
+
before do
|
127
|
+
@blog = Class.new(Blog)
|
128
|
+
@blog.named_scope :with_high_id, { :conditions => ['id > 100'] }
|
129
|
+
@blog.named_filter(:published) { with(:published, true) }
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should concatenate the filter with the scope correctly' do
|
133
|
+
@blog.with_high_id.published.inspect
|
134
|
+
@blog.last_find[:conditions].should == %q(("blogs".published = 't') AND (id > 100))
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should concatenate correctly when called in the other order' do
|
138
|
+
@blog.published.with_high_id.inspect
|
139
|
+
@blog.last_find[:conditions].should == %q((id > 100) AND ("blogs".published = 't'))
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe 'working with named scopes when there are a number of joins' do
|
144
|
+
before do
|
145
|
+
@blog = Class.new(Blog)
|
146
|
+
@blog.named_scope :ads_with_sale, { :joins => :ads, :conditions => ["'ads'.content LIKE ?", '%sale%'] }
|
147
|
+
@blog.named_filter(:with_permalinked_posts) { having(:posts).with(:permalink).is_not_null }
|
148
|
+
@blog.named_filter(:with_offensive_comments) { having(:comments).with(:offensive, true) }
|
149
|
+
@blog.with_permalinked_posts.ads_with_sale.with_offensive_comments.inspect
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'should concatenate the conditions correctly' do
|
153
|
+
@blog.last_find[:conditions].should == %q((blogs__posts__comments.offensive = 't') AND (('ads'.content LIKE '%sale%') AND (blogs__posts.permalink IS NOT NULL)))
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'should concatenate the joins correctly and not throw away my joins like AR usually does' do
|
157
|
+
@blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id), %q(INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id), %q(INNER JOIN "ads" ON ads.blog_id = blogs.id)]
|
158
|
+
end
|
159
|
+
end
|
136
160
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
161
|
+
describe 'working with named scopes that join to the same table' do
|
162
|
+
before do
|
163
|
+
@blog = Class.new(Blog)
|
164
|
+
@blog.named_scope :with_crazy_post_permalinks, { :joins => :posts, :conditions => ["'posts'.permalink = ?", 'crazy'] }
|
165
|
+
@blog.named_filter(:with_empty_permalinks) { having(:posts).with(:permalink, nil) }
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'should concatenate the conditions correctly' do
|
169
|
+
@blog.with_crazy_post_permalinks.with_empty_permalinks.inspect
|
170
|
+
@blog.last_find[:conditions].should == %q((blogs__posts.permalink IS NULL) AND ('posts'.permalink = 'crazy'))
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'should concatenate the joins correctly' do
|
174
|
+
@blog.with_crazy_post_permalinks.with_empty_permalinks.inspect
|
175
|
+
@blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id), %q(INNER JOIN "posts" ON posts.blog_id = blogs.id)]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe 'working with default scopes' do
|
180
|
+
describe 'with a simple filter' do
|
181
|
+
before do
|
182
|
+
Article.filter do
|
183
|
+
with(:contents, 'something')
|
184
|
+
end.inspect
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'should use the correct order' do
|
188
|
+
Article.last_find[:order].should == %q(created_at DESC)
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'should use the correct conditions' do
|
192
|
+
pending 'currently the IS NULL condition is added twice.'
|
193
|
+
Article.last_find[:conditions].should == %q((("articles"."created_at" IS NULL) AND ("articles".contents = 'something')))
|
194
|
+
end
|
195
|
+
end
|
143
196
|
end
|
144
197
|
end
|
data/spec/explicit_join_spec.rb
CHANGED
@@ -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 OUTER 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 OUTER 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 OUTER JOIN "features" AS
|
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 OUTER JOIN "features" AS
|
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
|
|
@@ -101,7 +101,7 @@ describe 'explicit joins' do
|
|
101
101
|
end
|
102
102
|
|
103
103
|
it 'should produce the correct join' do
|
104
|
-
@blog.last_find[:joins].should == %q(INNER JOIN "ads" AS blogs__ads ON "blogs".id = blogs__ads.blog_id LEFT OUTER JOIN "posts" AS
|
104
|
+
@blog.last_find[:joins].should == [%q(INNER JOIN "ads" AS blogs__ads ON "blogs".id = blogs__ads.blog_id), %q(LEFT OUTER JOIN "posts" AS blogs__post ON "blogs".id = blogs__post.blog_id), %q(INNER JOIN "comments" AS blogs__post__comment ON blogs__post.id = blogs__post__comment.post_id AND (blogs__post__comment.offensive = 't'))]
|
105
105
|
end
|
106
106
|
end
|
107
107
|
end
|
data/spec/implicit_join_spec.rb
CHANGED
@@ -14,7 +14,7 @@ describe 'implicit joins' do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'should add correct join' do
|
17
|
-
Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)
|
17
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)]
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'should query against condition on join table' do
|
@@ -24,7 +24,7 @@ describe 'implicit joins' do
|
|
24
24
|
|
25
25
|
shared_examples_for 'multiple conditions on single join' do
|
26
26
|
it 'should add join once' do
|
27
|
-
Post.last_find[:joins].should == %q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)
|
27
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "blogs" AS posts__blog ON "posts".blog_id = posts__blog.id)]
|
28
28
|
end
|
29
29
|
|
30
30
|
it 'should query against conditions on join table' do
|
@@ -65,7 +65,7 @@ describe 'implicit joins' do
|
|
65
65
|
end
|
66
66
|
|
67
67
|
it 'should add correct join' do
|
68
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id)
|
68
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id)]
|
69
69
|
end
|
70
70
|
|
71
71
|
it 'should query against condition on join table' do
|
@@ -84,8 +84,8 @@ describe 'implicit joins' do
|
|
84
84
|
end
|
85
85
|
|
86
86
|
it 'should add both joins' do
|
87
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id
|
88
|
-
|
87
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id),
|
88
|
+
%q(INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)]
|
89
89
|
end
|
90
90
|
|
91
91
|
it 'should query against both conditions' do
|
@@ -103,8 +103,8 @@ describe 'implicit joins' do
|
|
103
103
|
end
|
104
104
|
|
105
105
|
it 'should add both joins' do
|
106
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id
|
107
|
-
|
106
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id),
|
107
|
+
%q(INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)]
|
108
108
|
end
|
109
109
|
|
110
110
|
it 'should query against both conditions' do
|
@@ -120,7 +120,7 @@ describe 'implicit joins' do
|
|
120
120
|
end
|
121
121
|
|
122
122
|
it 'should add correct join' do
|
123
|
-
Post.last_find[:joins].should == %q(INNER JOIN "photos" AS posts__photo ON "posts".id = posts__photo.post_id)
|
123
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "photos" AS posts__photo ON "posts".id = posts__photo.post_id)]
|
124
124
|
end
|
125
125
|
|
126
126
|
it 'should query against condition on join table' do
|
@@ -136,8 +136,8 @@ describe 'implicit joins' do
|
|
136
136
|
end
|
137
137
|
|
138
138
|
it 'should add correct join' do
|
139
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id
|
140
|
-
|
139
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id),
|
140
|
+
%q(INNER JOIN "photos" AS blogs__posts__photo ON blogs__posts.id = blogs__posts__photo.post_id)]
|
141
141
|
end
|
142
142
|
|
143
143
|
it 'should query against condition on join table' do
|
@@ -153,8 +153,8 @@ describe 'implicit joins' do
|
|
153
153
|
end
|
154
154
|
|
155
155
|
it 'should add correct join' do
|
156
|
-
Post.last_find[:joins].should == %q(INNER JOIN "posts_tags" AS __posts__tags ON "posts".id = __posts__tags.post_id
|
157
|
-
|
156
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "posts_tags" AS __posts__tags ON "posts".id = __posts__tags.post_id),
|
157
|
+
%q(INNER JOIN "tags" AS posts__tags ON __posts__tags.tag_id = posts__tags.id)]
|
158
158
|
end
|
159
159
|
end
|
160
160
|
|
@@ -235,7 +235,7 @@ describe 'implicit joins' do
|
|
235
235
|
end
|
236
236
|
|
237
237
|
it 'should create the correct join' do
|
238
|
-
Blog.last_find[:joins].should == %q(LEFT OUTER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id)
|
238
|
+
Blog.last_find[:joins].should == [%q(LEFT OUTER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id)]
|
239
239
|
end
|
240
240
|
end
|
241
241
|
|
@@ -253,7 +253,7 @@ describe 'implicit joins' do
|
|
253
253
|
end
|
254
254
|
|
255
255
|
it 'should create the correct join' do
|
256
|
-
Blog.last_find[:joins].should == %q(LEFT OUTER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id LEFT OUTER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)
|
256
|
+
Blog.last_find[:joins].should == [%q(LEFT OUTER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id), %q(LEFT OUTER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)]
|
257
257
|
end
|
258
258
|
end
|
259
259
|
|
@@ -271,7 +271,7 @@ describe 'implicit joins' do
|
|
271
271
|
end
|
272
272
|
|
273
273
|
it 'should create the correct join' do
|
274
|
-
PublicPost.last_find[:joins].should == %q(INNER JOIN "reviews" AS posts__reviews ON "posts".id = posts__reviews.reviewable_id AND (posts__reviews.reviewable_type = 'Post'))
|
274
|
+
PublicPost.last_find[:joins].should == [%q(INNER JOIN "reviews" AS posts__reviews ON "posts".id = posts__reviews.reviewable_id AND (posts__reviews.reviewable_type = 'Post'))]
|
275
275
|
end
|
276
276
|
end
|
277
277
|
|
@@ -289,7 +289,7 @@ describe 'implicit joins' do
|
|
289
289
|
end
|
290
290
|
|
291
291
|
it 'should create the correct join' do
|
292
|
-
Blog.last_find[:joins].should == %q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)
|
292
|
+
Blog.last_find[:joins].should == [%q(INNER JOIN "posts" AS blogs__posts ON "blogs".id = blogs__posts.blog_id), %q(INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)]
|
293
293
|
end
|
294
294
|
end
|
295
295
|
|
@@ -307,7 +307,26 @@ describe 'implicit joins' do
|
|
307
307
|
end
|
308
308
|
|
309
309
|
it 'should create the correct join' do
|
310
|
-
Post.last_find[:joins].should == %q(INNER JOIN "authors" AS posts__author ON "posts".id = posts__author.post_id INNER JOIN "users" AS posts__author__user ON posts__author.user_id = posts__author__user.id)
|
310
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "authors" AS posts__author ON "posts".id = posts__author.post_id), %q(INNER JOIN "users" AS posts__author__user ON posts__author.user_id = posts__author__user.id)]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe 'passing strings instead of symbols' do
|
315
|
+
before do
|
316
|
+
Post.filter do
|
317
|
+
having('comments') do
|
318
|
+
with('offensive', true)
|
319
|
+
end
|
320
|
+
with('id').gte(12)
|
321
|
+
end.inspect
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'should create the correct condition' do
|
325
|
+
Post.last_find[:conditions].should == [%q((posts__comments.offensive = ?) AND ("posts".id >= ?)), true, 12]
|
326
|
+
end
|
327
|
+
|
328
|
+
it 'should create the correct join' do
|
329
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)]
|
311
330
|
end
|
312
331
|
end
|
313
332
|
end
|
data/spec/models.rb
CHANGED
@@ -4,6 +4,12 @@ class Ad < ActiveRecord::Base
|
|
4
4
|
end
|
5
5
|
|
6
6
|
|
7
|
+
class Article < ActiveRecord::Base
|
8
|
+
extend TestModel
|
9
|
+
default_scope :order => 'created_at DESC', :conditions => { :created_at => nil }
|
10
|
+
end
|
11
|
+
|
12
|
+
|
7
13
|
class Author < ActiveRecord::Base
|
8
14
|
extend TestModel
|
9
15
|
belongs_to :user
|
@@ -24,6 +30,7 @@ class Blog < ActiveRecord::Base
|
|
24
30
|
has_many :features
|
25
31
|
has_many :featured_posts, :through => :features, :source => :featurable, :source_type => 'Post'
|
26
32
|
has_many :posts_with_comments, :class_name => 'Post', :include => :comments
|
33
|
+
has_many :articles
|
27
34
|
end
|
28
35
|
|
29
36
|
|
data/spec/named_filter_spec.rb
CHANGED
@@ -119,7 +119,7 @@ describe 'named filters' do
|
|
119
119
|
having(:comments).offensive_or_not(true)
|
120
120
|
end.inspect
|
121
121
|
Post.last_find[:conditions].should == [%q(posts__comments.offensive = ?), true]
|
122
|
-
Post.last_find[:joins].should == %q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)
|
122
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)]
|
123
123
|
end
|
124
124
|
|
125
125
|
it 'should work correctly with the named filter called within the having block' do
|
@@ -129,7 +129,7 @@ describe 'named filters' do
|
|
129
129
|
end
|
130
130
|
end.inspect
|
131
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)
|
132
|
+
Post.last_find[:joins].should == [%q(INNER JOIN "comments" AS posts__comments ON "posts".id = posts__comments.post_id)]
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
@@ -181,7 +181,7 @@ describe 'named filters' do
|
|
181
181
|
base = @post.for_blog(1)
|
182
182
|
base.with_offensive_comments
|
183
183
|
base.inspect
|
184
|
-
@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)]
|
185
185
|
end
|
186
186
|
|
187
187
|
it 'should not change an original filter when reusing it' do
|
@@ -250,7 +250,7 @@ describe 'named filters' do
|
|
250
250
|
|
251
251
|
it 'compile the joins correctly' do
|
252
252
|
@blog.with_offensive_comments.with_ads_with_content('ack').inspect
|
253
|
-
@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)]
|
253
|
+
@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), %q(INNER JOIN "comments" AS blogs__posts__comments ON blogs__posts.id = blogs__posts__comments.post_id)]
|
254
254
|
end
|
255
255
|
end
|
256
256
|
|
data/spec/restrictions_spec.rb
CHANGED
@@ -61,6 +61,13 @@ describe 'RecordFilter restrictions' do
|
|
61
61
|
Post.last_find.should == { :conditions => [%q{"posts".blog_id NOT IN (?)}, [1, 3, 5]] }
|
62
62
|
end
|
63
63
|
|
64
|
+
it 'should work correctly for NOT IN' do
|
65
|
+
Post.filter do
|
66
|
+
with(:blog_id).not_in [1, 3, 5]
|
67
|
+
end.inspect
|
68
|
+
Post.last_find.should == { :conditions => [%q{"posts".blog_id NOT IN (?)}, [1, 3, 5]] }
|
69
|
+
end
|
70
|
+
|
64
71
|
it 'should do the right thing for IN filters with empty arrays' do
|
65
72
|
Post.filter do
|
66
73
|
with(:blog_id).in([])
|
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.
|
4
|
+
version: 0.9.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-05-
|
13
|
+
date: 2009-05-06 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies: []
|
16
16
|
|