sayso-meta_where 1.0.4.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG +90 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +20 -0
  6. data/README.rdoc +346 -0
  7. data/Rakefile +11 -0
  8. data/lib/core_ext/hash.rb +5 -0
  9. data/lib/core_ext/symbol.rb +39 -0
  10. data/lib/core_ext/symbol_operators.rb +48 -0
  11. data/lib/meta_where/association_reflection.rb +51 -0
  12. data/lib/meta_where/column.rb +31 -0
  13. data/lib/meta_where/compound.rb +20 -0
  14. data/lib/meta_where/condition.rb +32 -0
  15. data/lib/meta_where/condition_operators.rb +19 -0
  16. data/lib/meta_where/function.rb +108 -0
  17. data/lib/meta_where/join_dependency.rb +101 -0
  18. data/lib/meta_where/join_type.rb +43 -0
  19. data/lib/meta_where/not.rb +13 -0
  20. data/lib/meta_where/relation.rb +296 -0
  21. data/lib/meta_where/utility.rb +51 -0
  22. data/lib/meta_where/version.rb +3 -0
  23. data/lib/meta_where/visitors/attribute.rb +58 -0
  24. data/lib/meta_where/visitors/predicate.rb +149 -0
  25. data/lib/meta_where/visitors/visitor.rb +47 -0
  26. data/lib/meta_where.rb +51 -0
  27. data/meta_where.gemspec +48 -0
  28. data/test/fixtures/companies.yml +17 -0
  29. data/test/fixtures/company.rb +7 -0
  30. data/test/fixtures/data_type.rb +3 -0
  31. data/test/fixtures/data_types.yml +15 -0
  32. data/test/fixtures/developer.rb +7 -0
  33. data/test/fixtures/developers.yml +55 -0
  34. data/test/fixtures/developers_projects.yml +25 -0
  35. data/test/fixtures/fixed_bid_project.rb +2 -0
  36. data/test/fixtures/invalid_company.rb +4 -0
  37. data/test/fixtures/invalid_developer.rb +4 -0
  38. data/test/fixtures/note.rb +3 -0
  39. data/test/fixtures/notes.yml +95 -0
  40. data/test/fixtures/people.yml +14 -0
  41. data/test/fixtures/person.rb +4 -0
  42. data/test/fixtures/project.rb +7 -0
  43. data/test/fixtures/projects.yml +29 -0
  44. data/test/fixtures/schema.rb +53 -0
  45. data/test/fixtures/time_and_materials_project.rb +2 -0
  46. data/test/helper.rb +33 -0
  47. data/test/test_base.rb +21 -0
  48. data/test/test_relations.rb +476 -0
  49. metadata +184 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/CHANGELOG ADDED
