activerecord 3.0.20 → 3.1.0.beta1

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 (122) hide show
  1. data/CHANGELOG +220 -91
  2. data/README.rdoc +3 -3
  3. data/examples/performance.rb +88 -109
  4. data/lib/active_record.rb +6 -2
  5. data/lib/active_record/aggregations.rb +22 -45
  6. data/lib/active_record/associations.rb +264 -991
  7. data/lib/active_record/associations/alias_tracker.rb +85 -0
  8. data/lib/active_record/associations/association.rb +231 -0
  9. data/lib/active_record/associations/association_scope.rb +120 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +40 -60
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +15 -63
  12. data/lib/active_record/associations/builder/association.rb +53 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +63 -0
  16. data/lib/active_record/associations/builder/has_many.rb +65 -0
  17. data/lib/active_record/associations/builder/has_one.rb +63 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +524 -0
  20. data/lib/active_record/associations/collection_proxy.rb +125 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +27 -118
  22. data/lib/active_record/associations/has_many_association.rb +50 -79
  23. data/lib/active_record/associations/has_many_through_association.rb +98 -67
  24. data/lib/active_record/associations/has_one_association.rb +45 -115
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency.rb +215 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +150 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_helper.rb +56 -0
  31. data/lib/active_record/associations/preloader.rb +177 -0
  32. data/lib/active_record/associations/preloader/association.rb +126 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  42. data/lib/active_record/associations/singular_association.rb +55 -0
  43. data/lib/active_record/associations/through_association.rb +80 -0
  44. data/lib/active_record/attribute_methods.rb +19 -5
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +9 -8
  46. data/lib/active_record/attribute_methods/dirty.rb +8 -2
  47. data/lib/active_record/attribute_methods/primary_key.rb +33 -13
  48. data/lib/active_record/attribute_methods/read.rb +17 -17
  49. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -4
  50. data/lib/active_record/attribute_methods/write.rb +2 -1
  51. data/lib/active_record/autosave_association.rb +66 -45
  52. data/lib/active_record/base.rb +445 -273
  53. data/lib/active_record/callbacks.rb +24 -33
  54. data/lib/active_record/coders/yaml_column.rb +41 -0
  55. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +106 -13
  56. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +16 -2
  57. data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
  58. data/lib/active_record/connection_adapters/abstract/database_statements.rb +83 -12
  59. data/lib/active_record/connection_adapters/abstract/query_cache.rb +16 -16
  60. data/lib/active_record/connection_adapters/abstract/quoting.rb +61 -22
  61. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +16 -273
  62. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -42
  63. data/lib/active_record/connection_adapters/abstract_adapter.rb +44 -25
  64. data/lib/active_record/connection_adapters/column.rb +268 -0
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +686 -0
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +331 -88
  67. data/lib/active_record/connection_adapters/postgresql_adapter.rb +295 -267
  68. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +3 -7
  69. data/lib/active_record/connection_adapters/sqlite_adapter.rb +108 -26
  70. data/lib/active_record/counter_cache.rb +7 -4
  71. data/lib/active_record/fixtures.rb +174 -192
  72. data/lib/active_record/identity_map.rb +131 -0
  73. data/lib/active_record/locking/optimistic.rb +20 -14
  74. data/lib/active_record/locking/pessimistic.rb +4 -4
  75. data/lib/active_record/log_subscriber.rb +24 -4
  76. data/lib/active_record/migration.rb +265 -144
  77. data/lib/active_record/migration/command_recorder.rb +103 -0
  78. data/lib/active_record/named_scope.rb +68 -25
  79. data/lib/active_record/nested_attributes.rb +58 -15
  80. data/lib/active_record/observer.rb +3 -7
  81. data/lib/active_record/persistence.rb +58 -38
  82. data/lib/active_record/query_cache.rb +25 -3
  83. data/lib/active_record/railtie.rb +21 -12
  84. data/lib/active_record/railties/console_sandbox.rb +6 -0
  85. data/lib/active_record/railties/databases.rake +147 -116
  86. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  87. data/lib/active_record/reflection.rb +176 -44
  88. data/lib/active_record/relation.rb +125 -49
  89. data/lib/active_record/relation/batches.rb +7 -5
  90. data/lib/active_record/relation/calculations.rb +50 -18
  91. data/lib/active_record/relation/finder_methods.rb +47 -26
  92. data/lib/active_record/relation/predicate_builder.rb +24 -21
  93. data/lib/active_record/relation/query_methods.rb +117 -101
  94. data/lib/active_record/relation/spawn_methods.rb +27 -20
  95. data/lib/active_record/result.rb +34 -0
  96. data/lib/active_record/schema.rb +5 -6
  97. data/lib/active_record/schema_dumper.rb +11 -13
  98. data/lib/active_record/serialization.rb +2 -2
  99. data/lib/active_record/serializers/xml_serializer.rb +10 -10
  100. data/lib/active_record/session_store.rb +8 -2
  101. data/lib/active_record/test_case.rb +9 -20
  102. data/lib/active_record/timestamp.rb +21 -9
  103. data/lib/active_record/transactions.rb +16 -15
  104. data/lib/active_record/validations.rb +21 -22
  105. data/lib/active_record/validations/associated.rb +3 -1
  106. data/lib/active_record/validations/uniqueness.rb +48 -58
  107. data/lib/active_record/version.rb +3 -3
  108. data/lib/rails/generators/active_record.rb +6 -0
  109. data/lib/rails/generators/active_record/migration/templates/migration.rb +10 -2
  110. data/lib/rails/generators/active_record/model/model_generator.rb +2 -1
  111. data/lib/rails/generators/active_record/model/templates/migration.rb +6 -5
  112. data/lib/rails/generators/active_record/model/templates/model.rb +2 -0
  113. data/lib/rails/generators/active_record/model/templates/module.rb +2 -0
  114. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  115. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +2 -1
  116. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +2 -2
  117. metadata +106 -77
  118. checksums.yaml +0 -7
  119. data/lib/active_record/association_preload.rb +0 -431
  120. data/lib/active_record/associations/association_collection.rb +0 -572
  121. data/lib/active_record/associations/association_proxy.rb +0 -304
  122. data/lib/active_record/associations/through_association_scope.rb +0 -160
