activerecord 4.0.4 → 4.1.16

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1632 -1797
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/examples/performance.rb +30 -18
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +2 -1
  8. data/lib/active_record/association_relation.rb +4 -0
  9. data/lib/active_record/associations/alias_tracker.rb +49 -29
  10. data/lib/active_record/associations/association.rb +9 -17
  11. data/lib/active_record/associations/association_scope.rb +59 -49
  12. data/lib/active_record/associations/belongs_to_association.rb +34 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +6 -1
  14. data/lib/active_record/associations/builder/association.rb +84 -54
  15. data/lib/active_record/associations/builder/belongs_to.rb +90 -58
  16. data/lib/active_record/associations/builder/collection_association.rb +47 -45
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +119 -25
  18. data/lib/active_record/associations/builder/has_many.rb +3 -3
  19. data/lib/active_record/associations/builder/has_one.rb +5 -7
  20. data/lib/active_record/associations/builder/singular_association.rb +6 -7
  21. data/lib/active_record/associations/collection_association.rb +121 -111
  22. data/lib/active_record/associations/collection_proxy.rb +73 -18
  23. data/lib/active_record/associations/has_many_association.rb +14 -11
  24. data/lib/active_record/associations/has_many_through_association.rb +33 -6
  25. data/lib/active_record/associations/has_one_association.rb +1 -1
  26. data/lib/active_record/associations/join_dependency/join_association.rb +46 -104
  27. data/lib/active_record/associations/join_dependency/join_base.rb +6 -8
  28. data/lib/active_record/associations/join_dependency/join_part.rb +18 -37
  29. data/lib/active_record/associations/join_dependency.rb +208 -168
  30. data/lib/active_record/associations/preloader/association.rb +69 -27
  31. data/lib/active_record/associations/preloader/collection_association.rb +2 -2
  32. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  33. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/through_association.rb +58 -26
  35. data/lib/active_record/associations/preloader.rb +63 -49
  36. data/lib/active_record/associations/singular_association.rb +6 -5
  37. data/lib/active_record/associations/through_association.rb +30 -9
  38. data/lib/active_record/associations.rb +116 -42
  39. data/lib/active_record/attribute_assignment.rb +6 -3
  40. data/lib/active_record/attribute_methods/before_type_cast.rb +2 -1
  41. data/lib/active_record/attribute_methods/dirty.rb +35 -26
  42. data/lib/active_record/attribute_methods/primary_key.rb +8 -1
  43. data/lib/active_record/attribute_methods/read.rb +56 -29
  44. data/lib/active_record/attribute_methods/serialization.rb +44 -12
  45. data/lib/active_record/attribute_methods/time_zone_conversion.rb +13 -1
  46. data/lib/active_record/attribute_methods/write.rb +59 -26
  47. data/lib/active_record/attribute_methods.rb +82 -43
  48. data/lib/active_record/autosave_association.rb +209 -194
  49. data/lib/active_record/base.rb +6 -2
  50. data/lib/active_record/callbacks.rb +2 -2
  51. data/lib/active_record/coders/json.rb +13 -0
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +5 -10
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -24
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +13 -13
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +6 -3
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +90 -0
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +9 -8
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +45 -70
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +1 -0
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -96
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +74 -66
  63. data/lib/active_record/connection_adapters/column.rb +1 -35
  64. data/lib/active_record/connection_adapters/connection_specification.rb +231 -43
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +10 -5
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +24 -17
  67. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +22 -15
  68. data/lib/active_record/connection_adapters/postgresql/cast.rb +12 -4
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -44
  70. data/lib/active_record/connection_adapters/postgresql/oid.rb +38 -14
  71. data/lib/active_record/connection_adapters/postgresql/quoting.rb +37 -12
  72. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +20 -11
  73. data/lib/active_record/connection_adapters/postgresql_adapter.rb +98 -52
  74. data/lib/active_record/connection_adapters/schema_cache.rb +8 -29
  75. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -60
  76. data/lib/active_record/connection_handling.rb +39 -5
  77. data/lib/active_record/core.rb +38 -54
  78. data/lib/active_record/counter_cache.rb +9 -10
  79. data/lib/active_record/dynamic_matchers.rb +6 -2
  80. data/lib/active_record/enum.rb +199 -0
  81. data/lib/active_record/errors.rb +22 -5
  82. data/lib/active_record/fixture_set/file.rb +2 -1
  83. data/lib/active_record/fixtures.rb +173 -76
  84. data/lib/active_record/gem_version.rb +15 -0
  85. data/lib/active_record/inheritance.rb +23 -9
  86. data/lib/active_record/integration.rb +54 -1
  87. data/lib/active_record/locking/optimistic.rb +7 -2
  88. data/lib/active_record/locking/pessimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +6 -13
  90. data/lib/active_record/migration/command_recorder.rb +8 -2
  91. data/lib/active_record/migration.rb +91 -56
  92. data/lib/active_record/model_schema.rb +7 -14
  93. data/lib/active_record/nested_attributes.rb +25 -13
  94. data/lib/active_record/no_touching.rb +52 -0
  95. data/lib/active_record/null_relation.rb +26 -6
  96. data/lib/active_record/persistence.rb +23 -29
  97. data/lib/active_record/querying.rb +15 -12
  98. data/lib/active_record/railtie.rb +12 -61
  99. data/lib/active_record/railties/databases.rake +37 -56
  100. data/lib/active_record/readonly_attributes.rb +0 -6
  101. data/lib/active_record/reflection.rb +230 -79
  102. data/lib/active_record/relation/batches.rb +74 -24
  103. data/lib/active_record/relation/calculations.rb +52 -48
  104. data/lib/active_record/relation/delegation.rb +54 -39
  105. data/lib/active_record/relation/finder_methods.rb +210 -67
  106. data/lib/active_record/relation/merger.rb +15 -12
  107. data/lib/active_record/relation/predicate_builder/array_handler.rb +29 -0
  108. data/lib/active_record/relation/predicate_builder/relation_handler.rb +17 -0
  109. data/lib/active_record/relation/predicate_builder.rb +81 -40
  110. data/lib/active_record/relation/query_methods.rb +185 -108
  111. data/lib/active_record/relation/spawn_methods.rb +8 -5
  112. data/lib/active_record/relation.rb +79 -84
  113. data/lib/active_record/result.rb +45 -6
  114. data/lib/active_record/runtime_registry.rb +5 -0
  115. data/lib/active_record/sanitization.rb +4 -4
  116. data/lib/active_record/schema_dumper.rb +18 -6
  117. data/lib/active_record/schema_migration.rb +31 -18
  118. data/lib/active_record/scoping/default.rb +5 -18
  119. data/lib/active_record/scoping/named.rb +14 -29
  120. data/lib/active_record/scoping.rb +5 -0
  121. data/lib/active_record/store.rb +67 -18
  122. data/lib/active_record/tasks/database_tasks.rb +66 -26
  123. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -10
  124. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  125. data/lib/active_record/tasks/sqlite_database_tasks.rb +5 -1
  126. data/lib/active_record/timestamp.rb +6 -6
  127. data/lib/active_record/transactions.rb +10 -12
  128. data/lib/active_record/validations/presence.rb +1 -1
  129. data/lib/active_record/validations/uniqueness.rb +19 -9
  130. data/lib/active_record/version.rb +4 -7
  131. data/lib/active_record.rb +5 -7
  132. data/lib/rails/generators/active_record/migration/migration_generator.rb +4 -0
  133. data/lib/rails/generators/active_record/migration.rb +18 -0
  134. data/lib/rails/generators/active_record/model/model_generator.rb +4 -0
  135. data/lib/rails/generators/active_record.rb +2 -8
  136. metadata +18 -30
  137. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -65
  138. data/lib/active_record/associations/join_helper.rb +0 -45
  139. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +0 -56
  141. data/lib/active_record/tasks/oracle_database_tasks.rb +0 -45
  142. data/lib/active_record/tasks/sqlserver_database_tasks.rb +0 -48
  143. data/lib/active_record/test_case.rb +0 -96