@@ -0,0 +1,90 @@
1
+ Changes since 1.0.2 (2011-02-09):
2
+ * Fix eager loading with order values supplied
3
+
4
+ Changes since 1.0.1 (2011-01-18):
5
+ * Fix *_all behavior to not result in ORs when collapse_wheres is called
6
+ * Add support for NOT operator (-) on hashes, conditions, and compound
7
+ conditions. Unary NOTs the condition, binary does an AND NOT between
8
+ on the second condition.
9
+
10
+ Changes since 1.0.0 (2011-01-17):
11
+ * Update polymorphic join support to play nicely with MetaSearch
12
+
13
+ Changes since 0.9.10 (2011-01-06):
14
+ * Doc updates only, and a version change to 1.0.0.
15
+
16
+ Changes since 0.9.9 (2010-11-15):
17
+ * Multiple conditions on the same column with the same predicate will be "OR"ed
18
+ instead of dropping all but the last one. This tracks the same behavior in
19
+ Rails 3-0-stable.
20
+ * You can specify a polymorphic belongs_to join via something like:
21
+ Model.joins(:joinable.type(PolymorphicClass))
22
+ * You can specify ActiveRecord objects as values:
23
+ Developer.joins(:projects).where(:projects => Project.first) is the same as
24
+ Developer.joins(:projects).where(:projects => {:id => Project.first.id}), but
25
+ less hashy.
26
+
27
+ Changes since 0.9.6 (2010-09-30):
28
+ * ARel 2.x and Rails 3.0.2 compatibility
29
+ * Allow MetaWhere::And and MetaWhere::Or on the value side of a condition hash
30
+ to trigger treatment of key as an association.
31
+ ex: :comments => (:created_at.lt % 1.day.ago | :body.matches % '%hey%')
32
+ * Support for selecting join type: :association.outer / :association.inner results
33
+ in an outer or inner join.
34
+ * Having clauses are now fully MetaWhere-enabled.
35
+ * SQL functions are now supported. :concat.func(val1, val2) will result in an
36
+ SQL concat() call, with two parameters. If you've enabled Symbol operators, then
37
+ you can also just do :concat[val1, val2].
38
+
39
+ Changes since 0.9.5 (2010-09-27):
40
+ * Don't skip adding condition if an empty array is supplied
41
+
42
+ Changes since 0.9.4 (2010-09-23):
43
+ * Fix those fun problems with cache_classes.
44
+
45
+ Changes since 0.9.3 (2010-09-08):
46
+ * alias :& to :merge in singleton to work properly with Ruby 1.8.7.
47
+ * Fix merge to only use associations if not against the same base class. (STI)
48
+
49
+ Changes since 0.9.2 (2010-08-26):
50
+ * Updated gemspec to require Rails 3.0.0 final.
51
+
52
+ Changes since 0.9.1 (2010-08-25):
53
+ * Fix missing .any? on method chain in check for mw in association macros.
54
+
55
+ Changes since 0.9.0 (2010-08-24):
56
+ * Check for use of MetaWhere in association macros. This won't work as expected
57
+ so it's better to raise a helpful error than create a potentially confusing
58
+ behavior.
59
+
60
+ Changes since 0.5.2 (2010-07-09):
61
+ * Removed autojoin. Inner joins have side-effects. Newbies are the ones who aremost
62
+ likely to use autojoin, and they're also the ones least likely to understand why
63
+ certain rows stop being returned. Better to force a small learning curve. I've
64
+ decided against leaving it in with deprecation since Rails 3 isn't final yet.
65
+ Decided to get it out before it's "too late" to do so without impacting code running
66
+ on a stable version of Rails.
67
+ * Refactored build_arel to more closely mirror the refactoring that's been going on
68
+ to the method in Rails edge.
69
+ * Improved merge functonality. It shouldn't have ever required someone to do an
70
+ autojoin to begin with. Now it just works. If you're merging two relations, you
71
+ should expect to only get results that have a match on both sides.
72
+ * Attempt to gracefully degrade in the case where a MetaWhere::Column is expected
73
+ to be a normal symbol. Return the column name in #to_s.
74
+ * Fix order clauses when constructing a limited_ids_condition for postgresql.
75
+ * Fix problems with cache_classes == false on certain server configurations. Also
76
+ handle self-join with a merge of two identical classes and a second param.
77
+ * Drop ActiveSupport::Concern and only use alias_method_chain when needed.
78
+
79
+ Changes since 0.5.1 (2010-06-22):
80
+ * Added debug_sql method to Relations. Lets you see the actual SQL that
81
+ will be run against the database without having to resort to the
82
+ development.log. Differs from to_sql because that doesn't (and can't,
83
+ by necessity) handle eager loading.
84
+
85
+ Changes since 0.5.0 (2010-06-08):
86
+ * Track Emilio Tagua's performance enhancements in build_arel from edge.
87
+
88
+ Changes since 0.3.3 (2010-04-30):
89
+ * Lots. See http://metautonomo.us/2010/06/08/metasearch-and-metawhere-0-5-0-released/
90
+ for a summary.
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+ gemspec
3
+
4
+ gem 'arel', :git => 'git://github.com/rails/arel.git', :tag => 'v2.0.8.beta.20110131120940'
5
+ git 'git://github.com/rails/rails.git', :branch => '3-0-stable' do
6
+ gem 'activesupport'
7
+ gem 'activerecord'
8
+ end
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,346 @@
1
+ = MetaWhere
2
+
3
+ (If you're using edge Rails and you'd like to help me test the successor to
4
+ MetaWhere, please have a look at {Squeel}[http://github.com/ernie/squeel])
5
+
6
+ MetaWhere puts the power of Arel predications (comparison methods) in your ActiveRecord
7
+ condition hashes.
8
+
9
+ == Why?
10
+
11
+ <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.
12
+
13
+ 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>.
14
+
15
+ MetaWhere is a gem that sets out to right that wrong, and give tranquility to you, the Rails coder.
16
+
17
+ == Getting started
18
+
19
+ In your Gemfile:
20
+
21
+ gem "meta_where" # Last officially released gem
22
+ # gem "meta_where", :git => "git://github.com/ernie/meta_where.git" # Track git repo
23
+
24
+ or, to install as a plugin:
25
+
26
+ rails plugin install git://github.com/ernie/meta_where.git
27
+
28
+ == Example usage
29
+
30
+ === Where
31
+ You can use MetaWhere in your usual method chain:
32
+
33
+ Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)
34
+ => SELECT "articles".* FROM "articles" WHERE ("articles"."title" LIKE 'Hello%')
35
+ AND ("articles"."created_at" > '2010-04-12 18:39:32.592087')
36
+
37
+ === Find condition hash
38
+ You can also use similar syntax in a conditions hash supplied to ActiveRecord::Base#find:
39
+
40
+ Article.find(:all,
41
+ :conditions => {
42
+ :title.matches => 'Hello%',
43
+ :created_at.gt => 3.days.ago
44
+ }
45
+ )
46
+
47
+ === Scopes
48
+ They also work in named scopes as you would expect.
49
+
50
+ class Article
51
+ scope :recent, lambda {|v| where(:created_at.gt => v.days.ago)}
52
+ end
53
+
54
+ Article.recent(14).to_sql
55
+ => SELECT "articles".* FROM "articles"
56
+ WHERE ("articles"."created_at" > '2010-04-01 18:54:37.030951')
57
+
58
+ === Operators (Optionally)
59
+ Additionally, you can use certain operators as shorthand for certain Arel predication methods.
60
+
61
+ These are disabled by default, but can be enabled by calling MetaWhere.operator_overload! during
62
+ your app's initialization process.
63
+
64
+ These are experimental at this point and subject to change. Keep in mind that if you don't want
65
+ to enclose other conditions in {}, you should place operator conditions before any hash conditions.
66
+
67
+ Article.where(:created_at > 100.days.ago, :title =~ 'Hi%').to_sql
68
+ => SELECT "articles".* FROM "articles"
69
+ WHERE ("articles"."created_at" > '2010-01-05 20:11:44.997446')
70
+ AND ("articles"."title" LIKE 'Hi%')
71
+
72
+ Operators are:
73
+
74
+ * >> (equal)
75
+ * ^ (not equal)
76
+ * + (in array/range)
77
+ * - (not in array/range)
78
+ * =~ (matching -- not a regexp but a string for SQL LIKE) <b>NOTE:</b> This will override 1.9.x "symbol as string" =~ behavior.
79
+ * !~ (not matching, only available under Ruby 1.9)
80
+ * > (greater than)
81
+ * >= (greater than or equal to)
82
+ * < (less than)
83
+ * <= (less than or equal to)
84
+ * [] (SQL functions -- more on those below)
85
+
86
+ === Compounds
87
+ You can use the & and | operators to perform ands and ors within your queries.
88
+
89
+ <b>With operators:</b>
90
+ Article.where((:title =~ 'Hello%') | (:title =~ 'Goodbye%')).to_sql
91
+ => SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%'
92
+ OR "articles"."title" LIKE 'Goodbye%'))
93
+
94
+ That's kind of annoying, since operator precedence is such that you have to put
95
+ parentheses around everything. So MetaWhere also supports a substitution-inspired
96
+ (String#%) syntax.
97
+
98
+ <b>With "substitutions":</b>
99
+ Article.where(:title.matches % 'Hello%' | :title.matches % 'Goodbye%').to_sql
100
+ => SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%'
101
+ OR "articles"."title" LIKE 'Goodbye%'))
102
+
103
+ <b>With hashes:</b>
104
+ Article.where(
105
+ {:created_at.lt => Time.now} & {:created_at.gt => 1.year.ago}
106
+ ).to_sql
107
+ => SELECT "articles".* FROM "articles" WHERE
108
+ ((("articles"."created_at" < '2010-04-16 00:26:30.629467')
109
+ AND ("articles"."created_at" > '2009-04-16 00:26:30.629526')))
110
+
111
+ <b>With both hashes and substitutions:</b>
112
+ Article.where(
113
+ :title.matches % 'Hello%' &
114
+ {:created_at.lt => Time.now, :created_at.gt => 1.year.ago}
115
+ ).to_sql
116
+ => SELECT "articles".* FROM "articles" WHERE (("articles"."title" LIKE 'Hello%' AND
117
+ ("articles"."created_at" < '2010-04-16 01:04:38.023615' AND
118
+ "articles"."created_at" > '2009-04-16 01:04:38.023720')))
119
+
120
+ <b>With insanity... errr, complex combinations(*):</b>
121
+
122
+ Article.joins(:comments).where(
123
+ {:title => 'Greetings'} |
124
+ (
125
+ (
126
+ :created_at.gt % 21.days.ago &
127
+ :created_at.lt % 7.days.ago
128
+ ) &
129
+ :body.matches % '%from the past%'
130
+ ) &
131
+ {:comments => [:body =~ '%first post!%']}
132
+ ).to_sql
133
+ => SELECT "articles".*
134
+ FROM "articles"
135
+ INNER JOIN "comments"
136
+ ON "comments"."article_id" = "articles"."id"
137
+ WHERE
138
+ ((
139
+ "articles"."title" = 'Greetings'
140
+ OR
141
+ (
142
+ (
143
+ (
144
+ "articles"."created_at" > '2010-03-26 05:57:57.924258'
145
+ AND "articles"."created_at" < '2010-04-09 05:57:57.924984'
146
+ )
147
+ AND "articles"."body" LIKE '%from the past%'
148
+ )
149
+ AND "comments"."body" LIKE '%first post!%'
150
+ )
151
+ ))
152
+
153
+ (*) Formatting added for clarity. I said you could do this, not that you should. :)
154
+
155
+ == Join type specification
156
+ You can choose whether to use an inner join (the default) or a left outer join by tacking
157
+ <tt>.outer</tt> or <tt>.inner</tt> to the symbols specified in your joins() call:
158
+
159
+ Article.joins(:comments => :moderations.outer).to_sql
160
+ => SELECT "articles".* FROM "articles"
161
+ INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
162
+ LEFT OUTER JOIN "moderations" ON "moderations"."comment_id" = "comments"."id"
163
+
164
+ == SQL Functions
165
+ You can use SQL functions in your queries:
166
+
167
+ Manager.joins(:employees.outer).group('managers.id').
168
+ having(:employees => (:count.func(:id) < 3))
169
+ => SELECT "managers".* FROM "managers"
170
+ LEFT OUTER JOIN "employees" ON "employees"."manager_id" = "managers"."id"
171
+ GROUP BY managers.id HAVING count("employees"."id") < 3
172
+
173
+ If you enable Symbol operators, you can just use <tt>:count[:id]</tt>, instead of calling
174
+ <tt>func</tt> as shown above. SQL functions work in the SELECT, WHERE, and HAVING clauses,
175
+ and can be aliased with <tt>as</tt>:
176
+
177
+ Manager.select('managers.*').
178
+ select(:find_in_set[:id, '3,2,1'].as('position'))
179
+ => SELECT managers.*, find_in_set("managers"."id",'3,2,1') AS position
180
+ FROM "managers"
181
+
182
+ === But wait, there's more!
183
+
184
+ == Intelligent hash condition mapping
185
+ This is one of those things I hope you find so intuitive that you forget it wasn't
186
+ built in already.
187
+
188
+ PredicateBuilder (the part of ActiveRecord responsible for turning your conditions
189
+ hash into a valid SQL query) will allow you to nest conditions in order to specify a
190
+ table that the conditions apply to:
191
+
192
+ Article.joins(:comments).where(:comments => {:body => 'hey'}).to_sql
193
+ => SELECT "articles".* FROM "articles" INNER JOIN "comments"
194
+ ON "comments"."article_id" = "articles"."id"
195
+ WHERE ("comments"."body" = 'hey')
196
+
197
+ This feels pretty magical at first, but the magic quickly breaks down. Consider an
198
+ association named <tt>:other_comments</tt> that is just a condition against comments:
199
+
200
+ Article.joins(:other_comments).where(:other_comments => {:body => 'hey'}).to_sql
201
+ => ActiveRecord::StatementInvalid: No attribute named `body` exists for table `other_comments`
202
+
203
+ Ick. This is because the query is being created against tables, and not against associations.
204
+ You'd need to do...
205
+
206
+ Article.joins(:other_comments).where(:comments => {:body => 'hey'})
207
+
208
+ ...instead.
209
+
210
+ With MetaWhere:
211
+
212
+ Article.joins(:other_comments).where(:other_comments => {:body => 'hey'}).to_sql
213
+ => SELECT "articles".* FROM "articles" INNER JOIN "comments"
214
+ ON "comments"."article_id" = "articles"."id" WHERE (("comments"."body" = 'hey'))
215
+
216
+ The general idea is that if an association with the name provided exists, MetaWhere
217
+ will build the conditions against that association's table as it's been aliased, before falling
218
+ back to assuming you're specifying a table by name. It also handles nested associations:
219
+
220
+ Article.where(
221
+ :comments => {
222
+ :body => 'yo',
223
+ :moderations => [:value < 0]
224
+ },
225
+ :other_comments => {:body => 'hey'}
226
+ ).joins(
227
+ {:comments => :moderations},
228
+ :other_comments
229
+ ).to_sql
230
+ => SELECT "articles".* FROM "articles"
231
+ INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
232
+ INNER JOIN "moderations" ON "moderations"."comment_id" = "comments"."id"
233
+ INNER JOIN "comments" "other_comments_articles"
234
+ ON "other_comments_articles"."article_id" = "articles"."id"
235
+ WHERE (("comments"."body" = 'yo' AND "moderations"."value" < 0
236
+ AND "other_comments_articles"."body" = 'hey'))
237
+
238
+ Contrived example, I'll admit -- but I'll bet you can think of some uses for this.
239
+
240
+ == Enhanced relation merges
241
+
242
+ One of the changes MetaWhere makes to ActiveRecord is to delay "compiling" the
243
+ where_values into actual Arel predicates until absolutely necessary. This allows
244
+ for greater flexibility and last-second inference of associations/joins from any
245
+ hashes supplied. A drawback of this method is that when merging relations, ActiveRecord
246
+ just assumes that the values being merged are already firmed up against a specific table
247
+ name and can just be thrown together. This isn't the case with MetaWhere, and would
248
+ cause unexpected failures when merging. However, MetaWhere improves on the default
249
+ ActiveRecord merge functionality in two ways. First, when called with 1 parameter,
250
+ (as is always the case when using the & alias) MetaWhere will try to determine if
251
+ an association exists between the two models involved in the merge. If it does, the
252
+ association name will be used to construct criteria.
253
+
254
+ Additionally, to cover times when detection is impossible, or the first detected
255
+ association isn't the one you wanted, you can call merge with a second parameter,
256
+ specifying the association to be used during the merge.
257
+
258
+ This merge functionality allows you to do this...
259
+
260
+ (Comment.where(:id < 7) & Article.where(:title =~ '%blah%')).to_sql
261
+ => SELECT "comments".* FROM "comments" INNER JOIN "articles"
262
+ ON "articles"."id" = "comments"."article_id"
263
+ WHERE ("comments"."id" < 7) AND ("articles"."title" LIKE '%blah%')"
264
+
265
+ ...or this...
266
+
267
+ Article.where(:id < 2).merge(Comment.where(:id < 7), :lame_comments).to_sql
268
+ => "SELECT "articles".* FROM "articles"
269
+ INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
270
+ AND "comments"."body" = 'first post!'
271
+ WHERE ("articles"."id" < 2) AND ("comments"."id" < 7)"
272
+
273
+ == Enhanced order clauses
274
+
275
+ If you are used to doing stuff like <tt>Article.order('title asc')</tt>, that will still
276
+ work as you expect. However, if you pass symbols or arrays in to the <tt>order</tt> method,
277
+ you can take advantage of intelligent association detection (as with "Intelligent hash condition
278
+ mapping," above) and also some convenience methods for ascending and descending sorts.
279
+
280
+ Article.order(
281
+ :title.desc,
282
+ :comments => [:created_at.asc, :updated_at]
283
+ ).joins(:comments).to_sql
284
+ => SELECT "articles".* FROM "articles"
285
+ INNER JOIN "comments" ON "comments"."article_id" = "articles"."id"
286
+ ORDER BY "articles"."title" DESC,
287
+ "comments"."created_at" ASC, "comments"."updated_at"
288
+
289
+ == Polymorphic belongs_to joins
290
+
291
+ Polymorphic associations provide great flexibility, but they can sometimes be a bit of a hassle
292
+ when it comes to querying through a belongs_to association. First, you have to know what type
293
+ you're looking for to do a proper join, and then, you're forced into using a string join in order
294
+ to make it happen (which would prevent the use of MetaWhere intelligent condition mapping).
295
+
296
+ MetaWhere allows you to join polymorphic belongs_to associations like this:
297
+
298
+ Note.joins(:notable.type(Developer)).
299
+ where(:notable.type(Developer) => {:name.matches => 'Ernie%'})
300
+ => SELECT "notes".* FROM "notes"
301
+ INNER JOIN "developers" ON "developers"."id" = "notes"."notable_id"
302
+ AND "notes"."notable_type" = 'Developer'
303
+ WHERE "developers"."name" LIKE 'Ernie%'
304
+
305
+ == Using ActiveRecord objects as condition values
306
+
307
+ Wouldn't it be nice if you could do something like this?
308
+
309
+ # Developer belongs_to Company
310
+ company = Company.find(123)
311
+ Developer.where(:company => company)
312
+
313
+ # Developer HABTM Projects
314
+ projects = [Project.first, Project.last]
315
+ Developer.joins(:projects).where(:projects => projects)
316
+
317
+ # Note belongs_to :notable, :polymorphic => true
318
+ dev1 = Developer.first
319
+ dev2 = Developer.last
320
+ project = Project.first
321
+ company = Company.first
322
+ Note.where(:notable => [dev1, dev2, project, company]).to_sql
323
+ => SELECT "notes".* FROM "notes" WHERE (((("notes"."notable_id" IN (1, 8)
324
+ AND "notes"."notable_type" = 'Developer') OR ("notes"."notable_id" = 1
325
+ AND "notes"."notable_type" = 'Project')) OR ("notes"."notable_id" = 1
326
+ AND "notes"."notable_type" = 'Company')))
327
+
328
+ With MetaWhere, you can.
329
+
330
+ == Thanks
331
+ A huge thank you goes to Pratik Naik (lifo) for a dicussion on #rails-contrib about a patch
332
+ I'd submitted, and his take on a DSL for query conditions, which was the inspiration for this
333
+ gem.
334
+
335
+ == Contributions
336
+
337
+ There are several ways you can help MetaWhere continue to improve.
338
+
339
+ * Use MetaWhere in your real-world projects and {submit bug reports or feature suggestions}[http://metautonomous.lighthouseapp.com/projects/53011-metawhere/].
340
+ * 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!)
341
+ * If you like MetaWhere, spread the word. More users == more eyes on code == more bugs getting found == more bugs getting fixed (hopefully!)
342
+ * 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. :)
343
+
344
+ == Copyright
345
+
346
+ Copyright (c) 2010 {Ernie Miller}[http://metautonomo.us]. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+ require 'rake/testtask'
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'test'
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ require 'meta_where/condition_operators'
2
+
3
+ class Hash
4
+ include MetaWhere::ConditionOperators
5
+ end
@@ -0,0 +1,39 @@
1
+ class Symbol
2
+ MetaWhere::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 mw_func(*args)
15
+ MetaWhere::Function.new(self, *args)
16
+ end
17
+
18
+ alias_method :func, :mw_func unless method_defined?(:func)
19
+
20
+ def inner
21
+ MetaWhere::JoinType.new(self, Arel::Nodes::InnerJoin)
22
+ end
23
+
24
+ def outer
25
+ MetaWhere::JoinType.new(self, Arel::Nodes::OuterJoin)
26
+ end
27
+
28
+ def type(klass)
29
+ MetaWhere::JoinType.new(self, Arel::Nodes::InnerJoin, klass)
30
+ end
31
+
32
+ def asc
33
+ MetaWhere::Column.new(self, :asc)
34
+ end
35
+
36
+ def desc
37
+ MetaWhere::Column.new(self, :desc)
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ class Symbol
2
+ def [](*values)
3
+ MetaWhere::Function.new(self, *values)
4
+ end
5
+
6
+ def >>(value)
7
+ MetaWhere::Condition.new(self, value, :eq)
8
+ end
9
+
10
+ def ^(value)
11
+ MetaWhere::Condition.new(self, value, :not_eq)
12
+ end
13
+
14
+ def +(value)
15
+ MetaWhere::Condition.new(self, value, :in)
16
+ end
17
+
18
+ def -(value)
19
+ MetaWhere::Condition.new(self, value, :not_in)
20
+ end
21
+
22
+ def =~(value)
23
+ MetaWhere::Condition.new(self, value, :matches)
24
+ end
25
+
26
+ # Won't work on Ruby 1.8.x so need to do this conditionally
27
+ if respond_to?('!~')
28
+ define_method('!~') do |value|
29
+ MetaWhere::Condition.new(self, value, :does_not_match)
30
+ end
31
+ end
32
+
33
+ def >(value)
34
+ MetaWhere::Condition.new(self, value, :gt)
35
+ end
36
+
37
+ def >=(value)
38
+ MetaWhere::Condition.new(self, value, :gteq)
39
+ end
40
+
41
+ def <(value)
42
+ MetaWhere::Condition.new(self, value, :lt)
43
+ end
44
+
45
+ def <=(value)
46
+ MetaWhere::Condition.new(self, value, :lteq)
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ module MetaWhere
2
+ class MetaWhereInAssociationError < StandardError; end
3
+
4
+ module AssociationReflection
5
+
6
+ def initialize(macro, name, options, active_record)
7
+ super
8
+
9
+ if options.has_key?(:conditions)
10
+ ensure_no_metawhere_in_conditions(options[:conditions])
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def ensure_no_metawhere_in_conditions(obj)
17
+ case obj
18
+ when Hash
19
+ if obj.keys.grep(MetaWhere::Column).any?
20
+ raise MetaWhereInAssociationError, <<END
21
+ The :#{name} association has a MetaWhere::Column in its :conditions. \
22
+ If you actually needed to access conditions other than equality, then you most \
23
+ likely meant to set up a scope or method, instead. Associations only work with \
24
+ standard equality conditions, since they can be used to create records as well.
25
+ END
26
+ end
27
+
28
+ obj.values.each do |v|
29
+ case v
30
+ when MetaWhere::Condition, Array, Hash
31
+ ensure_no_metawhere_in_conditions(v)
32
+ end
33
+ end
34
+ when Array
35
+ obj.each do |v|
36
+ case v
37
+ when MetaWhere::Condition, Array, Hash
38
+ ensure_no_metawhere_in_conditions(v)
39
+ end
40
+ end
41
+ when MetaWhere::Condition
42
+ raise MetaWhereInAssociationError, <<END
43
+ The :#{name} association has a MetaWhere::Condition in its :conditions. \
44
+ If you actually needed to access conditions other than equality, then you most \
45
+ likely meant to set up a scope or method, instead. Associations only work with \
46
+ standard equality conditions, since they can be used to create records as well.
47
+ END
48
+ end
49
+ end
50
+ end
51
+ end