@@ -1,7 +1,7 @@
1
1
  require 'active_support/core_ext/object/blank'
2
2
 
3
3
  module ActiveRecord
4
- module Batches # :nodoc:
4
+ module Batches
5
5
  # Yields each record that was found by the find +options+. The find is
6
6
  # performed by find_in_batches with a batch size of 1000 (or as
7
7
  # specified by the <tt>:batch_size</tt> option).
@@ -20,6 +20,8 @@ module ActiveRecord
20
20
  find_in_batches(options) do |records|
21
21
  records.each { |record| yield record }
22
22
  end
23
+
24
+ self
23
25
  end
24
26
 
25
27
  # Yields each batch of records that was found by the find +options+ as
@@ -37,7 +39,7 @@ module ActiveRecord
37
39
  # ascending on the primary key ("id ASC") to make the batch ordering
38
40
  # work. This also mean that this method only works with integer-based
39
41
  # primary keys. You can't set the limit either, that's used to control
40
- # the the batch sizes.
42
+ # the batch sizes.
41
43
  #
42
44
  # Example:
43
45
  #
@@ -63,7 +65,7 @@ module ActiveRecord
63
65
  batch_size = options.delete(:batch_size) || 1000
64
66
 
65
67
  relation = relation.except(:order).order(batch_order).limit(batch_size)
66
- records = relation.where(primary_key.gteq(start)).all
68
+ records = relation.where(table[primary_key].gteq(start)).all
67
69
 
68
70
  while records.any?
69
71
  yield records
@@ -71,7 +73,7 @@ module ActiveRecord
71
73
  break if records.size < batch_size
72
74
 
73
75
  if primary_key_offset = records.last.id
74
- records = relation.where(primary_key.gt(primary_key_offset)).to_a
76
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
75
77
  else
76
78
  raise "Primary key not included in the custom select clause"
77
79
  end
@@ -81,7 +83,7 @@ module ActiveRecord
81
83
  private
82
84
 
83
85
  def batch_order
84
- "#{@klass.quoted_table_name}.#{@klass.quoted_primary_key} ASC"
86
+ "#{quoted_table_name}.#{quoted_primary_key} ASC"
85
87
  end
86
88
  end
87
89
  end
@@ -166,9 +166,9 @@ module ActiveRecord
166
166
  if operation == "count"
167
167
  column_name ||= (select_for_count || :all)
168
168
 
169
- if arel.join_sql =~ /LEFT OUTER/i
169
+ unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
170
170
  distinct = true