@@ -1,5 +1,7 @@
1
1
  module ActiveRecord
2
2
  module FinderMethods
3
+ ONE_AS_ONE = '1 AS one'
4
+
3
5
  # Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
4
6
  # If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
5
7
  # is an integer, find by id coerces its arguments using +to_i+.
@@ -12,9 +14,11 @@ module ActiveRecord
12
14
  # Person.find([1]) # returns an array for the object with ID = 1
13
15
  # Person.where("administrator = 1").order("created_on DESC").find(1)
14
16
  #
15
- # Note that returned records may not be in the same order as the ids you
16
- # provide since database rows are unordered. Give an explicit <tt>order</tt>
17
- # to ensure the results are sorted.
17
+ # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
18
+ #
19
+ # NOTE: The returned records may not be in the same order as the ids you
20
+ # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
21
+ # option if you want the results are sorted.
18
22
  #
19
23
  # ==== Find with lock
20
24
  #
@@ -29,9 +33,37 @@ module ActiveRecord
29
33
  # person.visits += 1
30
34
  # person.save!
31
35
  # end
36
+ #
37
+ # ==== Variations of +find+
38
+ #
39
+ # Person.where(name: 'Spartacus', rating: 4)
40
+ # # returns a chainable list (which can be empty).
41
+ #
42
+ # Person.find_by(name: 'Spartacus', rating: 4)
43
+ # # returns the first item or nil.
44
+ #
45
+ # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
46
+ # # returns the first item or returns a new instance (requires you call .save to persist against the database).
47
+ #
48
+ # Person.where(name: 'Spartacus', rating: 4).first_or_create
49
+ # # returns the first item or creates it and returns it, available since Rails 3.2.1.
50
+ #
51
+ # ==== Alternatives for +find+
52
+ #
53
+ # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
54
+ # # returns a boolean indicating if any record with the given conditions exist.
55
+ #
56
+ # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
57
+ # # returns a chainable list of instances with only the mentioned fields.
58
+ #
59
+ # Person.where(name: 'Spartacus', rating: 4).ids
60
+ # # returns an Array of ids, available since Rails 3.2.1.
61
+ #
62
+ # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
63
+ # # returns an Array of the required fields, available since Rails 3.1.
32
64
  def find(*args)
