activerecord 2.1.2 → 2.2.2

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 (110) hide show
  1. data/CHANGELOG +32 -6
  2. data/README +0 -0
  3. data/Rakefile +4 -5
  4. data/lib/active_record.rb +11 -10
  5. data/lib/active_record/aggregations.rb +110 -38
  6. data/lib/active_record/association_preload.rb +104 -15
  7. data/lib/active_record/associations.rb +427 -212
  8. data/lib/active_record/associations/association_collection.rb +101 -16
  9. data/lib/active_record/associations/association_proxy.rb +65 -13
  10. data/lib/active_record/associations/belongs_to_association.rb +2 -2
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +0 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -3
  13. data/lib/active_record/associations/has_many_association.rb +28 -28
  14. data/lib/active_record/associations/has_many_through_association.rb +21 -19
  15. data/lib/active_record/associations/has_one_association.rb +24 -7
  16. data/lib/active_record/associations/has_one_through_association.rb +3 -4
  17. data/lib/active_record/attribute_methods.rb +13 -5
  18. data/lib/active_record/base.rb +435 -212
  19. data/lib/active_record/calculations.rb +12 -5
  20. data/lib/active_record/callbacks.rb +28 -9
  21. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +355 -0
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +42 -215
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -5
  24. data/lib/active_record/connection_adapters/abstract/query_cache.rb +2 -1
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +48 -7
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +10 -4
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +67 -26
  28. data/lib/active_record/connection_adapters/mysql_adapter.rb +71 -45
  29. data/lib/active_record/connection_adapters/postgresql_adapter.rb +155 -84
  30. data/lib/active_record/dirty.rb +25 -7
  31. data/lib/active_record/dynamic_finder_match.rb +41 -0
  32. data/lib/active_record/fixtures.rb +10 -9
  33. data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
  34. data/lib/active_record/locale/en.yml +54 -0
  35. data/lib/active_record/migration.rb +47 -10
  36. data/lib/active_record/named_scope.rb +29 -16
  37. data/lib/active_record/reflection.rb +118 -54
  38. data/lib/active_record/schema_dumper.rb +13 -7
  39. data/lib/active_record/test_case.rb +18 -5
  40. data/lib/active_record/transactions.rb +89 -34
  41. data/lib/active_record/validations.rb +270 -180
  42. data/lib/active_record/version.rb +1 -1
  43. data/test/cases/active_schema_test_mysql.rb +5 -0
  44. data/test/cases/adapter_test.rb +6 -0
  45. data/test/cases/aggregations_test.rb +39 -0
  46. data/test/cases/associations/belongs_to_associations_test.rb +10 -0
  47. data/test/cases/associations/eager_load_nested_include_test.rb +30 -12
  48. data/test/cases/associations/eager_test.rb +54 -5
  49. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +77 -10
  50. data/test/cases/associations/has_many_associations_test.rb +74 -7
  51. data/test/cases/associations/has_many_through_associations_test.rb +50 -3
  52. data/test/cases/associations/has_one_associations_test.rb +17 -0
  53. data/test/cases/associations/has_one_through_associations_test.rb +49 -1
  54. data/test/cases/associations_test.rb +0 -0
  55. data/test/cases/attribute_methods_test.rb +59 -4
  56. data/test/cases/base_test.rb +93 -21
  57. data/test/cases/binary_test.rb +1 -5
  58. data/test/cases/calculations_test.rb +5 -0
  59. data/test/cases/callbacks_observers_test.rb +38 -0
  60. data/test/cases/connection_test_mysql.rb +1 -1
  61. data/test/cases/defaults_test.rb +32 -1
  62. data/test/cases/deprecated_finder_test.rb +0 -0
  63. data/test/cases/dirty_test.rb +13 -0
  64. data/test/cases/finder_test.rb +162 -12
  65. data/test/cases/fixtures_test.rb +32 -3
  66. data/test/cases/helper.rb +15 -0
  67. data/test/cases/i18n_test.rb +41 -0
  68. data/test/cases/inheritance_test.rb +2 -2
  69. data/test/cases/lifecycle_test.rb +0 -0
  70. data/test/cases/locking_test.rb +4 -9
  71. data/test/cases/method_scoping_test.rb +109 -2
  72. data/test/cases/migration_test.rb +43 -8
  73. data/test/cases/multiple_db_test.rb +25 -0
  74. data/test/cases/named_scope_test.rb +74 -0
  75. data/test/cases/pooled_connections_test.rb +103 -0
  76. data/test/cases/readonly_test.rb +0 -0
  77. data/test/cases/reflection_test.rb +11 -3
  78. data/test/cases/reload_models_test.rb +20 -0
  79. data/test/cases/sanitize_test.rb +25 -0
  80. data/test/cases/schema_authorization_test_postgresql.rb +2 -2
  81. data/test/cases/transactions_test.rb +62 -12
  82. data/test/cases/unconnected_test.rb +0 -0
  83. data/test/cases/validations_i18n_test.rb +921 -0
  84. data/test/cases/validations_test.rb +44 -33
  85. data/test/connections/native_mysql/connection.rb +1 -3
  86. data/test/fixtures/companies.yml +1 -0
  87. data/test/fixtures/customers.yml +10 -1
  88. data/test/fixtures/fixture_database.sqlite3 +0 -0
  89. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  90. data/test/fixtures/organizations.yml +5 -0
  91. data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
  92. data/test/models/author.rb +3 -0
  93. data/test/models/category.rb +3 -0
  94. data/test/models/club.rb +6 -0
  95. data/test/models/company.rb +25 -1
  96. data/test/models/customer.rb +19 -1
  97. data/test/models/member.rb +2 -0
  98. data/test/models/member_detail.rb +4 -0
  99. data/test/models/organization.rb +4 -0
  100. data/test/models/parrot.rb +1 -0
  101. data/test/models/post.rb +3 -0
  102. data/test/models/reply.rb +0 -0
  103. data/test/models/topic.rb +3 -0
  104. data/test/schema/schema.rb +12 -1
  105. metadata +22 -10
  106. data/lib/active_record/vendor/mysql.rb +0 -1214
  107. data/test/cases/adapter_test_sqlserver.rb +0 -95
  108. data/test/cases/table_name_test_sqlserver.rb +0 -23
  109. data/test/cases/threaded_connections_test.rb +0 -48
  110. data/test/schema/sqlserver_specific_schema.rb +0 -5