171
- column_name = @klass.primary_key if column_name == :all
171
+ column_name = primary_key if column_name == :all
172
172
  end
173
173
 
174
174
  distinct = nil if column_name =~ /\s*DISTINCT\s+/i
@@ -196,29 +196,36 @@ module ActiveRecord
196
196
  end
197
197
 
198
198
  def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
199
- column = aggregate_column(column_name)
200
-
201
199
  # Postgresql doesn't like ORDER BY when there are no GROUP BY
202
200
  relation = except(:order)
203
- select_value = operation_over_aggregate_column(column, operation, distinct)
204
201
 
205
- relation.select_values = [select_value]
202
+ if operation == "count" && (relation.limit_value || relation.offset_value)
203
+ # Shortcut when limit is zero.
204
+ return 0 if relation.limit_value == 0
205
+
206
+ query_builder = build_count_subquery(relation, column_name, distinct)
207
+ else
208
+ column = aggregate_column(column_name)
209
+
210
+ select_value = operation_over_aggregate_column(column, operation, distinct)
211
+
212
+ relation.select_values = [select_value]
213
+
214
+ query_builder = relation.arel
215
+ end
206
216
 
207
- type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
217
+ type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
208
218
  end
209
219
 
210
220
  def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
211
221
  group_attr = @group_values
212
222
  association = @klass.reflect_on_association(group_attr.first.to_sym)
213
223
  associated = group_attr.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
214
- group_fields = Array(associated ? association.primary_key_name : group_attr)
215
- group_aliases = []
216
- group_columns = {}
217
-
218
- group_fields.each do |field|
219
- group_aliases << column_alias_for(field)
220
- group_columns[column_alias_for(field)] = column_for(field)
221
- end
224
+ group_fields = Array(associated ? association.foreign_key : group_attr)
225
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
226
+ group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
227
+ [aliaz, column_for(field)]
228
+ }
222
229
 
223
230
  group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
224
231
 
@@ -228,9 +235,19 @@ module ActiveRecord
228
235
  aggregate_alias = column_alias_for(operation, column_name)
229
236
  end
230
237
 
238
+ select_values = [
239
+ operation_over_aggregate_column(
240
+ aggregate_column(column_name),
241
+ operation,
242
+ distinct).as(aggregate_alias)
243
+ ]
244
+
245
+ select_values.concat group_fields.zip(group_aliases).map { |field,aliaz|
246
+ "#{field} AS #{aliaz}"
247
+ }
248
+
231
249
  relation = except(:group).group(group.join(','))
232
- relation.select_values = [ operation_over_aggregate_column(aggregate_column(column_name), operation, distinct).as(aggregate_alias) ]
233
- group_fields.each_index{ |i| relation.select_values << "#{group_fields[i]} AS #{group_aliases[i]}" }
250
+ relation.select_values = select_values
234
251
 
235
252
  calculated_data = @klass.connection.select_all(relation.to_sql)
236
253
 
@@ -241,7 +258,9 @@ module ActiveRecord
241
258
  end
242
259
 
