meta_where 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/.gitmodules +6 -0
- data/CHANGELOG +25 -0
- data/Gemfile +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +274 -0
- data/Rakefile +68 -0
- data/VERSION +1 -0
- data/lib/core_ext/hash.rb +17 -0
- data/lib/core_ext/symbol.rb +31 -0
- data/lib/core_ext/symbol_operators.rb +44 -0
- data/lib/meta_where/builder.rb +84 -0
- data/lib/meta_where/column.rb +53 -0
- data/lib/meta_where/compound.rb +30 -0
- data/lib/meta_where/condition.rb +51 -0
- data/lib/meta_where/join_dependency.rb +48 -0
- data/lib/meta_where/relation.rb +270 -0
- data/lib/meta_where/utility.rb +27 -0
- data/lib/meta_where.rb +27 -0
- data/meta_where.gemspec +105 -0
- data/test/fixtures/companies.yml +17 -0
- data/test/fixtures/company.rb +7 -0
- data/test/fixtures/data_type.rb +3 -0
- data/test/fixtures/data_types.yml +15 -0
- data/test/fixtures/developer.rb +5 -0
- data/test/fixtures/developers.yml +55 -0
- data/test/fixtures/developers_projects.yml +25 -0
- data/test/fixtures/note.rb +3 -0
- data/test/fixtures/notes.yml +79 -0
- data/test/fixtures/people.yml +14 -0
- data/test/fixtures/person.rb +4 -0
- data/test/fixtures/project.rb +4 -0
- data/test/fixtures/projects.yml +24 -0
- data/test/fixtures/schema.rb +52 -0
- data/test/helper.rb +33 -0
- data/test/test_relations.rb +225 -0
- metadata +169 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.gitmodules
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Changes since 0.5.2 (2010-07-09):
|
2
|
+
* Removed autojoin. Inner joins have side-effects. Newbies are the ones who aremost
|
3
|
+
likely to use autojoin, and they're also the ones least likely to understand why
|
4
|
+
certain rows stop being returned. Better to force a small learning curve. I've
|
5
|
+
decided against leaving it in with deprecation since Rails 3 isn't final yet.
|
6
|
+
Decided to get it out before it's "too late" to do so without impacting code running
|
7
|
+
on a stable version of Rails.
|
8
|
+
* Refactored build_arel to more closely mirror the refactoring that's been going on
|
9
|
+
to the method in Rails edge.
|
10
|
+
* Improved merge functonality. It shouldn't have ever required someone to do an
|
11
|
+
autojoin to begin with. Now it just works. If you're merging two relations, you
|
12
|
+
should expect to only get results that have a match on both sides.
|
13
|
+
|
14
|
+
Changes since 0.5.1 (2010-06-22):
|
15
|
+
* Added debug_sql method to Relations. Lets you see the actual SQL that
|
16
|
+
will be run against the database without having to resort to the
|
17
|
+
development.log. Differs from to_sql because that doesn't (and can't,
|
18
|
+
by necessity) handle eager loading.
|
19
|
+
|
20
|
+
Changes since 0.5.0 (2010-06-08):
|
21
|
+
* Track Emilio Tagua's performance enhancements in build_arel from edge.
|
22
|
+
|
23
|
+
Changes since 0.3.3 (2010-04-30):
|
24
|
+
* Lots. See http://metautonomo.us/2010/06/08/metasearch-and-metawhere-0-5-0-released/
|
25
|
+
for a summary.
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ernie Miller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
= MetaWhere
|
2
|
+
|
3
|
+
MetaWhere puts the power of Arel predications (comparison methods) in your ActiveRecord
|
4
|
+
condition hashes.
|
5
|
+
|
6
|
+
== Why?
|
7
|
+
|
8
|
+
<b>I hate SQL fragments in Rails code.</b> Resorting to <tt>where('name LIKE ?', '%something%')</tt> is an admission of defeat. It says, "I concede to allow your rigid, 1970's-era syntax into my elegant Ruby world of object oriented goodness." While sometimes such concessions are necessary, they should <em>always</em> be a last resort, because <b>once you move away from an abstract representation of your intended query, your query becomes more brittle.</b> You're now reduced to hacking about with regular expressions, string scans, and the occasional deferred variable interpolation trick (like '#{quoted_table_name}') in order to maintain some semblance of flexibility.
|
9
|
+
|
10
|
+
It isn't that I hate SQL (much). I'm perfectly capable of constructing complex queries from scratch, and did more than my fair share before coming to the Rails world. It's that I hate the juxtaposition of SQL against Ruby. It's like seeing your arthritic grandfather hand in hand with some hot, flexible, yoga instructor. Good for him, but sooner or later something's going to get broken. It's like a sentence which, tanpa alasan, perubahan ke bahasa lain, then back again ("for no reason, changes to another language" -- with thanks to Google Translate, and apologies to native speakers of Indonesian). It just feels <em>wrong</em>. It breaks the spell -- the "magic" that adds to programmer joy, and <em>for no good reason</em>.
|
11
|
+
|
12
|
+
MetaWhere is a gem that sets out to right that wrong, and give tranquility to you, the Rails coder.
|
13
|
+
|
14
|
+
== Getting started
|
15
|
+
|
16
|
+
In your Gemfile:
|
17
|
+
|
18
|
+
gem "meta_where" # Last officially released gem
|
19
|
+
# gem "meta_where", :git => "git://github.com/ernie/meta_where.git" # Track git repo
|
20
|
+
|
21
|
+
or, to install as a plugin:
|
22
|
+
|
23
|
+
rails plugin install git://github.com/ernie/meta_where.git
|
24
|
+
|
25
|
+
== Example usage
|
26
|
+
|
27
|
+
=== Where
|
28
|
+
You can use MetaWhere in your usual method chain:
|
29
|
+
|
30
|
+
Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)
|
31
|
+
=> SELECT "articles".* FROM "articles" WHERE ("articles"."title" LIKE 'Hello%')
|
32
|
+
AND ("articles"."created_at" > '2010-04-12 18:39:32.592087')
|
33
|
+
|
34
|
+
=== Find condition hash
|
35
|
+
You can also use similar syntax in a conditions hash supplied to ActiveRecord::Base#find:
|
36
|
+
|
37
|
+
Article.find(:all,
|
38
|
+
:conditions => {
|
39
|
+
:title.matches => 'Hello%',
|
40
|
+
:created_at.gt => 3.days.ago
|
41
|
+
}
|
42
|
+
)
|
43
|
+
|
44
|
+
=== Scopes
|
45
|
+
They also work in named scopes as you would expect.
|
46
|
+
|
47
|
+
class Article
|
48
|
+
scope :recent, lambda {|v| where(:created_at.gt => v.days.ago)}
|
49
|
+
end
|
50
|
+
|
51
|
+
Article.recent(14).to_sql
|
52
|
+
=> SELECT "articles".* FROM "articles"
|
53
|
+
WHERE ("articles"."created_at" > '2010-04-01 18:54:37.030951')
|
54
|
+
|
55
|
+
=== Operators (Optionally)
|
56
|
+
Additionally, you can use certain operators as shorthand for certain Arel predication methods.
|
57
|
+
|
58
|
+
These are disabled by default, but can be enabled by calling MetaWhere.operator_overload! during
|
59
|
+
your app's initialization process.
|
60
|
+
|
61
|
+
These are experimental at this point and subject to change. Keep in mind that if you don't want
|
62
|
+
to enclose other conditions in {}, you should place operator conditions before any hash conditions.
|
63
|
+
|
64
|
+
Article.where(:created_at > 100.days.ago, :title =~ 'Hi%').to_sql
|
65
|
+
=> SELECT "articles".* FROM "articles"
|
66
|
+
WHERE ("articles"."created_at" > '2010-01-05 20:11:44.997446')
|
67
|
+
AND ("articles"."title" LIKE 'Hi%')
|
68
|
+
|
69
|
+
Operators are:
|
70
|
+
|
71
|
+
* [] (equal)
|
72
|
+
* ^ (not equal)
|
73
|
+
* + (in array/range)
|
74
|
+
* - (not in array/range)
|
75
|
+
* =~ (matching -- not a regexp but a string for SQL LIKE)
|
76
|
+
* !~ (not matching, only available under Ruby 1.9)
|
77
|
+
* > (greater than)
|
78
|
+
* >= (greater than or equal to)
|
79
|
+
* < (less than)
|
80
|
+
* <= (less than or equal to)
|
81
|
+
|
82
|
+
=== Compounds
|
83
|
+
You can use the & and | operators to perform ands and ors within your queries.
|
84
|
+
|
85
|
+
<b>With operators:</b>
|
86
|
+
Article.where((:title =~ 'Hello%') | (:title =~ 'Goodbye%')).to_sql
|
87
|
+
=> SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%'
|
88
|
+
OR "articles"."title" LIKE 'Goodbye%'))
|
89
|
+
|
90
|
+
That's kind of annoying, since operator precedence is such that you have to put
|
91
|
+
parentheses around everything. So MetaWhere also supports a substitution-inspired
|
92
|
+
(String#%) syntax.
|
93
|
+
|
94
|
+
<b>With "substitutions":</b>
|
95
|
+
Article.where(:title.matches % 'Hello%' | :title.matches % 'Goodbye%').to_sql
|
96
|
+
=> SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%'
|
97
|
+
OR "articles"."title" LIKE 'Goodbye%'))
|
98
|
+
|
99
|
+
<b>With hashes:</b>
|
100
|
+
Article.where(
|
101
|
+
{:created_at.lt => Time.now} & {:created_at.gt => 1.year.ago}
|
102
|
+
).to_sql
|
103
|
+
=> SELECT "articles".* FROM "articles" WHERE
|
104
|
+
((("articles"."created_at" < '2010-04-16 00:26:30.629467')
|
105
|
+
AND ("articles"."created_at" > '2009-04-16 00:26:30.629526')))
|
106
|
+
|
107
|
+
<b>With both hashes and substitutions:</b>
|
108
|
+
Article.where(
|
109
|
+
:title.matches % 'Hello%' &
|
110
|
+
{:created_at.lt => Time.now, :created_at.gt => 1.year.ago}
|
111
|
+
).to_sql
|
112
|
+
=> SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%' AND
|
113
|
+
("articles"."created_at" < '2010-04-16 01:04:38.023615' AND
|
114
|
+
"articles"."created_at" > '2009-04-16 01:04:38.023720')))
|
115
|
+
|
116
|
+
<b>With insanity... errr, complex combinations(*):</b>
|
117
|
+
|
118
|
+
Article.joins(:comments).where(
|
119
|
+
{:title => 'Greetings'} |
|
120
|
+
(
|
121
|
+
(
|
122
|
+
:created_at.gt % 21.days.ago &
|
123
|
+
:created_at.lt % 7.days.ago
|
124
|
+
) &
|
125
|
+
:body.matches % '%from the past%'
|
126
|
+
) &
|
127
|
+
{:comments => [:body =~ '%first post!%']}
|
128
|
+
).to_sql
|
129
|
+
=> SELECT "articles".*
|
130
|
+
FROM "articles"
|
131
|
+
INNER JOIN "comments"
|
132
|
+
ON "comments"."article_id" = "articles"."id"
|
133
|
+
WHERE
|
134
|
+
((
|
135
|
+
"articles"."title" = 'Greetings'
|
136
|
+
OR
|
137
|
+
(
|
138
|
+
(
|
139
|
+
(
|
140
|
+
"articles"."created_at" > '2010-03-26 05:57:57.924258'
|
141
|
+
AND "articles"."created_at" < '2010-04-09 05:57:57.924984'
|
142
|
+
)
|
143
|
+
AND "articles"."body" LIKE '%from the past%'
|
144
|
+
)
|
145
|
+
AND "comments"."body" LIKE '%first post!%'
|
146
|
+
)
|
147
|
+
))
|
148
|
+
|
149
|
+
(*) Formatting added for clarity. I said you could do this, not that you should. :)
|
150
|
+
|
151
|
+
=== But wait, there's more!
|
152
|
+
|
153
|
+
== Intelligent hash condition mapping
|
154
|
+
This is one of those things I hope you find so intuitive that you forget it wasn't
|
155
|
+
built in already.
|
156
|
+
|
157
|
+
PredicateBuilder (the part of ActiveRecord responsible for turning your conditions
|
158
|
+
hash into a valid SQL query) will allow you to nest conditions in order to specify a
|
159
|
+
table that the conditions apply to:
|
160
|
+
|
161
|
+
Article.joins(:comments).where(:comments => {:body => 'hey'}).to_sql
|
162
|
+
=> SELECT "articles".* FROM "articles" INNER JOIN "comments"
|
163
|
+
ON "comments"."article_id" = "articles"."id"
|
164
|
+
WHERE ("comments"."body" = 'hey')
|
165
|
+
|
166
|
+
This feels pretty magical at first, but the magic quickly breaks down. Consider an
|
167
|
+
association named <tt>:other_comments</tt> that is just a condition against comments:
|
168
|
+
|
169
|
+
Article.joins(:other_comments).where(:other_comments => {:body => 'hey'}).to_sql
|
170
|
+
=> ActiveRecord::StatementInvalid: No attribute named `body` exists for table `other_comments`
|
171
|
+
|
172
|
+
Ick. This is because the query is being created against tables, and not against associations.
|
173
|
+
You'd need to do...
|
174
|
+
|
175
|
+
Article.joins(:other_comments).where(:comments => {:body => 'hey'})
|
176
|
+
|
177
|
+
...instead.
|
178
|
+
|
179
|
+
With MetaWhere:
|
180
|
+
|
181
|
+
Article.joins(:other_comments).where(:other_comments => {:body => 'hey'}).to_sql
|
182
|
+
=> SELECT "articles".* FROM "articles" INNER JOIN "comments"
|
183
|
+
ON "comments"."article_id" = "articles"."id" WHERE (("comments"."body" = 'hey'))
|
184
|
+
|
185
|
+
The general idea is that if an association with the name provided exists, MetaWhere::Builder
|
186
|
+
will build the conditions against that association's table as it's been aliased, before falling
|
187
|
+
back to assuming you're specifying a table by name. It also handles nested associations:
|
188
|
+
|
189
|
+
Article.where(
|
190
|
+
:comments => {
|
191
|
+
:body => 'yo',
|
192
|
+
:moderations => [:value < 0]
|
193
|
+
},
|
194
|
+
:other_comments => {:body => 'hey'}
|
195
|
+
).joins(
|
196
|
+
{:comments => :moderations},
|
197
|
+
:other_comments
|
198
|
+
).to_sql
|
199
|
+
=> SELECT "articles".* FROM "articles"
|
200
|
+
INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
|
201
|
+
INNER JOIN "moderations" ON "moderations"."comment_id" = "comments"."id"
|
202
|
+
INNER JOIN "comments" "other_comments_articles"
|
203
|
+
ON "other_comments_articles"."article_id" = "articles"."id"
|
204
|
+
WHERE (("comments"."body" = 'yo' AND "moderations"."value" < 0
|
205
|
+
AND "other_comments_articles"."body" = 'hey'))
|
206
|
+
|
207
|
+
Contrived example, I'll admit -- but I'll bet you can think of some uses for this.
|
208
|
+
|
209
|
+
== Enhanced relation merges
|
210
|
+
|
211
|
+
One of the changes MetaWhere makes to ActiveRecord is to delay "compiling" the
|
212
|
+
where_values into actual Arel predicates until absolutely necessary. This allows
|
213
|
+
for greater flexibility and last-second inference of associations/joins from any
|
214
|
+
hashes supplied. A drawback of this method is that when merging relations, ActiveRecord
|
215
|
+
just assumes that the values being merged are already firmed up against a specific table
|
216
|
+
name and can just be thrown together. This isn't the case with MetaWhere, and would
|
217
|
+
cause unexpected failures when merging. However, MetaWhere improves on the default
|
218
|
+
ActiveRecord merge functionality in two ways. First, when called with 1 parameter,
|
219
|
+
(as is always the case when using the & alias) MetaWhere will try to determine if
|
220
|
+
an association exists between the two models involved in the merge. If it does, the
|
221
|
+
association name will be used to construct criteria.
|
222
|
+
|
223
|
+
Additionally, to cover times when detection is impossible, or the first detected
|
224
|
+
association isn't the one you wanted, you can call merge with a second parameter,
|
225
|
+
specifying the association to be used during the merge.
|
226
|
+
|
227
|
+
This merge functionality allows you to do this...
|
228
|
+
|
229
|
+
(Comment.where(:id < 7) & Article.where(:title =~ '%blah%')).to_sql
|
230
|
+
=> SELECT "comments".* FROM "comments" INNER JOIN "articles"
|
231
|
+
ON "articles"."id" = "comments"."article_id"
|
232
|
+
WHERE ("comments"."id" < 7) AND ("articles"."title" LIKE '%blah%')"
|
233
|
+
|
234
|
+
...or this...
|
235
|
+
|
236
|
+
Article.where(:id < 2).merge(Comment.where(:id < 7), :lame_comments).to_sql
|
237
|
+
=> "SELECT "articles".* FROM "articles"
|
238
|
+
INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
|
239
|
+
AND "comments"."body" = 'first post!'
|
240
|
+
WHERE ("articles"."id" < 2) AND ("comments"."id" < 7)"
|
241
|
+
|
242
|
+
== Enhanced order clauses
|
243
|
+
|
244
|
+
If you are used to doing stuff like <tt>Article.order('title asc')</tt>, that will still
|
245
|
+
work as you expect. However, if you pass symbols or arrays in to the <tt>order</tt> method,
|
246
|
+
you can take advantage of intelligent association detection (as with "Intelligent hash condition
|
247
|
+
mapping," above) and also some convenience methods for ascending and descending sorts.
|
248
|
+
|
249
|
+
Article.order(
|
250
|
+
:title.desc,
|
251
|
+
:comments => [:created_at.asc, :updated_at]
|
252
|
+
).joins(:comments).to_sql
|
253
|
+
=> SELECT "articles".* FROM "articles"
|
254
|
+
INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
|
255
|
+
ORDER BY "articles"."title" DESC,
|
256
|
+
"comments"."created_at" ASC, "comments"."updated_at"
|
257
|
+
|
258
|
+
== Thanks
|
259
|
+
A huge thank you goes to Pratik Naik (lifo) for a dicussion on #rails-contrib about a patch
|
260
|
+
I'd submitted, and his take on a DSL for query conditions, which was the inspiration for this
|
261
|
+
gem.
|
262
|
+
|
263
|
+
== Contributions
|
264
|
+
|
265
|
+
There are several ways you can help MetaWhere continue to improve.
|
266
|
+
|
267
|
+
* Use MetaWhere in your real-world projects and {submit bug reports or feature suggestions}[http://metautonomous.lighthouseapp.com/projects/53011-metawhere/].
|
268
|
+
* Better yet, if you’re so inclined, fix the issue yourself and submit a patch! Or you can {fork the project on GitHub}[http://github.com/ernie/meta_where] and send me a pull request (please include tests!)
|
269
|
+
* If you like MetaWhere, spread the word. More users == more eyes on code == more bugs getting found == more bugs getting fixed (hopefully!)
|
270
|
+
* Lastly, if MetaWhere has saved you hours of development time on your latest Rails gig, and you’re feeling magnanimous, please consider {making a donation}[http://pledgie.com/campaigns/10096] to the project. I have spent hours of my personal time coding and supporting MetaWhere, and your donation would go a great way toward justifying that time spent to my loving wife. :)
|
271
|
+
|
272
|
+
== Copyright
|
273
|
+
|
274
|
+
Copyright (c) 2010 {Ernie Miller}[http://metautonomo.us]. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "meta_where"
|
8
|
+
gem.summary = %Q{Add a dash of Arel awesomeness to your condition hashes.}
|
9
|
+
gem.description = %Q{
|
10
|
+
MetaWhere offers the ability to call any Arel predicate methods
|
11
|
+
(with a few convenient aliases) on your Model's attributes instead
|
12
|
+
of the ones normally offered by ActiveRecord's hash parameters. It also
|
13
|
+
adds convenient syntax for order clauses, smarter mapping of nested hash
|
14
|
+
conditions, and a debug_sql method to see the real SQL your code is
|
15
|
+
generating without running it against the database. If you like the new
|
16
|
+
AR 3.0 query interface, you'll love it with MetaWhere.
|
17
|
+
}
|
18
|
+
gem.email = "ernie@metautonomo.us"
|
19
|
+
gem.homepage = "http://metautonomo.us/projects/metawhere/"
|
20
|
+
gem.authors = ["Ernie Miller"]
|
21
|
+
gem.add_development_dependency "shoulda"
|
22
|
+
gem.add_dependency "activerecord", "~> 3.0.0.rc2"
|
23
|
+
gem.add_dependency "activesupport", "~> 3.0.0.rc2"
|
24
|
+
gem.add_dependency "arel", "~> 1.0.0.rc1"
|
25
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
26
|
+
end
|
27
|
+
Jeweler::GemcutterTasks.new
|
28
|
+
rescue LoadError
|
29
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
30
|
+
end
|
31
|
+
|
32
|
+
require 'rake/testtask'
|
33
|
+
Rake::TestTask.new(:test) do |test|
|
34
|
+
test.libs << 'lib' << 'test'
|
35
|
+
test.libs << 'vendor/rails/activerecord/lib'
|
36
|
+
test.libs << 'vendor/rails/activesupport/lib'
|
37
|
+
test.libs << 'vendor/arel/lib'
|
38
|
+
test.pattern = 'test/**/test_*.rb'
|
39
|
+
test.verbose = true
|
40
|
+
end
|
41
|
+
|
42
|
+
begin
|
43
|
+
require 'rcov/rcovtask'
|
44
|
+
Rcov::RcovTask.new do |test|
|
45
|
+
test.libs << 'test'
|
46
|
+
test.pattern = 'test/**/test_*.rb'
|
47
|
+
test.verbose = true
|
48
|
+
end
|
49
|
+
rescue LoadError
|
50
|
+
task :rcov do
|
51
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Don't check dependencies since we're testing with vendored libraries
|
56
|
+
# task :test => :check_dependencies
|
57
|
+
|
58
|
+
task :default => :test
|
59
|
+
|
60
|
+
require 'rake/rdoctask'
|
61
|
+
Rake::RDocTask.new do |rdoc|
|
62
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
63
|
+
|
64
|
+
rdoc.rdoc_dir = 'rdoc'
|
65
|
+
rdoc.title = "meta_where #{version}"
|
66
|
+
rdoc.rdoc_files.include('README*')
|
67
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
68
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.9.0
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Hash
|
2
|
+
def to_predicate(builder, parent = nil)
|
3
|
+
Arel::Predicates::All.new(*builder.build_predicates_from_hash(self, parent || builder.join_dependency.join_base))
|
4
|
+
end
|
5
|
+
|
6
|
+
def to_attribute(builder, parent = nil)
|
7
|
+
builder.build_attributes_from_hash(self, parent)
|
8
|
+
end
|
9
|
+
|
10
|
+
def |(other)
|
11
|
+
MetaWhere::Or.new(self, other)
|
12
|
+
end
|
13
|
+
|
14
|
+
def &(other)
|
15
|
+
MetaWhere::And.new(self, other)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Symbol
|
2
|
+
Arel::Attribute::PREDICATES.each do |predication|
|
3
|
+
define_method(predication) do
|
4
|
+
MetaWhere::Column.new(self, predication)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
MetaWhere::METHOD_ALIASES.each_pair do |aliased, predication|
|
9
|
+
define_method(aliased) do
|
10
|
+
MetaWhere::Column.new(self, predication)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_attribute(builder, parent = nil)
|
15
|
+
table = builder.build_table(parent)
|
16
|
+
|
17
|
+
unless attribute = table[self]
|
18
|
+
raise ::ActiveRecord::StatementInvalid, "No attribute named `#{self}` exists for table `#{table.name}`"
|
19
|
+
end
|
20
|
+
|
21
|
+
attribute
|
22
|
+
end
|
23
|
+
|
24
|
+
def asc
|
25
|
+
MetaWhere::Column.new(self, :asc)
|
26
|
+
end
|
27
|
+
|
28
|
+
def desc
|
29
|
+
MetaWhere::Column.new(self, :desc)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class Symbol
|
2
|
+
def [](value)
|
3
|
+
MetaWhere::Condition.new(self, value, :eq)
|
4
|
+
end
|
5
|
+
|
6
|
+
def ^(value)
|
7
|
+
MetaWhere::Condition.new(self, value, :not_eq)
|
8
|
+
end
|
9
|
+
|
10
|
+
def +(value)
|
11
|
+
MetaWhere::Condition.new(self, value, :in)
|
12
|
+
end
|
13
|
+
|
14
|
+
def -(value)
|
15
|
+
MetaWhere::Condition.new(self, value, :not_in)
|
16
|
+
end
|
17
|
+
|
18
|
+
def =~(value)
|
19
|
+
MetaWhere::Condition.new(self, value, :matches)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Won't work on Ruby 1.8.x so need to do this conditionally
|
23
|
+
if respond_to?('!~')
|
24
|
+
define_method('!~') do |value|
|
25
|
+
MetaWhere::Condition.new(self, value, :not_matches)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def >(value)
|
30
|
+
MetaWhere::Condition.new(self, value, :gt)
|
31
|
+
end
|
32
|
+
|
33
|
+
def >=(value)
|
34
|
+
MetaWhere::Condition.new(self, value, :gteq)
|
35
|
+
end
|
36
|
+
|
37
|
+
def <(value)
|
38
|
+
MetaWhere::Condition.new(self, value, :lt)
|
39
|
+
end
|
40
|
+
|
41
|
+
def <=(value)
|
42
|
+
MetaWhere::Condition.new(self, value, :lteq)
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'meta_where/utility'
|
2
|
+
|
3
|
+
module MetaWhere
|
4
|
+
class Builder
|
5
|
+
include MetaWhere::Utility
|
6
|
+
attr_reader :join_dependency
|
7
|
+
|
8
|
+
def initialize(join_dependency)
|
9
|
+
@join_dependency = join_dependency
|
10
|
+
@engine = join_dependency.join_base.arel_engine
|
11
|
+
@default_table = Arel::Table.new(join_dependency.join_base.table_name, :engine => @engine)
|
12
|
+
end
|
13
|
+
|
14
|
+
def build_table(parent_or_table_name = nil)
|
15
|
+
if parent_or_table_name.is_a?(Symbol)
|
16
|
+
Arel::Table.new(parent_or_table_name, :engine => @engine)
|
17
|
+
elsif parent_or_table_name.respond_to?(:aliased_table_name)
|
18
|
+
Arel::Table.new(parent_or_table_name.table_name, :as => parent_or_table_name.aliased_table_name, :engine => @engine)
|
19
|
+
else
|
20
|
+
@default_table
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_predicates_from_hash(attributes, parent = nil)
|
25
|
+
table = build_table(parent)
|
26
|
+
predicates = attributes.map do |column, value|
|
27
|
+
if value.is_a?(Hash)
|
28
|
+
association = parent.is_a?(Symbol) ? nil : @join_dependency.find_join_association(column, parent)
|
29
|
+
build_predicates_from_hash(value, association || column)
|
30
|
+
elsif value.is_a?(MetaWhere::Condition)
|
31
|
+
association = parent.is_a?(Symbol) ? nil : @join_dependency.find_join_association(column, parent)
|
32
|
+
value.to_predicate(self, association || column)
|
33
|
+
elsif value.is_a?(Array) && value.all? {|v| v.respond_to?(:to_predicate)}
|
34
|
+
association = parent.is_a?(Symbol) ? nil : @join_dependency.find_join_association(column, parent)
|
35
|
+
value.map {|val| val.to_predicate(self, association || column)}
|
36
|
+
else
|
37
|
+
if column.is_a?(MetaWhere::Column)
|
38
|
+
method = column.method
|
39
|
+
column = column.column
|
40
|
+
else
|
41
|
+
column = column.to_s
|
42
|
+
method = method_from_value(value)
|
43
|
+
end
|
44
|
+
|
45
|
+
if column.include?('.')
|
46
|
+
table_name, column = column.split('.', 2)
|
47
|
+
table = Arel::Table.new(table_name, :engine => parent.arel_engine)
|
48
|
+
end
|
49
|
+
|
50
|
+
unless attribute = table[column]
|
51
|
+
raise ::ActiveRecord::StatementInvalid, "No attribute named `#{column}` exists for table `#{table.name}`"
|
52
|
+
end
|
53
|
+
|
54
|
+
unless valid_comparison_method?(method)
|
55
|
+
raise ::ActiveRecord::StatementInvalid, "No comparison method named `#{method}` exists for column `#{column}`"
|
56
|
+
end
|
57
|
+
|
58
|
+
attribute.send(method, *args_for_predicate(method.to_s, value))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
predicates.flatten
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_attributes_from_hash(attributes, parent = nil)
|
66
|
+
table = build_table(parent)
|
67
|
+
built_attributes = attributes.map do |column, value|
|
68
|
+
if value.is_a?(Hash)
|
69
|
+
association = parent.is_a?(Symbol) ? nil : @join_dependency.find_join_association(column, parent)
|
70
|
+
build_attributes_from_hash(value, association || column)
|
71
|
+
elsif value.is_a?(Array) && value.all? {|v| v.respond_to?(:to_attribute)}
|
72
|
+
association = parent.is_a?(Symbol) ? nil : @join_dependency.find_join_association(column, parent)
|
73
|
+
value.map {|val| val.to_attribute(self, association || column)}
|
74
|
+
else
|
75
|
+
association = parent.is_a?(Symbol) ? nil : @join_dependency.find_join_association(column, parent)
|
76
|
+
value.respond_to?(:to_attribute) ? value.to_attribute(self, association || column) : value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
built_attributes.flatten
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module MetaWhere
|
2
|
+
class Column
|
3
|
+
attr_reader :column, :method
|
4
|
+
|
5
|
+
def initialize(column, method)
|
6
|
+
@column = column.to_s
|
7
|
+
@method = method.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
def %(value)
|
11
|
+
MetaWhere::Condition.new(column, value, method)
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other_column)
|
15
|
+
other_column.is_a?(Column) &&
|
16
|
+
other_column.column == column &&
|
17
|
+
other_column.method == method
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :eql?, :==
|
21
|
+
|
22
|
+
def to_attribute(builder, parent = nil)
|
23
|
+
column_name = column
|
24
|
+
if column_name.include?('.')
|
25
|
+
table_name, column_name = column_name.split('.', 2)
|
26
|
+
table = Arel::Table.new(table_name, :engine => parent.arel_engine)
|
27
|
+
else
|
28
|
+
table = builder.build_table(parent)
|
29
|
+
end
|
30
|
+
|
31
|
+
unless attribute = table[column_name]
|
32
|
+
raise ::ActiveRecord::StatementInvalid, "No attribute named `#{column_name}` exists for table `#{table.name}`"
|
33
|
+
end
|
34
|
+
|
35
|
+
attribute.send(method)
|
36
|
+
end
|
37
|
+
|
38
|
+
def hash
|
39
|
+
[column, method].hash
|
40
|
+
end
|
41
|
+
|
42
|
+
# Play "nicely" with expand_hash_conditions_for_aggregates
|
43
|
+
def to_sym
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Let's degrade hracefully if someone expects us to be a symbol or something
|
48
|
+
def to_s
|
49
|
+
@column
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|