@@ -13,14 +13,15 @@ module ActiveRecord
13
13
  def create_reflection(macro, name, options, active_record)
14
14
  case macro
15
15
  when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
16
- reflection = AssociationReflection.new(macro, name, options, active_record)
16
+ klass = options[:through] ? ThroughReflection : AssociationReflection
17
+ reflection = klass.new(macro, name, options, active_record)
17
18
  when :composed_of
18
19
  reflection = AggregateReflection.new(macro, name, options, active_record)
19
20
  end
20
21
  write_inheritable_hash :reflections, name => reflection
21
22
  reflection
22
23
  end
23
-
24
+
24
25
  # Returns a hash containing all AssociationReflection objects for the current class
25
26
  # Example:
26
27
  #
@@ -30,7 +31,7 @@ module ActiveRecord
30
31
  def reflections
31
32
  read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
32
33
  end
33
-
34
+
34
35
  # Returns an array of AggregateReflection objects for all the aggregations in the class.
35
36
  def reflect_on_all_aggregations
36
37
  reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
@@ -109,13 +110,18 @@ module ActiveRecord
109
110
  # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
110
111
  # and +other_aggregation+ has an options hash assigned to it.
111
112
  def ==(other_aggregation)
112
- name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
113
+ other_aggregation.kind_of?(self.class) && name == other_aggregation.name && other_aggregation.options && active_record == other_aggregation.active_record
113
114
  end
114
115
 
115
116
  def sanitized_conditions #:nodoc:
116
117
  @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
117
118
  end
118
119
 
120
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
121
+ def belongs_to?
122
+ macro == :belongs_to
123
+ end
124
+
119
125
  private
120
126
  def derive_class_name
121
127
  name.to_s.camelize
@@ -129,10 +135,45 @@ module ActiveRecord
129
135
 
130
136
  # Holds all the meta-data about an association as it was specified in the Active Record class.
131
137
  class AssociationReflection < MacroReflection #:nodoc:
138
+ # Returns the target association's class:
139
+ #
140
+ # class Author < ActiveRecord::Base
141
+ # has_many :books
142
+ # end
143
+ #
144
+ # Author.reflect_on_association(:books).klass
145
+ # # => Book
146
+ #
147
+ # <b>Note:</b> do not call +klass.new+ or +klass.create+ to instantiate
148
+ # a new association object. Use +build_association+ or +create_association+
149
+ # instead. This allows plugins to hook into association object creation.
132
150
  def klass
133
151
  @klass ||= active_record.send(:compute_type, class_name)
134
152
  end
135
153
 
154
+ # Returns a new, unsaved instance of the associated class. +options+ will
155
+ # be passed to the class's constructor.
156
+ def build_association(*options)
157
+ klass.new(*options)
158
+ end
159
+
160
+ # Creates a new instance of the associated class, and immediates saves it
161
+ # with ActiveRecord::Base#save. +options+ will be passed to the class's
162
+ # creation method. Returns the newly created object.
163
+ def create_association(*options)
164
+ klass.create(*options)
165
+ end
166
+
167
+ # Creates a new instance of the associated class, and immediates saves it
168
+ # with ActiveRecord::Base#save!. +options+ will be passed to the class's
169
+ # creation method. If the created record doesn't pass validations, then an
170
+ # exception will be raised.
171
+ #
172
+ # Returns the newly created object.
173
+ def create_association!(*options)
174
+ klass.create!(*options)
175
+ end
176
+
136
177
  def table_name
137
178
  @table_name ||= klass.table_name
138
179
  end
@@ -157,6 +198,52 @@ module ActiveRecord
157
198
  end
158
199
  end
159
200
 
201
+ def check_validity!
202
+ end
203
+
204
+ def through_reflection
205
+ false
206
+ end
207
+
208
+ def through_reflection_primary_key_name
209
+ end
210
+
211
+ def source_reflection
212
+ nil
213
+ end
214
+
215
+ private
216
+ def derive_class_name
217
+ class_name = name.to_s.camelize
218
+ class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
219
+ class_name
220
+ end
221
+
222
+ def derive_primary_key_name
223
+ if belongs_to?
224
+ "#{name}_id"
225
+ elsif options[:as]
226
+ "#{options[:as]}_id"
227
+ else
228
+ active_record.name.foreign_key
229
+ end
230
+ end
231
+ end
232
+
233
+ # Holds all the meta-data about a :through association as it was specified in the Active Record class.
234
+ class ThroughReflection < AssociationReflection #:nodoc:
235
+ # Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
236
+ # (The <tt>:tags</tt> association on Tagging below.)
237
+ #
238
+ # class Post < ActiveRecord::Base
239
+ # has_many :taggings
240
+ # has_many :tags, :through => :taggings
241
+ # end
242
+ #
243
+ def source_reflection
244
+ @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
245
+ end
246
+
160
247
  # Returns the AssociationReflection object specified in the <tt>:through</tt> option
161
248
  # of a HasManyThrough or HasOneThrough association. Example:
162
249
  #
@@ -169,7 +256,7 @@ module ActiveRecord
169
256
  # taggings_reflection = tags_reflection.through_reflection
170
257
  #
171
258
  def through_reflection
172
- @through_reflection ||= options[:through] ? active_record.reflect_on_association(options[:through]) : false
259
+ @through_reflection ||= active_record.reflect_on_association(options[:through])
173
260
  end
174
261
 
175
262
  # Gets an array of possible <tt>:through</tt> source reflection names:
@@ -180,63 +267,40 @@ module ActiveRecord
180
267
  @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
181
268
  end
182
269
 
183
- # Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
184
- # (The <tt>:tags</tt> association on Tagging below.)
185
- #
186
- # class Post < ActiveRecord::Base
187
- # has_many :taggings
188
- # has_many :tags, :through => :taggings
189
- # end
190
- #
191
- def source_reflection
192
- return nil unless through_reflection
193
- @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
194
- end
195
-
196
270
  def check_validity!
197
- if options[:through]
198
- if through_reflection.nil?
199
- raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
200
- end
201
-
202
- if source_reflection.nil?
203
- raise HasManyThroughSourceAssociationNotFoundError.new(self)
204
- end
271
+ if through_reflection.nil?
272
+ raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
273
+ end
205
274
 