243
260
  ActiveSupport::OrderedHash[calculated_data.map do |row|
244
- key = group_aliases.map{|group_alias| type_cast_calculated_value(row[group_alias], group_columns[group_alias])}
261
+ key = group_columns.map { |aliaz, column|
262
+ type_cast_calculated_value(row[aliaz], column)
263
+ }
245
264
  key = key.first if key.size == 1
246
265
  key = key_records[key] if associated
247
266
  [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
@@ -291,5 +310,18 @@ module ActiveRecord
291
310
  select if select !~ /(,|\*)/
292
311
  end
293
312
  end
313
+
314
+ def build_count_subquery(relation, column_name, distinct)
315
+ column_alias = Arel.sql('count_column')
316
+ subquery_alias = Arel.sql('subquery_for_count')
317
+
318
+ aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
319
+ relation.select_values = [aliased_column]
320
+ subquery = relation.arel.as(subquery_alias)
321
+
322
+ sm = Arel::SelectManager.new relation.engine
323
+ select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
324
+ sm.project(select_value).from(subquery)
325
+ end
294
326
  end
295
327
  end
@@ -19,7 +19,7 @@ module ActiveRecord
19
19
  #
20
20
  # All approaches accept an options hash as their last parameter.
21
21
  #
22
- # ==== Parameters
22
+ # ==== Options
23
23
  #
24
24
  # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1", <tt>["user_name = ?", username]</tt>,
25
25
  # or <tt>["user_name = :user_name", { :user_name => user_name }]</tt>. See conditions in the intro.
@@ -123,6 +123,12 @@ module ActiveRecord
123
123
  end
124
124
  end
125
125
 
126
+ # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
127
+ # is found. Note that <tt>first!</tt> accepts no arguments.
128
+ def first!
129
+ first or raise RecordNotFound
130
+ end
131
+
126
132
  # A convenience wrapper for <tt>find(:last, *args)</tt>. You can pass in all the
127
133
  # same arguments to this method as you can to <tt>find(:last)</tt>.
128
134
  def last(*args)
@@ -137,6 +143,12 @@ module ActiveRecord
137
143
  end
138
144
  end
139
145
 
146
+ # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
147
+ # is found. Note that <tt>last!</tt> accepts no arguments.
148
+ def last!
149
+ last or raise RecordNotFound
150
+ end
151
+
140
152
  # A convenience wrapper for <tt>find(:all, *args)</tt>. You can pass in all the
141
153
  # same arguments to this method as you can to <tt>find(:all)</tt>.
142
154
  def all(*args)
@@ -168,9 +180,7 @@ module ActiveRecord
168
180
  # Person.exists?(:name => "David")
169
181
  # Person.exists?(['name LIKE ?', "%#{query}%"])
170
182
  # Person.exists?
171
- def exists?(id = false)
172
- return false if id.nil?
173
-
183
+ def exists?(id = nil)
174
184
  id = id.id if ActiveRecord::Base === id
175
185
 
176
186
  join_dependency = construct_join_dependency_for_association_find
@@ -181,7 +191,7 @@ module ActiveRecord
181
191
  when Array, Hash
182
192
  relation = relation.where(id)
183
193
  else
184
- relation = relation.where(table[primary_key.name].eq(id)) if id
194
+ relation = relation.where(table[primary_key].eq(id)) if id
185
195
  end
186
196
 
187
197
  connection.select_value(relation.to_sql) ? true : false
@@ -191,7 +201,8 @@ module ActiveRecord
191
201
 
192
202
  def find_with_associations
193
203
  join_dependency = construct_join_dependency_for_association_find
194
- rows = construct_relation_for_association_find(join_dependency).to_a
204
+ relation = construct_relation_for_association_find(join_dependency)
205
+ rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values)
195
206
  join_dependency.instantiate(rows)
196
207
  rescue ThrowResult
197
208
  []
@@ -199,18 +210,18 @@ module ActiveRecord
199
210
 
200
211
  def construct_join_dependency_for_association_find
201
212
  including = (@eager_load_values + @includes_values).uniq
202
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, nil)
213
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
203
214
  end
204
215
 
205
216
  def construct_relation_for_association_calculations
206
217
  including = (@eager_load_values + @includes_values).uniq
207
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.join_sql)
218
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
208
219
  relation = except(:includes, :eager_load, :preload)
209
220
  apply_join_dependency(relation, join_dependency)
210
221
  end
211
222
 
212
223
  def construct_relation_for_association_find(join_dependency)
213
- relation = except(:includes, :eager_load, :preload, :select).select(column_aliases(join_dependency))
224
+ relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
214
225
  apply_join_dependency(relation, join_dependency)
215
226
  end
216
227
 
@@ -232,11 +243,13 @@ module ActiveRecord
232
243
  end
233
244
 
234
245
  def construct_limited_ids_condition(relation)
235
- orders = relation.order_values.join(", ")
236
- values = @klass.connection.distinct("#{@klass.connection.quote_table_name @klass.table_name}.#{@klass.primary_key}", orders)
246
+ orders = relation.order_values
247
+ values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
237
248
 
238
- ids_array = relation.select(values).collect {|row| row[@klass.primary_key]}
239
- ids_array.empty? ? raise(ThrowResult) : primary_key.in(ids_array)
249
+ relation = relation.dup
250
+
251
+ ids_array = relation.select(values).collect {|row| row[primary_key]}
252
+ ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
240
253
  end
241
254
 
242
255
  def find_by_attributes(match, attributes, *args)
@@ -266,8 +279,8 @@ module ActiveRecord
266
279
 
267
280
  unless record
268
281
  record = @klass.new do |r|
