squeel 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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