33
65
  if block_given?
34
- to_a.find { |*block_args| yield(*block_args) }
66
+ to_a.find(*args) { |*block_args| yield(*block_args) }
35
67
  else
36
68
  find_with_ids(*args)
37
69
  end
@@ -80,15 +112,24 @@ module ActiveRecord
80
112
  # Person.where(["user_name = :u", { u: user_name }]).first
81
113
  # Person.order("created_on DESC").offset(5).first
82
114
  # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
115
+ #
116
+ # ==== Rails 3
117
+ #
118
+ # Person.first # SELECT "people".* FROM "people" LIMIT 1
119
+ #
120
+ # NOTE: Rails 3 may not order this query by the primary key and the order
121
+ # will depend on the database implementation. In order to ensure that behavior,
122
+ # use <tt>User.order(:id).first</tt> instead.
123
+ #
124
+ # ==== Rails 4
125
+ #
126
+ # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
127
+ #
83
128
  def first(limit = nil)
84
129
  if limit
85
- if order_values.empty? && primary_key
86
- order(arel_table[primary_key].asc).limit(limit).to_a
87
- else
88
- limit(limit).to_a
89
- end
130
+ find_nth_with_limit(offset_value, limit)
90
131
  else
91
- find_first
132
+ find_nth(:first, offset_value)
92
133
  end
93
134
  end
94
135
 
@@ -131,6 +172,86 @@ module ActiveRecord
131
172
  last or raise RecordNotFound