269
- r.send(:attributes=, protected_attributes_for_create, true) unless protected_attributes_for_create.empty?
270
- r.send(:attributes=, unprotected_attributes_for_create, false) unless unprotected_attributes_for_create.empty?
282
+ r.assign_attributes(protected_attributes_for_create)
283
+ r.assign_attributes(unprotected_attributes_for_create, :without_protection => true)
271
284
  end
272
285
  yield(record) if block_given?
273
286
  record.save if match.instantiator == :create
@@ -298,19 +311,33 @@ module ActiveRecord
298
311
  def find_one(id)
299
312
  id = id.id if ActiveRecord::Base === id
300
313
 
301
- record = where(primary_key.eq(id)).first
314
+ if IdentityMap.enabled? && where_values.blank? &&
315
+ limit_value.blank? && order_values.blank? &&
316
+ includes_values.blank? && preload_values.blank? &&
317
+ readonly_value.nil? && joins_values.blank? &&
318
+ !@klass.locking_enabled? &&
319
+ record = IdentityMap.get(@klass, id)
320
+ return record
321
+ end
322
+
323
+ column = columns_hash[primary_key]
324
+
325
+ substitute = connection.substitute_at(column, @bind_values.length)
326
+ relation = where(table[primary_key].eq(substitute))
327
+ relation.bind_values = [[column, id]]
328
+ record = relation.first
302
329
 
303
330
  unless record
304
331
  conditions = arel.where_sql
305
332
  conditions = " [#{conditions}]" if conditions
306
- raise RecordNotFound, "Couldn't find #{@klass.name} with ID=#{id}#{conditions}"
333
+ raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}"
307
334
  end
308
335
 
309
336
  record
310
337
  end
311
338
 
312
339
  def find_some(ids)
313
- result = where(primary_key.in(ids)).all
340
+ result = where(table[primary_key].in(ids)).all
314
341
 
315
342
  expected_size =
316
343
  if @limit_value && ids.size > @limit_value
@@ -327,8 +354,8 @@ module ActiveRecord
327
354
  if result.size == expected_size
328
355
  result
329
356
  else
330
- conditions = arel.wheres.map { |x| x.value }.join(', ')
331
- conditions = " [WHERE #{conditions}]" if conditions.present?
357
+ conditions = arel.where_sql
358
+ conditions = " [#{conditions}]" if conditions
332
359
 
333
360
  error = "Couldn't find all #{@klass.name.pluralize} with IDs "
334
361
  error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
@@ -352,14 +379,8 @@ module ActiveRecord
352
379
  end
353
380
  end
354
381
 
355
- def column_aliases(join_dependency)
356
- join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
357
- "#{connection.quote_table_name join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
358
- end
359
-
360
382
  def using_limitable_reflections?(reflections)
361
383
  reflections.none? { |r| r.collection? }
362
384
  end
363
-
364
385
  end
365
386
  end
@@ -1,38 +1,42 @@
1
1
  module ActiveRecord
2
- class PredicateBuilder
3
-
4
- def initialize(engine)
5
- @engine = engine
6
- end
7
-
8
- def build_from_hash(attributes, default_table, allow_table_name = true)
2
+ class PredicateBuilder # :nodoc:
3
+ def self.build_from_hash(engine, attributes, default_table)
9
4
  predicates = attributes.map do |column, value|
10
5
  table = default_table
11
6
 
12
- if allow_table_name && value.is_a?(Hash)
13
- table = Arel::Table.new(column, :engine => @engine)
14
-
15
- if value.empty?
16
- '1 = 2'
17
- else
18
- build_from_hash(value, table, false)
19
- end
7
+ if value.is_a?(Hash)
8
+ table = Arel::Table.new(column, engine)
9
+ build_from_hash(engine, value, table)
20
10
  else
21
11
  column = column.to_s
22
12
 
23
- if allow_table_name && column.include?('.')
13
+ if column.include?('.')
24
14
  table_name, column = column.split('.', 2)
25
- table = Arel::Table.new(table_name, :engine => @engine)
15
+ table = Arel::Table.new(table_name, engine)
26
16
  end
27
17
 
28
- attribute = table[column] || Arel::Attribute.new(table, column)
18
+ attribute = table[column.to_sym]
29
19
 
30
20
  case value