206
- if options[:source_type] && source_reflection.options[:polymorphic].nil?
207
- raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
208
- end
209
-
210
- if source_reflection.options[:polymorphic] && options[:source_type].nil?
211
- raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
212
- end
213
-
214
- unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
215
- raise HasManyThroughSourceAssociationMacroError.new(self)
216
- end
275
+ if source_reflection.nil?
276
+ raise HasManyThroughSourceAssociationNotFoundError.new(self)
277
+ end
278
+
279
+ if options[:source_type] && source_reflection.options[:polymorphic].nil?
280
+ raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
217
281
  end
282
+
283
+ if source_reflection.options[:polymorphic] && options[:source_type].nil?
284
+ raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
285
+ end
286
+
287
+ unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
288
+ raise HasManyThroughSourceAssociationMacroError.new(self)
289
+ end
290
+ end
291
+
292
+ def through_reflection_primary_key
293
+ through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
294
+ end
295
+
296
+ def through_reflection_primary_key_name
297
+ through_reflection.primary_key_name if through_reflection.belongs_to?
218
298
  end
219
299
 
220
300
  private
221
301
  def derive_class_name
222
302
  # get the class_name of the belongs_to association of the through reflection
223
- if through_reflection
224
- options[:source_type] || source_reflection.class_name
225
- else
226
- class_name = name.to_s.camelize
227
- class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
228
- class_name
229
- end
230
- end
231
-
232
- def derive_primary_key_name
233
- if macro == :belongs_to
234
- "#{name}_id"
235
- elsif options[:as]
236
- "#{options[:as]}_id"
237
- else
238
- active_record.name.foreign_key
239
- end
303
+ options[:source_type] || source_reflection.class_name
240
304
  end
241
305
  end
242
306
  end
@@ -102,7 +102,7 @@ HEADER
102
102
  spec[:precision] = column.precision.inspect if !column.precision.nil?
103
103
  spec[:scale] = column.scale.inspect if !column.scale.nil?
104
104
  spec[:null] = 'false' if !column.null
