meta_where 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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/.gitmodules ADDED
@@ -0,0 +1,6 @@
1
+ [submodule "vendor/rails"]
2
+ path = vendor/rails
3
+ url = git://github.com/rails/rails.git
4
+ [submodule "vendor/arel"]
5
+ path = vendor/arel
6
+ url = git://github.com/rails/arel.git
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
@@ -0,0 +1,8 @@
1
+ source :rubygems
2
+ gem "arel", "~> 1.0.0.rc1"
3
+ gem "activerecord", "~> 3.0.0.rc2"
4
+ gem "activesupport", "~> 3.0.0.rc2"
5
+ group :test do
6
+ gem "rake"
7
+ gem "shoulda"
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,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