squeel 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +8 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +41 -0
  5. data/Rakefile +19 -0
  6. data/lib/core_ext/hash.rb +13 -0
  7. data/lib/core_ext/symbol.rb +36 -0
  8. data/lib/squeel.rb +26 -0
  9. data/lib/squeel/adapters/active_record.rb +6 -0
  10. data/lib/squeel/adapters/active_record/join_association.rb +90 -0
  11. data/lib/squeel/adapters/active_record/join_dependency.rb +68 -0
  12. data/lib/squeel/adapters/active_record/relation.rb +292 -0
  13. data/lib/squeel/configuration.rb +25 -0
  14. data/lib/squeel/constants.rb +23 -0
  15. data/lib/squeel/contexts/join_dependency_context.rb +74 -0
  16. data/lib/squeel/dsl.rb +31 -0
  17. data/lib/squeel/nodes.rb +10 -0
  18. data/lib/squeel/nodes/and.rb +8 -0
  19. data/lib/squeel/nodes/binary.rb +23 -0
  20. data/lib/squeel/nodes/function.rb +84 -0
  21. data/lib/squeel/nodes/join.rb +51 -0
  22. data/lib/squeel/nodes/key_path.rb +127 -0
  23. data/lib/squeel/nodes/nary.rb +35 -0
  24. data/lib/squeel/nodes/not.rb +8 -0
  25. data/lib/squeel/nodes/operation.rb +23 -0
  26. data/lib/squeel/nodes/operators.rb +27 -0
  27. data/lib/squeel/nodes/or.rb +8 -0
  28. data/lib/squeel/nodes/order.rb +35 -0
  29. data/lib/squeel/nodes/predicate.rb +49 -0
  30. data/lib/squeel/nodes/predicate_operators.rb +17 -0
  31. data/lib/squeel/nodes/stub.rb +113 -0
  32. data/lib/squeel/nodes/unary.rb +22 -0
  33. data/lib/squeel/predicate_methods.rb +22 -0
  34. data/lib/squeel/predicate_methods/function.rb +9 -0
  35. data/lib/squeel/predicate_methods/predicate.rb +11 -0
  36. data/lib/squeel/predicate_methods/stub.rb +9 -0
  37. data/lib/squeel/predicate_methods/symbol.rb +9 -0
  38. data/lib/squeel/version.rb +3 -0
  39. data/lib/squeel/visitors.rb +3 -0
  40. data/lib/squeel/visitors/base.rb +46 -0
  41. data/lib/squeel/visitors/order_visitor.rb +107 -0
  42. data/lib/squeel/visitors/predicate_visitor.rb +179 -0
  43. data/lib/squeel/visitors/select_visitor.rb +103 -0
  44. data/spec/blueprints/articles.rb +5 -0
  45. data/spec/blueprints/comments.rb +5 -0
  46. data/spec/blueprints/notes.rb +3 -0
  47. data/spec/blueprints/people.rb +4 -0
  48. data/spec/blueprints/tags.rb +3 -0
  49. data/spec/console.rb +22 -0
  50. data/spec/core_ext/symbol_spec.rb +68 -0
  51. data/spec/helpers/squeel_helper.rb +5 -0
  52. data/spec/spec_helper.rb +30 -0
  53. data/spec/squeel/adapters/active_record/join_association_spec.rb +18 -0
  54. data/spec/squeel/adapters/active_record/join_depdendency_spec.rb +60 -0
  55. data/spec/squeel/adapters/active_record/relation_spec.rb +437 -0
  56. data/spec/squeel/contexts/join_dependency_context_spec.rb +43 -0
  57. data/spec/squeel/dsl_spec.rb +73 -0
  58. data/spec/squeel/nodes/function_spec.rb +149 -0
  59. data/spec/squeel/nodes/join_spec.rb +27 -0
  60. data/spec/squeel/nodes/key_path_spec.rb +92 -0
  61. data/spec/squeel/nodes/operation_spec.rb +149 -0
  62. data/spec/squeel/nodes/operators_spec.rb +87 -0
  63. data/spec/squeel/nodes/order_spec.rb +30 -0
  64. data/spec/squeel/nodes/predicate_operators_spec.rb +88 -0
  65. data/spec/squeel/nodes/predicate_spec.rb +92 -0
  66. data/spec/squeel/nodes/stub_spec.rb +178 -0
  67. data/spec/squeel/visitors/order_visitor_spec.rb +128 -0
  68. data/spec/squeel/visitors/predicate_visitor_spec.rb +267 -0
  69. data/spec/squeel/visitors/select_visitor_spec.rb +115 -0
  70. data/spec/support/schema.rb +101 -0
  71. data/squeel.gemspec +44 -0
  72. metadata +221 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
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'
5
+ git 'git://github.com/rails/rails.git' do
6
+ gem 'activesupport'
7
+ gem 'activerecord'
8
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2011 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,41 @@
1
+ =Squeel
2
+
3
+ This is a complete rewrite of the library formerly called MetaWhere. It's Rails 3.1-only
4
+ for now.
5
+
6
+ It's not really suitable for actual use yet, but you're welcome to test it and send
7
+ me feedback.
8
+
9
+ ==What's new?
10
+
11
+ A lot.
12
+
13
+ * Symbol and hash methods aren't loaded by default. To enable them, do this in
14
+ your Squeel.configure block: <tt>config.load_core_extensions :hash, :symbol</tt>
15
+ * Speaking of, you can call Squeel.configure do |config| ... end and do
16
+ another bit of configuration, setting up your own aliases.
17
+ <tt>config.alias_predicate :new_name, :old_name</tt>
18
+ * The preferred way to use the various enhancements is now by passing a block to
19
+ the relation method you're calling. For example:
20
+
21
+ Person.select{max(id).as(max_id)} # Call SQL functions
22
+ Person.where{(name == 'bob') & (salary == 100000)} # Compounds & and | work
23
+
24
+ * Operators have changed. As before, operators starting with ! are only available
25
+ on Ruby 1.9. Upgrade, for the love of all that is good and holy.
26
+
27
+ * == - Equality
28
+ * != - Inequality
29
+ * ^ - Inequality, for those poor souls on 1.8.x
30
+ * >> - In, (mnemonic: value >> [1,2,3], the value is running INTO the array)
31
+ * << - Not in, (mnemonic: value << [1,2,3], the value is running OUT of the array)
32
+ * =~ - Matches (SQL LIKE)
33
+ * !~ - Not matches (SQL NOT LIKE) Again, only in Ruby 1.9
34
+ * > - Greater than
35
+ * >= - Greater than or equal to
36
+ * < - Less than
37
+ * <= - Less than or equal to
38
+ * [] - Alternative function syntax. Just use parentheses, not sure I'm gonna keep [].
39
+
40
+ There's more -- have a read through the specs for a better idea of what you can do,
41
+ or clone the repo, run <tt>bundle install</tt> and play around in <tt>rake console</tt>.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |rspec|
7
+ rspec.rspec_opts = ['--backtrace']
8
+ end
9
+
10
+ task :default => :spec
11
+
12
+ desc "Open an irb session with Squeel and the sample data used in specs"
13
+ task :console do
14
+ require 'irb'
15
+ require 'irb/completion'
16
+ require 'console'
17
+ ARGV.clear
18
+ IRB.start
19
+ end
@@ -0,0 +1,13 @@
1
+ require 'squeel/nodes/predicate_operators'
2
+
3
+ class Hash
4
+ # Hashes are "acceptable" by PredicateVisitor, so they
5
+ # can be treated like nodes for the purposes of and/or/not
6
+ # if you load core extensions with:
7
+ #
8
+ # Squeel.configure do |config|
9
+ # config.load_core_extensions :hash
10
+ # end
11
+
12
+ include Squeel::Nodes::PredicateOperators
13
+ end
@@ -0,0 +1,36 @@
1
+ require 'squeel/predicate_methods'
2
+
3
+ class Symbol
4
+ # These extensions to Symbol are loaded optionally, with:
5
+ #
6
+ # Squeel.configure do |config|
7
+ # config.load_core_extensions :symbol
8
+ # end
9
+
10
+ include Squeel::PredicateMethods
11
+
12
+ def asc
13
+ Squeel::Nodes::Order.new self, 1
14
+ end
15
+
16
+ def desc
17
+ Squeel::Nodes::Order.new self, -1
18
+ end
19
+
20
+ def func(*args)
21
+ Squeel::Nodes::Function.new(self, args)
22
+ end
23
+
24
+ def inner
25
+ Squeel::Nodes::Join.new(self, Arel::InnerJoin)
26
+ end
27
+
28
+ def outer
29
+ Squeel::Nodes::Join.new(self, Arel::OuterJoin)
30
+ end
31
+
32
+ def of_class(klass)
33
+ Squeel::Nodes::Join.new(self, Arel::InnerJoin, klass)
34
+ end
35
+
36
+ end
data/lib/squeel.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'squeel/configuration'
2
+
3
+ module Squeel
4
+
5
+ extend Configuration
6
+
7
+ def self.evil_things
8
+ original_verbosity = $VERBOSE
9
+ $VERBOSE = nil
10
+ yield
11
+ ensure
12
+ $VERBOSE = original_verbosity
13
+ end
14
+
15
+ Constants::PREDICATE_ALIASES.each do |original, aliases|
16
+ aliases.each do |aliaz|
17
+ alias_predicate aliaz, original
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ require 'squeel/nodes'
24
+ require 'squeel/dsl'
25
+ require 'squeel/visitors'
26
+ require 'squeel/adapters/active_record'
@@ -0,0 +1,6 @@
1
+ require 'squeel/adapters/active_record/relation'
2
+ require 'squeel/adapters/active_record/join_dependency'
3
+ require 'squeel/adapters/active_record/join_association'
4
+
5
+ ActiveRecord::Relation.send :include, Squeel::Adapters::ActiveRecord::Relation
6
+ ActiveRecord::Associations::JoinDependency.send :include, Squeel::Adapters::ActiveRecord::JoinDependency
@@ -0,0 +1,90 @@
1
+ require 'active_record'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+
7
+ class JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation
8
+
9
+ def initialize(reflection, join_dependency, parent = nil, polymorphic_class = nil)
10
+ if polymorphic_class && ::ActiveRecord::Base > polymorphic_class
11
+ swapping_reflection_klass(reflection, polymorphic_class) do |reflection|
12
+ super(reflection, join_dependency, parent)
13
+ end
14
+ else
15
+ super(reflection, join_dependency, parent)
16
+ end
17
+ end
18
+
19
+ def swapping_reflection_klass(reflection, klass)
20
+ reflection = reflection.clone
21
+ original_polymorphic = reflection.options.delete(:polymorphic)
22
+ reflection.instance_variable_set(:@klass, klass)
23
+ yield reflection
24
+ ensure
25
+ reflection.options[:polymorphic] = original_polymorphic
26
+ end
27
+
28
+ def join_to(relation)
29
+ tables = @tables.dup
30
+ foreign_table = parent_table
31
+
32
+ # The chain starts with the target table, but we want to end with it here (makes
33
+ # more sense in this context), so we reverse
34
+ chain.reverse.each_with_index do |reflection, i|
35
+ table = tables.shift
36
+
37
+ case reflection.source_macro
38
+ when :belongs_to
39
+ key = reflection.association_primary_key
40
+ foreign_key = reflection.foreign_key
41
+ when :has_and_belongs_to_many
42
+ # Join the join table first...
43
+ relation.from(join(
44
+ table,
45
+ table[reflection.foreign_key].
46
+ eq(foreign_table[reflection.active_record_primary_key])
47
+ ))
48
+
49
+ foreign_table, table = table, tables.shift
50
+
51
+ key = reflection.association_primary_key
52
+ foreign_key = reflection.association_foreign_key
53
+ else
54
+ key = reflection.foreign_key
55
+ foreign_key = reflection.active_record_primary_key
56
+ end
57
+
58
+ constraint = table[key].eq(foreign_table[foreign_key])
59
+
60
+ if reflection.options[:polymorphic]
61
+ constraint = constraint.and(
62
+ foreign_table[reflection.foreign_type].eq(reflection.klass.name)
63
+ )
64
+ end
65
+
66
+ if reflection.klass.finder_needs_type_condition?
67
+ constraint = table.create_and([
68
+ constraint,
69
+ reflection.klass.send(:type_condition, table)
70
+ ])
71
+ end
72
+
73
+ relation.from(join(table, constraint))
74
+
75
+ unless conditions[i].empty?
76
+ relation.where(sanitize(conditions[i], table))
77
+ end
78
+
79
+ # The current table in this iteration becomes the foreign table in the next
80
+ foreign_table = table
81
+ end
82
+
83
+ relation
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,68 @@
1
+ require 'active_record'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+ module JoinDependency
7
+
8
+ # Yes, I'm using alias_method_chain here. No, I don't feel too
9
+ # bad about it. JoinDependency, or, to call it by its full proper
10
+ # name, ::ActiveRecord::Associations::JoinDependency, is one of the
11
+ # most "for internal use only" chunks of ActiveRecord.
12
+ def self.included(base)
13
+ base.class_eval do
14
+ alias_method_chain :build, :squeel
15
+ end
16
+ end
17
+
18
+ def build_with_squeel(associations, parent = nil, join_type = Arel::InnerJoin)
19
+ associations = associations.symbol if Nodes::Stub === associations
20
+
21
+ case associations
22
+ when Nodes::Join
23
+ parent ||= join_parts.last
24
+ reflection = parent.reflections[associations.name] or
25
+ raise ::ActiveRecord::ConfigurationError, "Association named '#{ associations.name }' was not found; perhaps you misspelled it?"
26
+
27
+ unless join_association = find_join_association_respecting_polymorphism(reflection, parent, associations)
28
+ @reflections << reflection
29
+ join_association = build_join_association_respecting_polymorphism(reflection, parent, associations)
30
+ join_association.join_type = associations.type
31
+ @join_parts << join_association
32
+ cache_joined_association(join_association)
33
+ end
34
+
35
+ join_association
36
+ when Nodes::KeyPath
37
+ parent ||= join_parts.last
38
+ associations.path_with_endpoint.each do |key|
39
+ parent = build(key, parent, join_type)
40
+ end
41
+ parent
42
+ else
43
+ build_without_squeel(associations, parent, join_type)
44
+ end
45
+ end
46
+
47
+ def find_join_association_respecting_polymorphism(reflection, parent, join)
48
+ if association = find_join_association(reflection, parent)
49
+ unless reflection.options[:polymorphic]
50
+ association
51
+ else
52
+ association if association.active_record == join.klass
53
+ end
54
+ end
55
+ end
56
+
57
+ def build_join_association_respecting_polymorphism(reflection, parent, join)
58
+ if reflection.options[:polymorphic] && join.polymorphic?
59
+ JoinAssociation.new(reflection, self, parent, join.klass)
60
+ else
61
+ JoinAssociation.new(reflection, self, parent)
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,292 @@
1
+ require 'active_record'
2
+
3
+ module Squeel
4
+ module Adapters
5
+ module ActiveRecord
6
+ module Relation
7
+
8
+ JoinAssociation = ::ActiveRecord::Associations::JoinDependency::JoinAssociation
9
+ JoinDependency = ::ActiveRecord::Associations::JoinDependency
10
+
11
+ attr_writer :join_dependency
12
+ private :join_dependency=
13
+
14
+ def join_dependency
15
+ @join_dependency ||= (build_join_dependency(table.from(table), @joins_values) && @join_dependency)
16
+ end
17
+
18
+ def select_visitor
19
+ Visitors::SelectVisitor.new(
20
+ Contexts::JoinDependencyContext.new(join_dependency)
21
+ )
22
+ end
23
+
24
+ def predicate_visitor
25
+ Visitors::PredicateVisitor.new(
26
+ Contexts::JoinDependencyContext.new(join_dependency)
27
+ )
28
+ end
29
+
30
+ def order_visitor
31
+ Visitors::OrderVisitor.new(
32
+ Contexts::JoinDependencyContext.new(join_dependency)
33
+ )
34
+ end
35
+
36
+ def merge(r, association_name = nil)
37
+ if association_name || relation_with_different_base?(r)
38
+ r = r.clone
39
+ association_name ||= infer_association_for_relation_merge(r)
40
+ prepare_relation_for_association_merge!(r, association_name)
41
+ self.joins_values += [association_name] if reflect_on_association(association_name)
42
+ end
43
+
44
+ super(r)
45
+ end
46
+
47
+ def relation_with_different_base?(r)
48
+ ::ActiveRecord::Relation === r &&
49
+ base_class.name != r.klass.base_class.name
50
+ end
51
+
52
+ def infer_association_for_relation_merge(r)
53
+ default_association = reflect_on_all_associations.detect {|a| a.class_name == r.klass.name}
54
+ default_association ? default_association.name : r.table_name.to_sym
55
+ end
56
+
57
+ def prepare_relation_for_association_merge!(r, association_name)
58
+ r.where_values.map! {|w| Squeel::Visitors::PredicateVisitor.can_accept?(w) ? {association_name => w} : w}
59
+ r.having_values.map! {|h| Squeel::Visitors::PredicateVisitor.can_accept?(h) ? {association_name => h} : h}
60
+ r.joins_values.map! {|j| [Symbol, Hash, Nodes::Stub, Nodes::Join].include?(j.class) ? {association_name => j} : j}
61
+ end
62
+
63
+ def build_arel
64
+ arel = table.from table
65
+
66
+ build_join_dependency(arel, @joins_values) unless @joins_values.empty?
67
+
68
+ predicate_viz = predicate_visitor
69
+
70
+ collapse_wheres(arel, predicate_viz.accept((@where_values - ['']).uniq))
71
+
72
+ arel.having(*predicate_viz.accept(@having_values.uniq.reject{|h| h.blank?})) unless @having_values.empty?
73
+
74
+ arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
75
+ arel.skip(@offset_value) if @offset_value
76
+
77
+ arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
78
+
79
+ unless @order_values.empty?
80
+ order_viz = order_visitor
81
+ arel.order(*order_viz.accept(@order_values.uniq.reject{|o| o.blank?}))
82
+ end
83
+
84
+ build_select(arel, select_visitor.accept(@select_values.uniq))
85
+
86
+ arel.from(@from_value) if @from_value
87
+ arel.lock(@lock_value) if @lock_value
88
+
89
+ arel
90
+ end
91
+
92
+ def build_join_dependency(manager, joins)
93
+ buckets = joins.group_by do |join|
94
+ case join
95
+ when String
96
+ 'string_join'
97
+ when Hash, Symbol, Array, Nodes::Stub, Nodes::Join, Nodes::KeyPath
98
+ 'association_join'
99
+ when JoinAssociation
100
+ 'stashed_join'
101
+ when Arel::Nodes::Join
102
+ 'join_node'
103
+ else
104
+ raise 'unknown class: %s' % join.class.name
105
+ end
106
+ end
107
+
108
+ association_joins = buckets['association_join'] || []
109
+ stashed_association_joins = buckets['stashed_join'] || []
110
+ join_nodes = buckets['join_node'] || []
111
+ string_joins = (buckets['string_join'] || []).map { |x|
112
+ x.strip
113
+ }.uniq
114
+
115
+ join_list = custom_join_ast(manager, string_joins)
116
+
117
+ # All of this duplication just to add
118
+ self.join_dependency = JoinDependency.new(
119
+ @klass,
120
+ association_joins,
121
+ join_list
122
+ )
123
+
124
+ join_nodes.each do |join|
125
+ join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
126
+ end
127
+
128
+ join_dependency.graft(*stashed_association_joins)
129
+
130
+ @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
131
+
132
+ join_dependency.join_associations.each do |association|
133
+ association.join_to(manager)
134
+ end
135
+
136
+ manager.join_sources.concat join_nodes.uniq
137
+ manager.join_sources.concat join_list
138
+
139
+ manager
140
+ end
141
+
142
+ def select(value = Proc.new)
143
+ if block_given? && Proc === value
144
+ if value.arity > 0
145
+ to_a.select {|*block_args| value.call(*block_args)}
146
+ else
147
+ relation = clone
148
+ relation.select_values += Array.wrap(DSL.evaluate &value)
149
+ relation
150
+ end
151
+ else
152
+ super
153
+ end
154
+ end
155
+
156
+ def where(opts = Proc.new, *rest)
157
+ if block_given? && Proc === opts
158
+ super(DSL.evaluate &opts)
159
+ else
160
+ super
161
+ end
162
+ end
163
+
164
+ def build_where(opts, other = [])
165
+ case opts
166
+ when String, Array
167
+ super
168
+ else # Let's prevent PredicateBuilder from doing its thing
169
+ [opts, *other].map do |arg|
170
+ case arg
171
+ when Array # Just in case there's an array in there somewhere
172
+ @klass.send(:sanitize_sql, arg)
173
+ when Hash
174
+ @klass.send(:expand_hash_conditions_for_aggregates, arg)
175
+ else
176
+ arg
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ def order(*args)
183
+ if block_given? && args.empty?
184
+ super(DSL.evaluate &Proc.new)
185
+ else
186
+ super
187
+ end
188
+ end
189
+
190
+ def joins(*args)
191
+ if block_given? && args.empty?
192
+ super(DSL.evaluate &Proc.new)
193
+ else
194
+ super
195
+ end
196
+ end
197
+
198
+ def having(*args)
199
+ if block_given? && args.empty?
200
+ super(DSL.evaluate &Proc.new)
201
+ else
202
+ super
203
+ end
204
+ end
205
+
206
+ def collapse_wheres(arel, wheres)
207
+ wheres = [wheres] unless Array === wheres
208
+ binaries = wheres.grep(Arel::Nodes::Binary)
209
+
210
+ groups = binaries.group_by {|b| [b.class, b.left]}
211
+
212
+ groups.each do |_, bins|
213
+ arel.where(Arel::Nodes::And.new(bins))
214
+ end
215
+
216
+ (wheres - binaries).each do |where|
217
+ where = Arel.sql(where) if String === where
218
+ arel.where(Arel::Nodes::Grouping.new(where))
219
+ end
220
+ end
221
+
222
+ def find_equality_predicates(nodes)
223
+ nodes.map { |node|
224
+ case node
225
+ when Arel::Nodes::Equality
226
+ node
227
+ when Arel::Nodes::Grouping
228
+ find_equality_predicates([node.expr])
229
+ when Arel::Nodes::And
230
+ find_equality_predicates(node.children)
231
+ else
232
+ nil
233
+ end
234
+ }.compact.flatten
235
+ end
236
+
237
+ # Simulate the logic that occurs in #to_a
238
+ #
239
+ # This will let us get a dump of the SQL that will be run against the
240
+ # DB for debug purposes without actually running the query.
241
+ def debug_sql
242
+ if eager_loading?
243
+ including = (@eager_load_values + @includes_values).uniq
244
+ join_dependency = JoinDependency.new(@klass, including, [])
245
+ construct_relation_for_association_find(join_dependency).to_sql
246
+ else
247
+ arel.to_sql
248
+ end
249
+ end
250
+
251
+ ### ZOMG ALIAS_METHOD_CHAIN IS BELOW. HIDE YOUR EYES!
252
+ # ...
253
+ # ...
254
+ # ...
255
+ # Since you're still looking, let me explain this horrible
256
+ # transgression you see before you.
257
+ # You see, Relation#where_values_hash is defined on the
258
+ # ActiveRecord::Relation class. Since it's defined there, but
259
+ # I would very much like to modify its behavior, I have three
260
+ # choices.
261
+ #
262
+ # 1. Inherit from ActiveRecord::Relation in a Squeel::Relation
263
+ # class, and make an attempt to usurp all of the various calls
264
+ # to methods on ActiveRecord::Relation by doing some really
265
+ # evil stuff with constant reassignment, all for the sake of
266
+ # being able to use super().
267
+ #
268
+ # 2. Submit a patch to Rails core, breaking this method off into
269
+ # another module, all for my own selfish desire to use super()
270
+ # while mucking about in Rails internals.
271
+ #
272
+ # 3. Use alias_method_chain, and say 10 hail Hanssons as penance.
273
+ #
274
+ # I opted to go with #3. Except for the hail Hansson thing.
275
+ # Unless you're DHH, in which case, I totally said them.
276
+
277
+ def self.included(base)
278
+ base.class_eval do
279
+ alias_method_chain :where_values_hash, :squeel
280
+ end
281
+ end
282
+
283
+ def where_values_hash_with_squeel
284
+ equalities = find_equality_predicates(predicate_visitor.accept(@where_values))
285
+
286
+ Hash[equalities.map { |where| [where.left.name, where.right] }]
287
+ end
288
+
289
+ end
290
+ end
291
+ end
292
+ end