31
- when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation
21
+ when ActiveRecord::Relation
22
+ value.select_values = [value.klass.arel_table['id']] if value.select_values.empty?
23
+ attribute.in(value.arel.ast)
24
+ when Array, ActiveRecord::Associations::CollectionProxy
32
25
  values = value.to_a.map { |x|
33
26
  x.is_a?(ActiveRecord::Base) ? x.id : x
34
27
  }
35
- attribute.in(values)
28
+
29
+ if values.include?(nil)
30
+ values = values.compact
31
+ if values.empty?
32
+ attribute.eq nil
33
+ else
34
+ attribute.in(values.compact).or attribute.eq(nil)
35
+ end
36
+ else
37
+ attribute.in(values)
38
+ end
39
+
36
40
  when Range, Arel::Relation
37
41
  attribute.in(value)
38
42
  when ActiveRecord::Base
@@ -48,6 +52,5 @@ module ActiveRecord
48
52
 
49
53
  predicates.flatten
50
54
  end
51
-
52
55
  end
53
56
  end
@@ -6,13 +6,15 @@ module ActiveRecord
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  attr_accessor :includes_values, :eager_load_values, :preload_values,
9
- :select_values, :group_values, :order_values, :reorder_flag, :joins_values, :where_values, :having_values,
10
- :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value
9
+ :select_values, :group_values, :order_values, :joins_values,
10
+ :where_values, :having_values, :bind_values,
11
+ :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
12
+ :from_value, :reorder_value
11
13
 
12
14
  def includes(*args)
13
15
  args.reject! {|a| a.blank? }
14
16
 
15
- return clone if args.empty?
17
+ return self if args.empty?
16
18
 
17
19
  relation = clone
18
20
  relation.includes_values = (relation.includes_values + args).flatten.uniq
@@ -20,14 +22,18 @@ module ActiveRecord
20
22
  end
21
23
 
22
24
  def eager_load(*args)
25
+ return self if args.blank?
26
+
23
27
  relation = clone
24
- relation.eager_load_values += args unless args.blank?
28
+ relation.eager_load_values += args
25
29
  relation
26
30
  end
27
31
 
28
32
  def preload(*args)
33
+ return self if args.blank?
34
+
29
35
  relation = clone
30
- relation.preload_values += args unless args.blank?
36
+ relation.preload_values += args
31
37
  relation
32
38
  end
33
39
 
@@ -42,44 +48,59 @@ module ActiveRecord
42
48
  end
43
49
 
44
50
  def group(*args)
51
+ return self if args.blank?
52
+
45
53
  relation = clone
46
- relation.group_values += args.flatten unless args.blank?
54
+ relation.group_values += args.flatten
47
55
  relation
48
56
  end
49
57
 
50
58
  def order(*args)
59
+ return self if args.blank?
60
+
51
61
  relation = clone
52
- relation.order_values += args.flatten unless args.blank?
62
+ relation.order_values += args.flatten
53
63
  relation
54
64
  end
55
65
 
56
66
  def reorder(*args)
67
+ return self if args.blank?
68
+
57
69
  relation = clone
58
- unless args.blank?
59
- relation.order_values = args
60
- relation.reorder_flag = true
61
- end
70
+ relation.reorder_value = args.flatten
62
71
  relation
63
72
  end
64
73
 
65
74
  def joins(*args)
75
+ return self if args.compact.blank?
76
+
66
77
  relation = clone
67
78
 
68
79
  args.flatten!
69
- relation.joins_values += args unless args.blank?
80
+ relation.joins_values += args
70
81
 
71
82
  relation
72
83
  end
73
84
 
85
+ def bind(value)
86
+ relation = clone
87
+ relation.bind_values += [value]
88
+ relation
89
+ end
90
+
74
91
  def where(opts, *rest)
92
+ return self if opts.blank?
93
+
75
94
  relation = clone
76
- relation.where_values += build_where(opts, rest) unless opts.blank?
95
+ relation.where_values += build_where(opts, rest)
77
96
  relation
78
97
  end
79
98
 
80
99
  def having(*args)
100
+ return self if args.blank?
101
+
81
102
  relation = clone
82
- relation.having_values += build_where(*args) unless args.blank?
103
+ relation.having_values += build_where(*args)
83
104
  relation
84
105
  end
85
106
 
@@ -116,7 +137,7 @@ module ActiveRecord
116
137
 
117
138
  def create_with(value)
118
139
  relation = clone
119
- relation.create_with_value = value
140
+ relation.create_with_value = value && (@create_with_value || {}).merge(value)
120
141
  relation