132
173
  end
133
174
 
175
+ # Find the second record.
176
+ # If no order is defined it will order by primary key.
177
+ #
178
+ # Person.second # returns the second object fetched by SELECT * FROM people
179
+ # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
180
+ # Person.where(["user_name = :u", { u: user_name }]).second
181
+ def second
182
+ find_nth(:second, offset_value ? offset_value + 1 : 1)
183
+ end
184
+
185
+ # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
186
+ # is found.
187
+ def second!
188
+ second or raise RecordNotFound
189
+ end
190
+
191
+ # Find the third record.
192
+ # If no order is defined it will order by primary key.
193
+ #
194
+ # Person.third # returns the third object fetched by SELECT * FROM people
195
+ # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
196
+ # Person.where(["user_name = :u", { u: user_name }]).third
197
+ def third
198
+ find_nth(:third, offset_value ? offset_value + 2 : 2)
199
+ end
200
+
201
+ # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
202
+ # is found.
203
+ def third!
204
+ third or raise RecordNotFound
205
+ end
206
+
207
+ # Find the fourth record.
208
+ # If no order is defined it will order by primary key.
209
+ #
210
+ # Person.fourth # returns the fourth object fetched by SELECT * FROM people
211
+ # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
212
+ # Person.where(["user_name = :u", { u: user_name }]).fourth
213
+ def fourth
214
+ find_nth(:fourth, offset_value ? offset_value + 3 : 3)
215
+ end
216
+
217
+ # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
218
+ # is found.
219
+ def fourth!
220
+ fourth or raise RecordNotFound
221
+ end
222
+
223
+ # Find the fifth record.
224
+ # If no order is defined it will order by primary key.
225
+ #
226
+ # Person.fifth # returns the fifth object fetched by SELECT * FROM people
227
+ # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
228
+ # Person.where(["user_name = :u", { u: user_name }]).fifth
229
+ def fifth
230
+ find_nth(:fifth, offset_value ? offset_value + 4 : 4)
231
+ end
232
+
233
+ # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
234
+ # is found.
235
+ def fifth!
236
+ fifth or raise RecordNotFound
237
+ end
238
+
239
+ # Find the forty-second record. Also known as accessing "the reddit".
240
+ # If no order is defined it will order by primary key.
241
+ #
242
+ # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
243
+ # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
244
+ # Person.where(["user_name = :u", { u: user_name }]).forty_two
245
+ def forty_two
246
+ find_nth(:forty_two, offset_value ? offset_value + 41 : 41)
247
+ end
248
+
249
+ # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
250
+ # is found.
251
+ def forty_two!
252
+ forty_two or raise RecordNotFound
253
+ end
254
+
134
255
  # Returns +true+ if a record exists in the table that matches the +id+ or
135
256
  # conditions given, or +false+ otherwise. The argument can take six forms:
136
257
  #
@@ -154,6 +275,7 @@ module ActiveRecord
154
275
  # Person.exists?(5)
155
276
  # Person.exists?('5')
156
277
  # Person.exists?(['name LIKE ?', "%#{query}%"])
278
+ # Person.exists?(id: [1, 4, 8])
157
279
  # Person.exists?(name: 'David')
158
280
  # Person.exists?(false)
159
281
  # Person.exists?
@@ -161,9 +283,10 @@ module ActiveRecord
161
283
  conditions = conditions.id if Base === conditions
162
284
  return false if !conditions
163
285
 
164
- join_dependency = construct_join_dependency_for_association_find
165
- relation = construct_relation_for_association_find(join_dependency)
166
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
286
+ relation = apply_join_dependency(self, construct_join_dependency)
287
+ return false if ActiveRecord::NullRelation === relation
288
+
289
+ relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
167
290
 
168
291
  case conditions
169
292
  when Array, Hash
@@ -173,8 +296,6 @@ module ActiveRecord
173
296
  end
174
297
 
175
298
  connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
176
- rescue ThrowResult
177
- false
178
299
  end
