Skip to content

Commit b337390

Browse files
committed
transactions can be turned off per Migration.
Closes #9483. There are SQL Queries that can't run inside a transaction. Since the Migrator used to wrap all Migrations inside a transaction there was no way to run these queries within a migration. This patch adds `self.disable_ddl_transaction!` to the migration to turn transactions off when necessary.
1 parent f1241ef commit b337390

File tree

5 files changed

+90
-9
lines changed

5 files changed

+90
-9
lines changed

activerecord/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
## Rails 4.0.0 (unreleased) ##
22

3+
* Make it possible to execute migrations without a transaction even
4+
if the database adapter supports DDL transactions.
5+
Fixes #9483.
6+
7+
Example:
8+
9+
class ChangeEnum < ActiveRecord::Migration
10+
self.disable_ddl_transaction!
11+
def up
12+
execute "ALTER TYPE model_size ADD VALUE 'new_value'"
13+
end
14+
end
15+
16+
*Yves Senn*
17+
318
* Assigning "0.0" to a nullable numeric column does not make it dirty.
419
Fix #9034.
520

activerecord/lib/active_record/migration.rb

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,23 @@ def initialize
330330
#
331331
# For a list of commands that are reversible, please see
332332
# <tt>ActiveRecord::Migration::CommandRecorder</tt>.
333+
#
334+
# == Transactional Migrations
335+
#
336+
# If the database adapter supports DDL transactions, all migrations will
337+
# automatically be wrapped in a transaction. There are queries that you
338+
# can't execute inside a transaction though, and for these situations
339+
# you can turn the automatic transactions off.
340+
#
341+
# class ChangeEnum < ActiveRecord::Migration
342+
# self.disable_ddl_transaction!
343+
# def up
344+
# execute "ALTER TYPE model_size ADD VALUE 'new_value'"
345+
# end
346+
# end
347+
#
348+
# Remember that you can still open your own transactions, even if you
349+
# are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
333350
class Migration
334351
autoload :CommandRecorder, 'active_record/migration/command_recorder'
335352

@@ -351,6 +368,7 @@ def call(env)
351368

352369
class << self
353370
attr_accessor :delegate # :nodoc:
371+
attr_accessor :disable_ddl_transaction # :nodoc:
354372
end
355373

356374
def self.check_pending!
@@ -365,8 +383,16 @@ def self.migrate(direction)
365383
new.migrate direction
366384
end
367385

368-
cattr_accessor :verbose
386+
# Disable DDL transactions for this migration.
387+
def self.disable_ddl_transaction!
388+
@disable_ddl_transaction = true
389+
end
390+
391+
def disable_ddl_transaction # :nodoc:
392+
self.class.disable_ddl_transaction
393+
end
369394

395+
cattr_accessor :verbose
370396
attr_accessor :name, :version
371397

372398
def initialize(name = self.class.name, version = nil)
@@ -375,8 +401,8 @@ def initialize(name = self.class.name, version = nil)
375401
@connection = nil
376402
end
377403

404+
self.verbose = true
378405
# instantiate the delegate object after initialize is defined
379-
self.verbose = true
380406
self.delegate = new
381407

382408
# Reverses the migration commands for the given block and
@@ -663,7 +689,7 @@ def basename
663689
File.basename(filename)
664690
end
665691

666-
delegate :migrate, :announce, :write, :to => :migration
692+
delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
667693

668694
private
669695

@@ -856,12 +882,12 @@ def migrate
856882
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
857883

858884
begin
859-
ddl_transaction do
885+
ddl_transaction(migration) do
860886
migration.migrate(@direction)
861887
record_version_state_after_migrating(migration.version)
862888
end
863889
rescue => e
864-
canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
890+
canceled_msg = use_transaction?(migration) ? "this and " : ""
865891
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
866892
end
867893
end
@@ -935,12 +961,16 @@ def down?
935961
end
936962

937963
# Wrap the migration in a transaction only if supported by the adapter.
938-
def ddl_transaction
939-
if Base.connection.supports_ddl_transactions?
964+
def ddl_transaction(migration)
965+
if use_transaction?(migration)
940966
Base.transaction { yield }
941967
else
942968
yield
943969
end
944970
end
971+
972+
def use_transaction?(migration)
973+
!migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
974+
end
945975
end
946976
end

activerecord/test/cases/migration/logger_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class LoggerTest < ActiveRecord::TestCase
77
self.use_transactional_fixtures = false
88

99
Migration = Struct.new(:name, :version) do
10+
def disable_ddl_transaction; false end
1011
def migrate direction
1112
# do nothing
1213
end
@@ -34,4 +35,3 @@ def test_migration_should_be_run_without_logger
3435
end
3536
end
3637
end
37-

activerecord/test/cases/migration_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,39 @@ def migrate(x)
254254
assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
255255

256256
Person.reset_column_information
257+
assert_not Person.column_methods_hash.include?(:last_name),
258+
"On error, the Migrator should revert schema changes but it did not."
259+
end
260+
261+
def test_migration_without_transaction
262+
unless ActiveRecord::Base.connection.supports_ddl_transactions?
263+
skip "not supported on #{ActiveRecord::Base.connection.class}"
264+
end
265+
257266
assert_not Person.column_methods_hash.include?(:last_name)
267+
268+
migration = Class.new(ActiveRecord::Migration) {
269+
self.disable_ddl_transaction!
270+
271+
def version; 101 end
272+
def migrate(x)
273+
add_column "people", "last_name", :string
274+
raise 'Something broke'
275+
end
276+
}.new
277+
278+
migrator = ActiveRecord::Migrator.new(:up, [migration], 101)
279+
e = assert_raise(StandardError) { migrator.migrate }
280+
assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message
281+
282+
Person.reset_column_information
283+
assert Person.column_methods_hash.include?(:last_name),
284+
"without ddl transactions, the Migrator should not rollback on error but it did."
285+
ensure
286+
Person.reset_column_information
287+
if Person.column_methods_hash.include?(:last_name)
288+
Person.connection.remove_column('people', 'last_name')
289+
end
258290
end
259291

260292
def test_schema_migrations_table_name

guides/source/migrations.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ migrations are wrapped in a transaction. If the database does not support this
6161
then when a migration fails the parts of it that succeeded will not be rolled
6262
back. You will have to rollback the changes that were made by hand.
6363

64+
NOTE: There are certain queries that can't run inside a transaction. If your
65+
adapter supports DDL transactions you can use `disable_ddl_transaction!` to
66+
disable them for a single migration.
67+
6468
If you wish for a migration to do something that Active Record doesn't know how
6569
to reverse, you can use `reversible`:
6670

@@ -180,7 +184,7 @@ end
180184
```
181185

182186
If the migration name is of the form "CreateXXX" and is
183-
followed by a list of column names and types then a migration creating the table
187+
followed by a list of column names and types then a migration creating the table
184188
XXX with the columns listed will be generated. For example:
185189

186190
```bash

0 commit comments

Comments
 (0)