121
142
  end
122
143
 
@@ -126,8 +147,10 @@ module ActiveRecord
126
147
  relation
127
148
  end
128
149
 
129
- def extending(*modules, &block)
130
- modules << Module.new(&block) if block_given?
150
+ def extending(*modules)
151
+ modules << Module.new(&Proc.new) if block_given?
152
+
153
+ return self if modules.empty?
131
154
 
132
155
  relation = clone
133
156
  relation.send(:apply_modules, modules.flatten)
@@ -135,86 +158,73 @@ module ActiveRecord
135
158
  end
136
159
 
137
160
  def reverse_order
138
- order_clause = arel.order_clauses.join(', ')
139
- relation = except(:order)
161
+ order_clause = arel.order_clauses
140
162
 
141
- order = order_clause.blank? ?
142
- "#{@klass.table_name}.#{@klass.primary_key} DESC" :
143
- reverse_sql_order(order_clause)
163
+ order = order_clause.empty? ?
164
+ "#{table_name}.#{primary_key} DESC" :
165
+ reverse_sql_order(order_clause).join(', ')
144
166
 
145
- relation.order(Arel.sql(order))
167
+ except(:order).order(Arel.sql(order))
146
168
  end
147
169
 
148
170
  def arel
149
- @arel ||= build_arel
150
- end
151
-
152
- def custom_join_sql(*joins)
153
- arel = table.select_manager
154
-
155
- joins.each do |join|
156
- next if join.blank?
157
-
158
- @implicit_readonly = true
159
-
160
- case join
161
- when Array
162
- join = Arel.sql(join.join(' ')) if array_of_strings?(join)
163
- when String
164
- join = Arel.sql(join)
165
- end
166
-
167
- arel.join(join)
168
- end
169
-
170
- arel.join_sql
171
+ @arel ||= with_default_scope.build_arel
171
172
  end
172
173
 
173
174
  def build_arel
174
- arel = table
175
+ arel = table.from table
175
176
 
176
- arel = build_joins(arel, @joins_values) unless @joins_values.empty?
177
+ build_joins(arel, @joins_values) unless @joins_values.empty?
177
178
 
178
- arel = collapse_wheres(arel, (@where_values - ['']).uniq)
179
+ collapse_wheres(arel, (@where_values - ['']).uniq)
179
180
 
180
- arel = arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty?
181
+ arel.having(*@having_values.uniq.reject{|h| h.blank?}) unless @having_values.empty?
181
182
 
182
- arel = arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
183
- arel = arel.skip(@offset_value) if @offset_value
183
+ arel.take(connection.sanitize_limit(@limit_value)) if @limit_value
184
+ arel.skip(@offset_value) if @offset_value
184
185
 
185
- arel = arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
186
+ arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
186
187
 
187
- arel = arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty?
188
+ order = @reorder_value ? @reorder_value : @order_values
189
+ arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
188
190
 
189
- arel = build_select(arel, @select_values.uniq)
191
+ build_select(arel, @select_values.uniq)
190
192
 
191
- arel = arel.from(@from_value) if @from_value
192
- arel = arel.lock(@lock_value) if @lock_value
193
+ arel.from(@from_value) if @from_value
194
+ arel.lock(@lock_value) if @lock_value
193
195
 
194
196
  arel
195
197
  end
196
198
 
197
199
  private
198
200
 
199
- def collapse_wheres(arel, wheres)
200
- equalities = wheres.grep(Arel::Nodes::Equality)
201
+ def custom_join_ast(table, joins)
202
+ joins = joins.reject { |join| join.blank? }
201
203
 
202
- groups = equalities.group_by do |equality|
203
- equality.left
204
- end
204
+ return [] if joins.empty?
205
205
 
206
- groups.each do |_, eqls|
207
- test = eqls.inject(eqls.shift) do |memo, expr|
208
- memo.and(expr)
206
+ @implicit_readonly = true
207
+
208
+ joins.map do |join|
209
+ case join
210
+ when Array
211
+ join = Arel.sql(join.join(' ')) if array_of_strings?(join)
212
+ when String
213
+ join = Arel.sql(join)
209
214
  end
210
- arel = arel.where(test)
215
+ table.create_string_join(join)
211
216
  end
217
+ end
218
+
219
+ def collapse_wheres(arel, wheres)
220
+ equalities = wheres.grep(Arel::Nodes::Equality)
221
+
222
+ arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
212
223
 