179
300
 
180
301
  # This method is called whenever no records are found with either a single
@@ -190,73 +311,96 @@ module ActiveRecord
190
311
  conditions = " [#{conditions}]" if conditions
191
312
 
192
313
  if Array(ids).size == 1
193
- error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}"
314
+ error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}"
194
315
  else
195
- error = "Couldn't find all #{@klass.name.pluralize} with IDs "
316
+ error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': "
196
317
  error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
197
318
  end
198
319
 
199
320
  raise RecordNotFound, error
200
321
  end
201
322
 
202
- protected
323
+ private
203
324
 
204
325
  def find_with_associations
205
- join_dependency = construct_join_dependency_for_association_find
206
- relation = construct_relation_for_association_find(join_dependency)
207
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
208
- join_dependency.instantiate(rows)
209
- rescue ThrowResult
210
- []
211
- end
326
+ # NOTE: the JoinDependency constructed here needs to know about
327
+ # any joins already present in `self`, so pass them in
328
+ #
329
+ # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
330
+ # incorrect SQL is generated. In that case, the join dependency for
331
+ # SpecialCategorizations is constructed without knowledge of the
332
+ # preexisting join in joins_values to categorizations (by way of
333
+ # the `has_many :through` for categories).
334
+ #
335
+ join_dependency = construct_join_dependency(joins_values)
336
+
337
+ aliases = join_dependency.aliases
338
+ relation = select aliases.columns
339
+ relation = apply_join_dependency(relation, join_dependency)
212
340
 
213
- def construct_join_dependency_for_association_find
214
- including = (eager_load_values + includes_values).uniq
215
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
341
+ if block_given?
342
+ yield relation
343
+ else
344
+ if ActiveRecord::NullRelation === relation
345
+ []
346
+ else
347
+ rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
348
+ join_dependency.instantiate(rows, aliases)
349
+ end
350
+ end
216
351
  end
217
352
 
218
- def construct_relation_for_association_calculations
219
- including = (eager_load_values + includes_values).uniq
220
- join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
221
- relation = except(:includes, :eager_load, :preload)
222
- apply_join_dependency(relation, join_dependency)
353
+ def construct_join_dependency(joins = [])
354
+ including = eager_load_values + includes_values
355
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
223
356
  end
224
357
 
225
- def construct_relation_for_association_find(join_dependency)
226
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns + select_values)
227
- apply_join_dependency(relation, join_dependency)
358
+ def construct_relation_for_association_calculations
359
+ from = arel.froms.first
360
+ if Arel::Table === from
361
+ apply_join_dependency(self, construct_join_dependency(joins_values))
362
+ else
363
+ # FIXME: as far as I can tell, `from` will always be an Arel::Table.
364
+ # There are no tests that test this branch, but presumably it's
365
+ # possible for `from` to be a list?
366
+ apply_join_dependency(self, construct_join_dependency(from))
367
+ end
228
368
  end
229
369
 
230
370
  def apply_join_dependency(relation, join_dependency)
231
- join_dependency.join_associations.each do |association|
232
- relation = association.join_relation(relation)
233
- end
234
-
235
- limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
371
+ relation = relation.except(:includes, :eager_load, :preload)
372
+ relation = relation.joins join_dependency
236
373
 
237
- if !limitable_reflections && relation.limit_value
238
- limited_id_condition = construct_limited_ids_condition(relation.except(:select))
239
- relation = relation.where(limited_id_condition)
374
+ if using_limitable_reflections?(join_dependency.reflections)
375
+ relation
376
+ else
377
+ if relation.limit_value
378
+ limited_ids = limited_ids_for(relation)
379
+ limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
380
+ end
381
+ relation.except(:limit, :offset)
240
382
  end
241
-
242
- relation = relation.except(:limit, :offset) unless limitable_reflections
243
-
244
- relation
245
383
  end
246
384
 