105
- spec[:default] = default_string(column.default) if !column.default.nil?
105
+ spec[:default] = default_string(column.default) if column.has_default?
106
106
  (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")}
107
107
  spec
108
108
  end.compact
@@ -159,13 +159,19 @@ HEADER
159
159
  end
160
160
 
161
161
  def indexes(table, stream)
162
- indexes = @connection.indexes(table)
163
- indexes.each do |index|
164
- stream.print " add_index #{index.table.inspect}, #{index.columns.inspect}, :name => #{index.name.inspect}"
165
- stream.print ", :unique => true" if index.unique
162
+ if (indexes = @connection.indexes(table)).any?
163
+ add_index_statements = indexes.map do |index|
164
+ statment_parts = [ ('add_index ' + index.table.inspect) ]
165
+ statment_parts << index.columns.inspect
166
+ statment_parts << (':name => ' + index.name.inspect)
167
+ statment_parts << ':unique => true' if index.unique
168
+
169
+ ' ' + statment_parts.join(', ')
170
+ end
171
+
172
+ stream.puts add_index_statements.sort.join("\n")
166
173
  stream.puts
167
174
  end
168
- stream.puts unless indexes.empty?
169
175
  end
170
176
  end
171
- end
177
+ end
@@ -11,11 +11,9 @@ module ActiveRecord
11
11
  end
12
12
 
13
13
  def assert_date_from_db(expected, actual, message = nil)
14
- # SQL Server doesn't have a separate column type just for dates,
14
+ # SybaseAdapter doesn't have a separate column type just for dates,
15
15
  # so the time is in the string and incorrectly formatted
16
- if current_adapter?(:SQLServerAdapter)
17
- assert_equal expected.strftime("%Y/%m/%d 00:00:00"), actual.strftime("%Y/%m/%d 00:00:00")
18
- elsif current_adapter?(:SybaseAdapter)
16
+ if current_adapter?(:SybaseAdapter)
19
17
  assert_equal expected.to_s, actual.to_date.to_s, message
20
18
  else
21
19
  assert_equal expected.to_s, actual.to_s, message
@@ -37,11 +35,26 @@ module ActiveRecord
37
35
  $queries_executed = []
38
36
  yield
39
37
  ensure
40
- assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
38
+ assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed.#{$queries_executed.size == 0 ? '' : "\nQueries:\n#{$queries_executed.join("\n")}"}"
41
39
  end
42
40
 
43
41
  def assert_no_queries(&block)
44
42
  assert_queries(0, &block)
45
43
  end
44
+
45
+ def self.use_concurrent_connections
46
+ setup :connection_allow_concurrency_setup
47
+ teardown :connection_allow_concurrency_teardown
48
+ end
49
+
50
+ def connection_allow_concurrency_setup
51
+ @connection = ActiveRecord::Base.remove_connection
52
+ ActiveRecord::Base.establish_connection(@connection.merge({:allow_concurrency => true}))
53
+ end
54
+
55
+ def connection_allow_concurrency_teardown
56
+ ActiveRecord::Base.clear_all_connections!
57
+ ActiveRecord::Base.establish_connection(@connection)
58
+ end
46
59
  end
47
60
  end
@@ -1,7 +1,8 @@
1
1
  require 'thread'
2
2
 
3
3
  module ActiveRecord
4
- module Transactions # :nodoc:
4
+ # See ActiveRecord::Transactions::ClassMethods for documentation.
5
+ module Transactions
5
6
  class TransactionError < ActiveRecordError # :nodoc:
6
7
  end
7
8
 
@@ -15,26 +16,33 @@ module ActiveRecord
15
16
  end
16
17
  end
17
18
 
18
- # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
19
- # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and
20
- # vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs.
21
- # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
22
- # not at all. Example:
19
+ # Transactions are protective blocks where SQL statements are only permanent
20
+ # if they can all succeed as one atomic action. The classic example is a
21
+ # transfer between two accounts where you can only have a deposit if the
22
+ # withdrawal succeeded and vice versa. Transactions enforce the integrity of
23
+ # the database and guard the data against program errors or database
24
+ # break-downs. So basically you should use transaction blocks whenever you
25
+ # have a number of statements that must be executed together or not at all.
26
+ # Example:
23
27
  #
24
- # transaction do
28
+ # ActiveRecord::Base.transaction do
25
29
  # david.withdrawal(100)
26
30
  # mary.deposit(100)
27
31
  # end
28
32
  #
29
- # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception.
30
- # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though,
31
- # that the objects will _not_ have their instance data returned to their pre-transactional state.
33
+ # This example will only take money from David and give to Mary if neither
34
+ # +withdrawal+ nor +deposit+ raises an exception. Exceptions will force a
35
+ # ROLLBACK that returns the database to the state before the transaction was
36
+ # begun. Be aware, though, that the objects will _not_ have their instance
37
+ # data returned to their pre-transactional state.
32
38
  #
33
39
  # == Different Active Record classes in a single transaction
34
40
  #
35
41
  # Though the transaction class method is called on some Active Record class,
36
42
  # the objects within the transaction block need not all be instances of
37
- # that class.
43
+ # that class. This is because transactions are per-database connection, not
44
+ # per-model.
45
+ #
38
46
  # In this example a <tt>Balance</tt> record is transactionally saved even
39
47
  # though <tt>transaction</tt> is called on the <tt>Account</tt> class:
40
48
  #
@@ -43,6 +51,14 @@ module ActiveRecord
43
51
  # account.save!
44
52
  # end
45
53
  #
54
+ # Note that the +transaction+ method is also available as a model instance
55
+ # method. For example, you can also do this:
56
+ #
57
+ # balance.transaction do
58
+ # balance.save!
59
+ # account.save!
60
+ # end
61
+ #
46
62
  # == Transactions are not distributed across database connections
47
63
  #
48
64
  # A transaction acts on a single database connection. If you have
@@ -62,48 +78,72 @@ module ActiveRecord
62
78
  #
63
79
  # == Save and destroy are automatically wrapped in a transaction
64
80
  #
65
- # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks
66
- # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction
67
- # depends on or you can raise exceptions in the callbacks to rollback.
81
+ # Both Base#save and Base#destroy come wrapped in a transaction that ensures
82
+ # that whatever you do in validations or callbacks will happen under the
83
+ # protected cover of a transaction. So you can use validations to check for
84
+ # values that the transaction depends on or you can raise exceptions in the
85
+ # callbacks to rollback, including <tt>after_*</tt> callbacks.
86
+ #
87
+ # == Exception handling and rolling back
88
+ #
89
+ # Also have in mind that exceptions thrown within a transaction block will
90
+ # be propagated (after triggering the ROLLBACK), so you should be ready to
91
+ # catch those in your application code.
92
+ #
93
+ # One exception is the ActiveRecord::Rollback exception, which will trigger
94
+ # a ROLLBACK when raised, but not be re-raised by the transaction block.
95
+ #
96
+ # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
97
+ # inside a transaction block. StatementInvalid exceptions indicate that an
98
+ # error occurred at the database level, for example when a unique constraint
99
+ # is violated. On some database systems, such as PostgreSQL, database errors
100
+ # inside a transaction causes the entire transaction to become unusable
101
+ # until it's restarted from the beginning. Here is an example which
102
+ # demonstrates the problem:
68
103
  #
69
- # == Exception handling
104
+ # # Suppose that we have a Number model with a unique column called 'i'.
105
+ # Number.transaction do
106
+ # Number.create(:i => 0)
107
+ # begin
108
+ # # This will raise a unique constraint error...
109
+ # Number.create(:i => 0)
110
+ # rescue ActiveRecord::StatementInvalid
111
+ # # ...which we ignore.
112
+ # end
113
+ #
114
+ # # On PostgreSQL, the transaction is now unusable. The following
115
+ # # statement will cause a PostgreSQL error, even though the unique
116
+ # # constraint is no longer violated:
117
+ # Number.create(:i => 1)
118
+ # # => "PGError: ERROR: current transaction is aborted, commands
119
+ # # ignored until end of transaction block"
120
+ # end
70
121
  #
71
- # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you
72
- # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will
73
- # trigger a ROLLBACK when raised, but not be re-raised by the transaction block.
122
+ # One should restart the entire transaction if a StatementError occurred.
74
123
  module ClassMethods
124
+ # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
75
125
  def transaction(&block)
76
- increment_open_transactions
126
+ connection.increment_open_transactions
77
127
 
78
128
  begin
79
- connection.transaction(Thread.current['start_db_transaction'], &block)
129
+ connection.transaction(connection.open_transactions == 1, &block)
80
130
  ensure
81
- decrement_open_transactions
131
+ connection.decrement_open_transactions
82
132
  end
83
133
  end
84
-
85
- private
86
- def increment_open_transactions #:nodoc:
87
- open = Thread.current['open_transactions'] ||= 0
88
- Thread.current['start_db_transaction'] = open.zero?
89
- Thread.current['open_transactions'] = open + 1
90
- end
91
-
92
- def decrement_open_transactions #:nodoc:
93
- Thread.current['open_transactions'] -= 1
94
- end
95
134
  end
96
135
 
136
+ # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
97
137
  def transaction(&block)
98
138
  self.class.transaction(&block)
99
139
  end
100
140
 
101
141
  def destroy_with_transactions #:nodoc:
102
- transaction { destroy_without_transactions }
142
+ with_transaction_returning_status(:destroy_without_transactions)
103
143
  end
104
144
 
105
145
  def save_with_transactions(perform_validation = true) #:nodoc:
106
- rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } }
146
+ rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
107
147
  end
108
148
 
109
149
  def save_with_transactions! #:nodoc:
@@ -126,5 +166,20 @@ module ActiveRecord
126
166
  end
127
167
  raise
128
168
  end
169
+
170
+ # Executes +method+ within a transaction and captures its return value as a
171
+ # status flag. If the status is true the transaction is committed, otherwise
172
+ # a ROLLBACK is issued. In any case the status flag is returned.
173
+ #
174
+ # This method is available within the context of an ActiveRecord::Base
175
+ # instance.
176
+ def with_transaction_returning_status(method, *args)
177
+ status = nil
178
+ transaction do
179
+ status = send(method, *args)
180
+ raise ActiveRecord::Rollback unless status
181
+ end
182
+ status
183
+ end
129
184
  end
130
185
  end