213
224
  (wheres - equalities).each do |where|
214
225
  where = Arel.sql(where) if String === where
215
- arel = arel.where(Arel::Nodes::Grouping.new(where))
226
+ arel.where(Arel::Nodes::Grouping.new(where))
216
227
  end
217
- arel
218
228
  end
219
229
 
220
230
  def build_where(opts, other = [])
@@ -223,48 +233,60 @@ module ActiveRecord
223
233
  [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
224
234
  when Hash
225
235
  attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
226
- PredicateBuilder.new(table.engine).build_from_hash(attributes, table)
236
+ PredicateBuilder.build_from_hash(table.engine, attributes, table)
227
237
  else
228
238
  [opts]
229
239
  end
230
240
  end
231
241
 
232
- def build_joins(relation, joins)
233
- association_joins = []
234
-
235
- joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
236
-
237
- joins.each do |join|
238
- association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join)
242
+ def build_joins(manager, joins)
243
+ buckets = joins.group_by do |join|
244
+ case join
245
+ when String
246
+ 'string_join'
247
+ when Hash, Symbol, Array
248
+ 'association_join'
249
+ when ActiveRecord::Associations::JoinDependency::JoinAssociation
250
+ 'stashed_join'
251
+ when Arel::Nodes::Join
252
+ 'join_node'
253
+ else
254
+ raise 'unknown class: %s' % join.class.name
255
+ end
239
256
  end
240
257
 
241
- stashed_association_joins = joins.grep(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)
258
+ association_joins = buckets['association_join'] || []
259
+ stashed_association_joins = buckets['stashed_join'] || []
260
+ join_nodes = buckets['join_node'] || []
261
+ string_joins = (buckets['string_join'] || []).map { |x|
262
+ x.strip
263
+ }.uniq
264
+
265
+ join_list = custom_join_ast(manager, string_joins)
242
266
 
243
- non_association_joins = (joins - association_joins - stashed_association_joins)
244
- custom_joins = custom_join_sql(*non_association_joins)
267
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(
268
+ @klass,
269
+ association_joins,
270
+ join_list
271
+ )
245
272
 
246
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
273
+ join_nodes.each do |join|
274
+ join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase)
275
+ end
247
276
 
248
277
  join_dependency.graft(*stashed_association_joins)
249
278
 
250
279
  @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
251
280
 
252
- to_join = []
253
-
281
+ # FIXME: refactor this to build an AST
254
282
  join_dependency.join_associations.each do |association|
255
- if (association_relation = association.relation).is_a?(Array)
256
- to_join << [association_relation.first, association.join_type, association.association_join.first]
257
- to_join << [association_relation.last, association.join_type, association.association_join.last]
258
- else
259
- to_join << [association_relation, association.join_type, association.association_join]
260
- end
283
+ association.join_to(manager)
261
284
  end
262
285
 
263
- to_join.uniq.each do |left, join_type, right|
264
- relation = relation.join(left, join_type).on(*right)
265
- end
286
+ manager.join_sources.concat join_nodes.uniq
287
+ manager.join_sources.concat join_list
266
288
 
267
- relation.join(custom_joins)
289
+ manager
268
290
  end
269
291
 
270
292
  def build_select(arel, selects)
@@ -272,7 +294,7 @@ module ActiveRecord
272
294
  @implicit_readonly = false
273
295
  arel.project(*selects)
274
296
  else
275
- arel.project(Arel::SqlLiteral.new(@klass.quoted_table_name + '.*'))
297
+ arel.project(@klass.arel_table[Arel.star])
276
298
  end
277
299
  end
278
300
 
@@ -284,15 +306,9 @@ module ActiveRecord
284
306
  end
285
307
 
286
308
  def reverse_sql_order(order_query)
287
- order_query.to_s.split(/,/).each { |s|
288
- if s.match(/\s(asc|ASC)$/)
289
- s.gsub!(/\s(asc|ASC)$/, ' DESC')
290
- elsif s.match(/\s(desc|DESC)$/)
291
- s.gsub!(/\s(desc|DESC)$/, ' ASC')
292
- else
293
- s.concat(' DESC')
294
- end
295
- }.join(',')
309
+ order_query.join(', ').split(',').collect do |s|
310
+ s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
311
+ end
296
312
  end
297
313
 
298
314
  def array_of_strings?(o)