247
- def construct_limited_ids_condition(relation)
248
- orders = relation.order_values.map { |val| val.presence }.compact
249
- values = @klass.connection.columns_for_distinct("#{quoted_table_name}.#{quoted_primary_key}", orders)
385
+ def limited_ids_for(relation)
386
+ values = @klass.connection.columns_for_distinct(
387
+ "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
250
388
 
251
- relation = relation.dup.select(values).distinct!
389
+ relation = relation.except(:select).select(values).distinct!
252
390
 
253
391
  id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
254
- ids_array = id_rows.map {|row| row[primary_key]}
392
+ id_rows.map {|row| row[primary_key]}
393
+ end
255
394
 
256
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
395
+ def using_limitable_reflections?(reflections)
396
+ reflections.none? { |r| r.collection? }
257
397
  end
258
398
 
399
+ protected
400
+
259
401
  def find_with_ids(*ids)
402
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
403
+
260
404
  expects_array = ids.first.kind_of?(Array)
261
405
  return ids.first if expects_array && ids.first.empty?
262
406
 
@@ -317,16 +461,19 @@ module ActiveRecord
317
461
  end
318
462
  end
319
463
 
320
- def find_first
464
+ def find_nth(ordinal, offset)
321
465
  if loaded?
322
- @records.first
466
+ @records.send(ordinal)
323
467
  else
324
- @first ||=
325
- if with_default_scope.order_values.empty? && primary_key
326
- order(arel_table[primary_key].asc).limit(1).to_a.first
327
- else
328
- limit(1).to_a.first
329
- end
468
+ @offsets[offset] ||= find_nth_with_limit(offset, 1).first
469
+ end
470
+ end
471
+
472
+ def find_nth_with_limit(offset, limit)
473
+ if order_values.empty? && primary_key
474
+ order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a
475
+ else
476
+ limit(limit).offset(offset).to_a
330
477
  end
331
478
  end
332
479
 
@@ -335,16 +482,12 @@ module ActiveRecord
335
482
  @records.last
336
483
  else
337
484
  @last ||=
338
- if offset_value || limit_value
485
+ if limit_value
339
486
  to_a.last
340
487
  else
341
488
  reverse_order.limit(1).to_a.first
342
489
  end
343
490
  end
344
491
  end
345
-
346
- def using_limitable_reflections?(reflections)
347
- reflections.none? { |r| r.collection? }
348
- end
349
492
  end
350
493
  end
@@ -22,7 +22,7 @@ module ActiveRecord
22
22
  # build a relation to merge in rather than directly merging
23
23
  # the values.
24
24
  def other
25
- other = Relation.new(relation.klass, relation.table)
25
+ other = Relation.create(relation.klass, relation.table)
26
26
  hash.each { |k, v|
27
27
  if k == :joins
28
28
  if Hash === v
@@ -30,6 +30,8 @@ module ActiveRecord
30
30
  else
31
31
  other.joins!(*v)
32
32
  end
33
+ elsif k == :select
34
+ other._select!(v)
33
35
  else
34
36
  other.send("#{k}!", v)
35
37
  end
@@ -42,10 +44,6 @@ module ActiveRecord
42
44
  attr_reader :relation, :values, :other
43
45
 
44
46
  def initialize(relation, other)
45
- if other.default_scoped? && other.klass != relation.klass
46
- other = other.with_default_scope
47
- end
48
-
49
47
  @relation = relation
50
48
  @values = other.values
51
49
  @other = other
@@ -66,7 +64,13 @@ module ActiveRecord
66
64
  # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
67
65
  # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
68
66
  # don't fall through the cracks.
69
- relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value)
67
+ unless value.nil? || (value.blank? && false != value)
68
+ if name == :select
69
+ relation._select!(*value)
70
+ else
71
+ relation.send("#{name}!", *value)
72
+ end
73
+ end
70
74
  end
71
75
 
72
76
  merge_multi_values
@@ -98,9 +102,7 @@ module ActiveRecord
98
102
  [])
99
103
  relation.joins! rest
100
104
 
101
- join_dependency.join_associations.each do |association|
102
- @relation = association.join_relation(relation)
103
- end
105
+ @relation = relation.joins join_dependency
104
106
  end
105
107
  end
106
108
 
@@ -135,7 +137,7 @@ module ActiveRecord
135
137
  # override any order specified in the original relation
136
138
  relation.reorder! values[:order]
137
139
  elsif values[:order]
138
- # merge in order_values from r
140
+ # merge in order_values from relation
139
141
  relation.order! values[:order]
140
142
  end
141
143
 
@@ -153,7 +155,9 @@ module ActiveRecord
153
155
  end
154
156
 
155
157
  def filter_binds(lhs_binds, removed_wheres)
156
- set = Set.new removed_wheres.map { |x| x.left.name }
158
+ return lhs_binds if removed_wheres.empty?
159
+
160
+ set = Set.new removed_wheres.map { |x| x.left.name.to_s }
157
161
  lhs_binds.dup.delete_if { |col,_| set.include? col.name }
158
162
  end
159
163
 
@@ -174,7 +178,6 @@ module ActiveRecord
174
178
  w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
175
179
  end
176
180
  end
177
-
178
181
  end
179
182
  end
180
183
  end
@@ -0,0 +1,29 @@
1
+ module ActiveRecord
2
+ class PredicateBuilder
3
+ class ArrayHandler # :nodoc:
4
+ def call(attribute, value)
5
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
6
+ ranges, values = values.partition { |v| v.is_a?(Range) }
7
+
8
+ values_predicate = if values.include?(nil)
9
+ values = values.compact
10
+
11
+ case values.length
12
+ when 0
13
+ attribute.eq(nil)
14
+ when 1
15
+ attribute.eq(values.first).or(attribute.eq(nil))
16
+ else
17
+ attribute.in(values).or(attribute.eq(nil))
18
+ end
19
+ else
20
+ attribute.in(values)
21
+ end
22
+
23
+ array_predicates = ranges.map { |range| attribute.in(range) }
24
+ array_predicates << values_predicate
25
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ class PredicateBuilder
3
+ class RelationHandler # :nodoc:
4
+ def call(attribute, value)
5
+ if value.select_values.empty?
6
+ value = value.select(value.klass.arel_table[value.klass.primary_key])
7
+ end
8
+
9
+ value = value.dup
10
+ value.where_values = value.where_values.map do |node|
11
+ node.dup rescue node
12
+ end
13
+ attribute.in(value.arel.ast)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,20 @@
1
1
  module ActiveRecord
2
2
  class PredicateBuilder # :nodoc:
3
+ @handlers = []
4
+
5
+ autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
6
+ autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
7
+
8
+ def self.resolve_column_aliases(klass, hash)
9
+ hash = hash.dup
10
+ hash.keys.grep(Symbol) do |key|
11
+ if klass.attribute_alias? key
12
+ hash[klass.attribute_alias(key)] = hash.delete key
13
+ end
14
+ end
15
+ hash
16
+ end
17
+
3
18
  def self.build_from_hash(klass, attributes, default_table)
4
19
  queries = []
5
20
 
@@ -11,7 +26,7 @@ module ActiveRecord
11
26
  queries << '1=0'
12
27
  else
13
28
  table = Arel::Table.new(column, default_table.engine)
14
- association = klass.reflect_on_association(column.to_sym)
29
+ association = klass._reflect_on_association(column.to_sym)
15
30
 
16
31
  value.each do |k, v|
17
32
  queries.concat expand(association && association.klass, table, k, v)
@@ -40,18 +55,36 @@ module ActiveRecord
40
55
  #
41
56
  # For polymorphic relationships, find the foreign key and type:
42
57
  # PriceEstimate.where(estimate_of: treasure)
43
- if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
44
- if reflection.polymorphic?
45
- queries << build(table[reflection.foreign_type], value.class.base_class)
58
+ if klass && reflection = klass._reflect_on_association(column.to_sym)
59
+ base_class = polymorphic_base_class_from_value(value)
60
+ if reflection.polymorphic? && base_class
61
+ queries << build(table[reflection.foreign_type], base_class)
46
62
  end
47
63
 
48
64
  column = reflection.foreign_key
65
+
66
+ if base_class
67
+ primary_key = reflection.association_primary_key(base_class)
68
+ value = convert_value_to_association_ids(value, primary_key)
69
+ end
49
70
  end
50
71
 
51
72
  queries << build(table[column], value)
52
73
  queries
53
74
  end
54
75
 
76
+ def self.polymorphic_base_class_from_value(value)
77
+ case value
78
+ when Relation
79
+ value.klass.base_class
80
+ when Array
81
+ val = value.compact.first
82
+ val.class.base_class if val.is_a?(Base)
83
+ when Base
84
+ value.class.base_class
85
+ end
86
+ end
87
+
55
88
  def self.references(attributes)
56
89
  attributes.map do |key, value|
57
90
  if value.is_a?(Hash)
@@ -63,44 +96,52 @@ module ActiveRecord
63
96
  end.compact
64
97
  end
65
98
 
99
+ # Define how a class is converted to Arel nodes when passed to +where+.
100
+ # The handler can be any object that responds to +call+, and will be used
101
+ # for any value that +===+ the class given. For example:
102
+ #
103
+ # MyCustomDateRange = Struct.new(:start, :end)
104
+ # handler = proc do |column, range|
105
+ # Arel::Nodes::Between.new(column,
106
+ # Arel::Nodes::And.new([range.start, range.end])
107
+ # )
108
+ # end
109
+ # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
110
+ def self.register_handler(klass, handler)
111
+ @handlers.unshift([klass, handler])
112
+ end
113
+
114
+ register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) })
115
+ # FIXME: I think we need to deprecate this behavior
116
+ register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
117
+ register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
118
+ register_handler(Range, ->(attribute, value) { attribute.in(value) })
119
+ register_handler(Relation, RelationHandler.new)
120
+ register_handler(Array, ArrayHandler.new)
121
+
66
122
  private
67
- def self.build(attribute, value)
68
- case value
69
- when Array
70
- values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
71
- ranges, values = values.partition {|v| v.is_a?(Range)}
72
-
73
- values_predicate = if values.include?(nil)
74
- values = values.compact
75
-
76
- case values.length
77
- when 0
78
- attribute.eq(nil)
79
- when 1
80
- attribute.eq(values.first).or(attribute.eq(nil))
81
- else
82
- attribute.in(values).or(attribute.eq(nil))
83
- end
84
- else
85
- attribute.in(values)
86
- end
87
123
 
88
- array_predicates = ranges.map { |range| attribute.in(range) }
89
- array_predicates << values_predicate
90
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
91
- when ActiveRecord::Relation
92
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
93
- attribute.in(value.arel.ast)
94
- when Range
95
- attribute.in(value)
96
- when ActiveRecord::Base
97
- attribute.eq(value.id)
98
- when Class
99
- # FIXME: I think we need to deprecate this behavior
100
- attribute.eq(value.name)
101
- else
102
- attribute.eq(value)
103
- end
124
+ def self.build(attribute, value)
125
+ handler_for(value).call(attribute, value)
126
+ end
127
+ private_class_method :build
128
+
129
+ def self.handler_for(object)
130
+ @handlers.detect { |klass, _| klass === object }.last
131
+ end
132
+ private_class_method :handler_for
133
+
134
+ def self.convert_value_to_association_ids(value, primary_key)
135
+ case value
136
+ when Relation
137
+ value.select(primary_key)
138
+ when Array
139
+ value.map { |v| convert_value_to_association_ids(v, primary_key) }
140
+ when Base
141
+ value.read_attribute(primary_key)
142
+ else
143
+ value
104
144
  end
145
+ end
105
146
  end
106
147
  end