From aa20af8b6914e056bcd6e36ff680873a110dd369 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 15 Oct 2026 00:11:32 +0300 Subject: [PATCH 1/7] Add migration generation transaction mode (#367) --- CHANGELOG.md | 2 + README.md | 3 + bin/yii-db-migration | 3 +- bin/yii-db-migration.php | 5 + config/di-console.php | 8 ++ config/params.php | 1 + docs/guide/en/README.md | 1 + docs/guide/en/transactional-migrations.md | 95 +++++++++++++++++++ resources/views/addColumnMigration.php | 10 +- resources/views/createTableMigration.php | 9 +- resources/views/dropColumnMigration.php | 10 +- resources/views/dropTableMigration.php | 10 +- resources/views/migration.php | 10 +- src/Command/CreateCommand.php | 9 ++ src/DatabaseSet.php | 3 + src/DatabaseSetRegistry.php | 2 +- src/Service/Generate/CreateService.php | 8 ++ .../Generate/AbstractCreateServiceTest.php | 82 ++++++++++++++++ tests/Driver/Sqlite/CreateCommandTest.php | 58 +++++++++++ tests/Driver/Sqlite/DatabaseCommandsTest.php | 34 +++++++ tests/Migration/BinTest.php | 39 ++++++++ tests/Migration/ConfigTest.php | 51 +++++++++- tests/Support/Helper/ContainerConfig.php | 1 + tests/Support/Helper/ContainerHelper.php | 1 + tests/Support/Templates/transactionMode.php | 11 +++ 25 files changed, 456 insertions(+), 10 deletions(-) create mode 100644 docs/guide/en/transactional-migrations.md create mode 100644 tests/Support/Templates/transactionMode.php diff --git a/CHANGELOG.md b/CHANGELOG.md index fe32a236..c8760bbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 2.1.2 under development +- New #367: Add configurable transaction mode and `--transaction` / `--no-transaction` options for migration generation + (@samdark) - New #363: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark) - New #360, #366: Add support for additional named databases with separate migration history and `--db` option to select database (@samdark, @vjik) diff --git a/README.md b/README.md index aa1dfc45..78421f7d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,9 @@ migrate:up Applies new migrations. The create command allows defining fields for the table being created. +Use `--transaction` or `--no-transaction` to choose the transaction mode when creating a migration. +See [Transactional migrations](docs/guide/en/transactional-migrations.md) to configure the default mode. + Use `--db=name` to select a [named database migration set](docs/guide/en/multiple-databases.md). ## Documentation diff --git a/bin/yii-db-migration b/bin/yii-db-migration index 7ebbfd4a..3b348902 100644 --- a/bin/yii-db-migration +++ b/bin/yii-db-migration @@ -52,6 +52,7 @@ if (!file_exists($configFile)) { * maxSqlOutputLength: int|null, * newMigrationNamespace: string, * newMigrationPath: string, + * newMigrationTransactional?: bool|null, * sourceNamespaces: list, * sourcePaths: list, * } $params @@ -69,7 +70,7 @@ $migrator = new Migrator( $params['migrationNameLimit'], $params['maxSqlOutputLength'] ); -$createService = new CreateService($db, $params['useTablePrefix']); +$createService = new CreateService($db, $params['useTablePrefix'], $params['newMigrationTransactional'] ?? null); $downRunner = new DownRunner($migrator); $updateRunner = new UpdateRunner($migrator); diff --git a/bin/yii-db-migration.php b/bin/yii-db-migration.php index 2039bdac..3dc6e679 100644 --- a/bin/yii-db-migration.php +++ b/bin/yii-db-migration.php @@ -40,6 +40,11 @@ */ 'newMigrationPath' => '', + /** + * Whether generated migrations use transactions. Set to null to keep template defaults. + */ + 'newMigrationTransactional' => null, + /** * List of directories containing the migration classes. * Migration classes located at these paths should be declared without a namespace. diff --git a/config/di-console.php b/config/di-console.php index cd84c342..885d1e63 100644 --- a/config/di-console.php +++ b/config/di-console.php @@ -6,6 +6,7 @@ use Yiisoft\Db\Migration\DatabaseSetRegistry; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Informer\MigrationInformerInterface; +use Yiisoft\Db\Migration\Service\Generate\CreateService; use Yiisoft\Db\Migration\Service\MigrationService; use Yiisoft\Definitions\DynamicReference; use Yiisoft\Definitions\Reference; @@ -13,6 +14,13 @@ /** @var array $params */ return [ + CreateService::class => [ + 'class' => CreateService::class, + '__construct()' => [ + 'transactional' => $params['yiisoft/db-migration']['newMigrationTransactional'] ?? null, + ], + ], + DatabaseSetRegistry::class => [ 'class' => DatabaseSetRegistry::class, '__construct()' => [ diff --git a/config/params.php b/config/params.php index 8916751c..3674e60c 100644 --- a/config/params.php +++ b/config/params.php @@ -26,6 +26,7 @@ 'yiisoft/db-migration' => [ 'newMigrationNamespace' => '', 'newMigrationPath' => '', + 'newMigrationTransactional' => null, 'sourceNamespaces' => [], 'sourcePaths' => [], 'databases' => [], diff --git a/docs/guide/en/README.md b/docs/guide/en/README.md index d73f339f..71ab7feb 100644 --- a/docs/guide/en/README.md +++ b/docs/guide/en/README.md @@ -4,4 +4,5 @@ - [Symfony application](usage-with-symfony.md) - [Standalone](usage-standalone.md) - [Multiple databases](multiple-databases.md) +- [Transactional migrations](transactional-migrations.md) - [Modifying migration history](migration-history.md) diff --git a/docs/guide/en/transactional-migrations.md b/docs/guide/en/transactional-migrations.md new file mode 100644 index 00000000..ef6a8806 --- /dev/null +++ b/docs/guide/en/transactional-migrations.md @@ -0,0 +1,95 @@ +# Transactional migrations + +A migration implementing `Yiisoft\Db\Migration\TransactionalMigrationInterface` runs in a transaction. Applying the +migration and recording it in history share a transaction. Reverting the migration and removing its history record also +share a transaction. Each migration has its own transaction. + +To generate a transactional migration, use `--transaction`: + +```shell +./yii migrate:create update_posts --transaction +``` + +To generate a migration without a transaction, use `--no-transaction`: + +```shell +./yii migrate:create post --command=table --no-transaction +``` + +These options work with all generator commands: `create`, `table`, `dropTable`, `addColumn`, `dropColumn`, and `junction`. +Use `php bin/console` for Symfony or `./vendor/bin/yii-db-migration` for the standalone tool instead of `./yii`. + +## Default transaction mode + +The nullable `newMigrationTransactional` setting controls the mode for newly generated migrations: + +| Value | Behavior | +| --- | --- | +| `true` | Generate transactional migrations. | +| `false` | Generate migrations without transactions. | +| `null` | Keep each template's default. | + +With `null`, the built-in `table` and `junction` templates generate transactional migrations. The `create`, `dropTable`, +`addColumn`, and `dropColumn` templates generate migrations without transactions. This preserves the existing behavior. + +An explicit CLI option overrides the configured mode. If neither specifies a mode, the template chooses its default. + +For Yii Console, configure the default database in `config/console/params.php`: + +```php +'yiisoft/db-migration' => [ + // ... + 'newMigrationTransactional' => true, +], +``` + +For the standalone tool, set `'newMigrationTransactional' => true` in the root `yii-db-migration.php` configuration file. + +For Symfony, configure the `CreateService` constructor argument in `config/services.yml`: + +```yaml +services: + Yiisoft\Db\Migration\Service\Generate\CreateService: + arguments: + $transactional: true +``` + +When constructing the service directly, use `new CreateService($db, transactional: true)`. A call to +`CreateService::run()` can override this mode with its `transactional` argument without changing subsequent calls. + +## Named databases + +Each additional database has its own `newMigrationTransactional` setting. It does not inherit the default database's +setting. For Yii Console, add the setting to the corresponding entry in `databases`. For standalone or Symfony usage, +pass it as a `DatabaseSet` constructor argument: + +```php +new DatabaseSet( + $mapsConnection, + newMigrationPath: __DIR__ . '/migrations/maps', + newMigrationTransactional: true, +); +``` + +The CLI option overrides the setting for the selected database: + +```shell +./yii migrate:create update_places --db=maps --no-transaction +``` + +See [Multiple databases](multiple-databases.md) for connection configuration. + +## Existing migrations and custom templates + +Generation settings do not change existing migrations. Add or remove `TransactionalMigrationInterface` in an existing +migration's implemented interfaces to change its transaction mode. Keep `RevertibleMigrationInterface` if the migration +supports `down()`. + +Custom templates registered through `CreateService::setTemplate()` or `setTemplates()` receive a nullable `$transactional` +variable. It contains the per-call override, otherwise the configured mode. If both are `null`, the template can choose +its own default, for example with `$transactional ??= false`. To honor the setting, conditionally emit the import and +implementation of `TransactionalMigrationInterface`. Existing custom templates that ignore this variable keep their +previous behavior. + +Transactions remain subject to the database's restrictions. Some schema operations implicitly commit or cannot run in a +transaction; adding the interface cannot make these operations rollbackable. diff --git a/resources/views/addColumnMigration.php b/resources/views/addColumnMigration.php index ec72757d..0d20f9bf 100644 --- a/resources/views/addColumnMigration.php +++ b/resources/views/addColumnMigration.php @@ -15,11 +15,14 @@ * @var PhpRenderer $this * @var string $className The new migration class name without namespace. * @var string $namespace The new migration class namespace. + * @var bool|null $transactional Whether the migration uses transactions. `null` keeps the template default. * @var string $table The table name. * @var Column[] $columns Fields. * @var ForeignKey[] $foreignKeys Foreign keys. */ +$transactional ??= false; + echo " +use Yiisoft\Db\Migration\TransactionalMigrationInterface; + /** * Handles adding columns to table ``. @@ -38,7 +44,9 @@ 'foreignKeys' => $foreignKeys, ]) ?> */ -final class implements RevertibleMigrationInterface +final class implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void { diff --git a/resources/views/createTableMigration.php b/resources/views/createTableMigration.php index 51add26e..4ea28a25 100644 --- a/resources/views/createTableMigration.php +++ b/resources/views/createTableMigration.php @@ -15,12 +15,15 @@ * @var PhpRenderer $this * @var string $className The new migration class name without namespace. * @var string $namespace The new migration class namespace. + * @var bool|null $transactional Whether the migration uses transactions. `null` keeps the template default. * @var string $table The table name. * @var string $tableComment The table comment. * @var Column[] $columns Fields. * @var ForeignKey[] $foreignKeys Foreign keys. */ +$transactional ??= true; + echo " use Yiisoft\Db\Migration\TransactionalMigrationInterface; + /** * Handles the creation of table ``. @@ -40,7 +45,9 @@ 'foreignKeys' => $foreignKeys, ]) ?> */ -final class implements RevertibleMigrationInterface, TransactionalMigrationInterface +final class implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void { diff --git a/resources/views/dropColumnMigration.php b/resources/views/dropColumnMigration.php index c016c3f9..d9abd8e5 100644 --- a/resources/views/dropColumnMigration.php +++ b/resources/views/dropColumnMigration.php @@ -15,11 +15,14 @@ * @var PhpRenderer $this * @var string $className The new migration class name without namespace. * @var string $namespace The new migration class namespace. + * @var bool|null $transactional Whether the migration uses transactions. `null` keeps the template default. * @var string $table The table name. * @var Column[] $columns Fields. * @var ForeignKey[] $foreignKeys Foreign keys. */ +$transactional ??= false; + echo " +use Yiisoft\Db\Migration\TransactionalMigrationInterface; + /** * Handles dropping columns from table ``. @@ -38,7 +44,9 @@ 'foreignKeys' => $foreignKeys, ]) ?> */ -final class implements RevertibleMigrationInterface +final class implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void { diff --git a/resources/views/dropTableMigration.php b/resources/views/dropTableMigration.php index 667e5ff3..dfbd0bdb 100644 --- a/resources/views/dropTableMigration.php +++ b/resources/views/dropTableMigration.php @@ -15,11 +15,14 @@ * @var PhpRenderer $this * @var string $className The new migration class name without namespace. * @var string $namespace The new migration class namespace. + * @var bool|null $transactional Whether the migration uses transactions. `null` keeps the template default. * @var string $table The table name. * @var Column[] $columns Fields. * @var ForeignKey[] $foreignKeys Foreign keys. */ +$transactional ??= false; + echo " +use Yiisoft\Db\Migration\TransactionalMigrationInterface; + /** * Handles the dropping of table ``. @@ -38,7 +44,9 @@ 'foreignKeys' => $foreignKeys, ]) ?> */ -final class implements RevertibleMigrationInterface +final class implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void { diff --git a/resources/views/migration.php b/resources/views/migration.php index ffeda780..1018f0bb 100644 --- a/resources/views/migration.php +++ b/resources/views/migration.php @@ -13,8 +13,11 @@ * @var PhpRenderer $this * @var string $className The new migration class name without namespace. * @var string $namespace The new migration class namespace. + * @var bool|null $transactional Whether the migration uses transactions. `null` keeps the template default. */ +$transactional ??= false; + echo " +use Yiisoft\Db\Migration\TransactionalMigrationInterface; + -final class implements RevertibleMigrationInterface +final class implements RevertibleMigrationInterface { public function up(MigrationBuilder $b): void { diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index 1a84cc62..3f920013 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -90,6 +90,12 @@ protected function configure(): void ->addOption('and', null, InputOption::VALUE_REQUIRED, 'And junction.') ->addOption('path', null, InputOption::VALUE_REQUIRED, 'Path to migration directory.') ->addOption('namespace', 'ns', InputOption::VALUE_REQUIRED, 'Migration file namespace.') + ->addOption( + 'transaction', + null, + InputOption::VALUE_NEGATABLE, + 'Generate a transactional migration. If omitted, use the configured mode or template default.', + ) ->setHelp('This command generates new migration file.'); } @@ -179,6 +185,8 @@ protected function executeForDatabase( $fields = $input->getOption('fields'); /** @var string|null $tableComment */ $tableComment = $input->getOption('table-comment'); + /** @var bool|null $transactional */ + $transactional = $input->getOption('transaction'); $content = $createService->run( $command, @@ -188,6 +196,7 @@ protected function executeForDatabase( $fields, $and, $tableComment, + $transactional, ); $file = $migrationPath . DIRECTORY_SEPARATOR . $className . '.php'; diff --git a/src/DatabaseSet.php b/src/DatabaseSet.php index 7625032f..37da983d 100644 --- a/src/DatabaseSet.php +++ b/src/DatabaseSet.php @@ -14,6 +14,8 @@ final class DatabaseSet /** * @param string[] $sourceNamespaces * @param string[] $sourcePaths + * @param bool|null $newMigrationTransactional Whether generated migrations use transactions. + * `null` keeps template defaults. */ public function __construct( public readonly ConnectionInterface $db, @@ -25,5 +27,6 @@ public function __construct( public readonly ?int $migrationNameLimit = 180, public readonly bool $useTablePrefix = true, public readonly ?int $maxSqlOutputLength = null, + public readonly ?bool $newMigrationTransactional = null, ) {} } diff --git a/src/DatabaseSetRegistry.php b/src/DatabaseSetRegistry.php index e2db8369..8bc84e15 100644 --- a/src/DatabaseSetRegistry.php +++ b/src/DatabaseSetRegistry.php @@ -88,7 +88,7 @@ public function createContext(string $name): ?DatabaseContext return new DatabaseContext( $migrator, $service, - new CreateService($database->db, $database->useTablePrefix), + new CreateService($database->db, $database->useTablePrefix, $database->newMigrationTransactional), new DownRunner($migrator), new UpdateRunner($migrator), ); diff --git a/src/Service/Generate/CreateService.php b/src/Service/Generate/CreateService.php index 30181d6d..5e7f40c0 100644 --- a/src/Service/Generate/CreateService.php +++ b/src/Service/Generate/CreateService.php @@ -24,14 +24,20 @@ final class CreateService /** * @param bool $useTablePrefix Indicates whether the table names generated should consider the `tablePrefix` setting * of the DB connection. For example, if the table name is `post`, the generator will return `{{%post}}`. + * @param bool|null $transactional Whether generated migrations use transactions. `null` keeps template defaults. */ public function __construct( private readonly ConnectionInterface $db, private readonly bool $useTablePrefix = true, + private readonly ?bool $transactional = null, ) { $this->phpRenderer = new PhpRenderer(); } + /** + * @param bool|null $transactional Overrides the configured transaction mode for this migration. + * `null` uses the configured mode or the template default. + */ public function run( string $command, string $table, @@ -40,6 +46,7 @@ public function run( ?string $fields = null, ?string $and = null, ?string $tableComment = null, + ?bool $transactional = null, ): string { $templateFile = $this->getTemplate($command); @@ -83,6 +90,7 @@ public function run( 'columns' => $columns, 'foreignKeys' => $foreignKeys, 'tableComment' => $tableComment, + 'transactional' => $transactional ?? $this->transactional, ], ); } diff --git a/tests/Common/Service/Generate/AbstractCreateServiceTest.php b/tests/Common/Service/Generate/AbstractCreateServiceTest.php index c92c6100..d60a434f 100644 --- a/tests/Common/Service/Generate/AbstractCreateServiceTest.php +++ b/tests/Common/Service/Generate/AbstractCreateServiceTest.php @@ -4,15 +4,97 @@ namespace Yiisoft\Db\Migration\Tests\Common\Service\Generate; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Migration\Service\Generate\CreateService; +use function dirname; +use function token_get_all; + +use const TOKEN_PARSE; + abstract class AbstractCreateServiceTest extends TestCase { protected ContainerInterface $container; + public static function transactionModes(): iterable + { + $modes = [ + 'template default' => [null, null, null], + 'configured on' => [true, null, true], + 'configured off' => [false, null, false], + 'override on' => [null, true, true], + 'override off' => [null, false, false], + 'override configured on' => [true, false, false], + 'override configured off' => [false, true, true], + 'explicit on' => [true, true, true], + 'explicit off' => [false, false, false], + ]; + + foreach (['create', 'table', 'dropTable', 'addColumn', 'dropColumn', 'junction'] as $command) { + foreach ($modes as $name => [$default, $override, $expected]) { + yield "$command: $name" => [ + $command, + $default, + $override, + $expected ?? ($command === 'table' || $command === 'junction'), + ]; + } + } + } + + #[DataProvider('transactionModes')] + public function testTransactionMode(string $command, ?bool $default, ?bool $override, bool $expected): void + { + $service = new CreateService($this->container->get(ConnectionInterface::class), transactional: $default); + + $code = $service->run( + $command, + 'post', + 'Example', + fields: 'title:string', + and: 'tag', + transactional: $override, + ); + + token_get_all($code, TOKEN_PARSE); + if ($expected) { + $this->assertStringContainsString('use Yiisoft\\Db\\Migration\\TransactionalMigrationInterface;', $code); + $this->assertStringContainsString( + 'implements RevertibleMigrationInterface, TransactionalMigrationInterface', + $code, + ); + } else { + $this->assertStringNotContainsString('TransactionalMigrationInterface', $code); + $this->assertStringContainsString('implements RevertibleMigrationInterface', $code); + } + } + + public static function customTemplateTransactionModes(): array + { + return [ + [null, null, 'template default'], + [true, null, 'transactional'], + [false, null, 'nontransactional'], + [true, false, 'nontransactional'], + [false, true, 'transactional'], + ]; + } + + #[DataProvider('customTemplateTransactionModes')] + public function testCustomTemplateReceivesTransactionMode(?bool $default, ?bool $override, string $expected): void + { + $service = new CreateService($this->container->get(ConnectionInterface::class), transactional: $default); + $service->setTemplate('table', dirname(__DIR__, 3) . '/Support/Templates/transactionMode.php'); + + $code = $service->run('table', 'post', 'Example', transactional: $override); + + $this->assertSame($expected, $code); + } + public function testJunctionWithoutRelatedTable(): void { $service = $this->container->get(CreateService::class); diff --git a/tests/Driver/Sqlite/CreateCommandTest.php b/tests/Driver/Sqlite/CreateCommandTest.php index b37ccf80..eb8a279c 100644 --- a/tests/Driver/Sqlite/CreateCommandTest.php +++ b/tests/Driver/Sqlite/CreateCommandTest.php @@ -4,9 +4,16 @@ namespace Yiisoft\Db\Migration\Tests\Driver\Sqlite; +use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Component\Console\Command\Command; use Yiisoft\Db\Migration\Tests\Common\Command\AbstractCreateCommandTest; use Yiisoft\Db\Migration\Tests\Support\Factory\SqLiteFactory; use Yiisoft\Db\Migration\Tests\Support\Helper\ContainerConfig; +use Yiisoft\Db\Migration\Tests\Support\Helper\MigrationHelper; + +use function file_get_contents; +use function glob; +use function str_contains; /** * @group sqlite @@ -26,6 +33,57 @@ public function tearDown(): void SqLiteFactory::clearDatabase($this->container); } + public static function transactionModes(): array + { + return [ + 'blank default' => ['create', null, [], false], + 'table default' => ['table', null, [], true], + 'configured on' => ['create', true, [], true], + 'configured off' => ['table', false, [], false], + 'override on' => ['create', null, ['--transaction' => true], true], + 'override off' => ['table', null, ['--no-transaction' => true], false], + 'override configured on' => ['create', true, ['--no-transaction' => true], false], + 'override configured off' => ['table', false, ['--transaction' => true], true], + ]; + } + + #[DataProvider('transactionModes')] + public function testTransactionMode(string $type, ?bool $default, array $options, bool $expected): void + { + $config = new ContainerConfig(); + $config->newMigrationTransactional = $default; + $this->container = SqLiteFactory::createContainer($config); + $path = MigrationHelper::useMigrationsPath($this->container); + $command = $this->createCommand($this->container); + + $exitCode = $command->execute(['name' => 'post', '--command' => $type] + $options); + + $this->assertSame(Command::SUCCESS, $exitCode); + $files = glob($path . '/*.php'); + $this->assertCount(1, $files); + $this->assertSame($expected, str_contains(file_get_contents($files[0]), 'TransactionalMigrationInterface')); + } + + public function testTransactionOverrideDoesNotAffectLaterRuns(): void + { + $path = MigrationHelper::useMigrationsPath($this->container); + $command = $this->createCommand($this->container); + + $firstExitCode = $command->execute(['name' => 'First', '--transaction' => true]); + $secondExitCode = $command->execute(['name' => 'Second']); + + $this->assertSame(Command::SUCCESS, $firstExitCode); + $this->assertSame(Command::SUCCESS, $secondExitCode); + $this->assertStringContainsString( + 'TransactionalMigrationInterface', + file_get_contents(glob($path . '/*First.php')[0]), + ); + $this->assertStringNotContainsString( + 'TransactionalMigrationInterface', + file_get_contents(glob($path . '/*Second.php')[0]), + ); + } + public function testWithoutTablePrefix(): void { $containerConfig = new ContainerConfig(); diff --git a/tests/Driver/Sqlite/DatabaseCommandsTest.php b/tests/Driver/Sqlite/DatabaseCommandsTest.php index 2307eb73..0a17f60e 100644 --- a/tests/Driver/Sqlite/DatabaseCommandsTest.php +++ b/tests/Driver/Sqlite/DatabaseCommandsTest.php @@ -35,6 +35,7 @@ use Yiisoft\Db\Migration\Tests\Support\MigrationsExtra\M231108183919Empty2; use function dirname; +use function str_contains; final class DatabaseCommandsTest extends TestCase { @@ -273,6 +274,39 @@ public function testCreateDefaultsToDefaultAndCanSelectAnotherSet(): void self::assertSame([], glob($this->directory . '/analytics/*.php')); } + public static function transactionModes(): array + { + return [ + 'configured on' => ['create', true, [], true], + 'configured off' => ['table', false, [], false], + 'override on' => ['create', false, ['--transaction' => true], true], + 'override off' => ['table', true, ['--no-transaction' => true], false], + ]; + } + + #[DataProvider('transactionModes')] + public function testCreateUsesNamedSetTransactionMode( + string $type, + bool $default, + array $options, + bool $expected, + ): void { + $this->sets['maps'] = new DatabaseSet( + $this->connections['maps'], + newMigrationPath: $this->directory . '/maps', + newMigrationTransactional: $default, + ); + $command = $this->command('create'); + + $exitCode = $command->execute(['name' => 'post', '--command' => $type, '--db' => 'maps'] + $options); + + self::assertSame(Command::SUCCESS, $exitCode); + $files = glob($this->directory . '/maps/*.php'); + self::assertCount(1, $files); + self::assertSame($expected, str_contains(file_get_contents($files[0]), 'TransactionalMigrationInterface')); + self::assertSame([], glob($this->directory . '/default/*.php')); + } + public function testStopsOnExceptionAndRollsBackOnlyTheFailingDatabase(): void { $defaultClass = $this->writeMigration('default'); diff --git a/tests/Migration/BinTest.php b/tests/Migration/BinTest.php index 96e483a4..7811c187 100644 --- a/tests/Migration/BinTest.php +++ b/tests/Migration/BinTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Migration\Tests\Migration; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Files\FileHelper; use Yiisoft\Db\Cache\SchemaCache; @@ -12,6 +13,10 @@ use Yiisoft\Test\Support\SimpleCache\MemorySimpleCache; use function dirname; +use function file_get_contents; +use function glob; +use function str_contains; +use function var_export; final class BinTest extends TestCase { @@ -35,6 +40,7 @@ protected function tearDown(): void public function testBase(): void { $this->replaceParams("'databases' => [],", ''); + $this->replaceParams("'newMigrationTransactional' => null,", ''); $this->replaceParams( "'db' => null,", <<<'PHP' @@ -51,6 +57,39 @@ public function testBase(): void $this->assertStringContainsString('Yii Database Migration Tool', $output); } + public static function transactionModes(): array + { + return [ + 'configured on' => ['create', true, [], true], + 'configured off' => ['table', false, [], false], + 'override on' => ['create', false, ['--transaction'], true], + 'override off' => ['table', true, ['--no-transaction'], false], + ]; + } + + #[DataProvider('transactionModes')] + public function testCreateTransactionMode(string $command, bool $default, array $options, bool $expected): void + { + $this->replaceParams("'db' => null,", <<<'PHP' + 'db' => new \Yiisoft\Db\Sqlite\Connection( + new \Yiisoft\Db\Sqlite\Driver('sqlite::memory:'), + new \Yiisoft\Db\Cache\SchemaCache(new \Yiisoft\Test\Support\SimpleCache\MemorySimpleCache()) + ), + PHP); + $this->replaceParams("'newMigrationPath' => '',", "'newMigrationPath' => __DIR__,"); + $this->replaceParams( + "'newMigrationTransactional' => null,", + "'newMigrationTransactional' => " . var_export($default, true) . ',', + ); + + [$output, $exitCode] = $this->runYiiDbMigration(['migrate:create', 'post', "--command=$command", ...$options]); + + $this->assertSame(0, $exitCode, $output); + $files = glob(dirname(__DIR__, 2) . '/runtime/tests/bin/M*.php'); + $this->assertCount(1, $files); + $this->assertSame($expected, str_contains(file_get_contents($files[0]), 'TransactionalMigrationInterface')); + } + public function testWithoutConnection(): void { [$output, $exitCode] = $this->runYiiDbMigration(); diff --git a/tests/Migration/ConfigTest.php b/tests/Migration/ConfigTest.php index 99a00b0a..2d886be1 100644 --- a/tests/Migration/ConfigTest.php +++ b/tests/Migration/ConfigTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Db\Migration\Tests\Migration; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Psr\SimpleCache\CacheInterface; use Yiisoft\Db\Connection\ConnectionInterface; @@ -32,6 +33,7 @@ use Yiisoft\Db\Migration\Service\MigrationService; use function dirname; +use function str_contains; final class ConfigTest extends TestCase { @@ -99,8 +101,50 @@ public function testAdditionalDatabaseConfiguration(): void ); } - private function createConsoleContainer(array $databases = [], array $definitions = []): Container + public static function transactionModes(): array { + return [ + ['create', null, false], + ['table', null, true], + ['create', true, true], + ['table', false, false], + ]; + } + + #[DataProvider('transactionModes')] + public function testTransactionModeConfiguration(string $command, ?bool $transactional, bool $expected): void + { + $container = $this->createConsoleContainer(transactional: $transactional); + + $code = $container->get(CreateService::class)->run($command, 'post', 'Example'); + + $this->assertSame($expected, str_contains($code, 'TransactionalMigrationInterface')); + } + + #[DataProvider('transactionModes')] + public function testNamedDatabaseTransactionModeConfiguration( + string $command, + ?bool $transactional, + bool $expected, + ): void { + $container = $this->createConsoleContainer([ + 'maps' => [ + 'db' => ConnectionInterface::class, + 'newMigrationTransactional' => $transactional, + ], + ]); + + $code = $container->get(DatabaseSetRegistry::class)->createContext('maps') + ->createService->run($command, 'post', 'Example'); + + $this->assertSame($expected, str_contains($code, 'TransactionalMigrationInterface')); + } + + private function createConsoleContainer( + array $databases = [], + array $definitions = [], + ?bool $transactional = null, + ): Container { $config = ContainerConfig::create() ->withDefinitions(array_merge( [ @@ -116,15 +160,16 @@ private function createConsoleContainer(array $databases = [], array $definition ], ], $definitions, - $this->getConsoleDefinitions($databases), + $this->getConsoleDefinitions($databases, $transactional), )); return new Container($config); } - private function getConsoleDefinitions(array $databases): array + private function getConsoleDefinitions(array $databases, ?bool $transactional): array { $params = $this->getParams(); $params['yiisoft/db-migration']['databases'] = $databases; + $params['yiisoft/db-migration']['newMigrationTransactional'] = $transactional; return require dirname(__DIR__, 2) . '/config/di-console.php'; } diff --git a/tests/Support/Helper/ContainerConfig.php b/tests/Support/Helper/ContainerConfig.php index 540c8bdb..06647231 100644 --- a/tests/Support/Helper/ContainerConfig.php +++ b/tests/Support/Helper/ContainerConfig.php @@ -7,4 +7,5 @@ final class ContainerConfig { public bool $useTablePrefix = true; + public ?bool $newMigrationTransactional = null; } diff --git a/tests/Support/Helper/ContainerHelper.php b/tests/Support/Helper/ContainerHelper.php index f2968551..70cc39b8 100644 --- a/tests/Support/Helper/ContainerHelper.php +++ b/tests/Support/Helper/ContainerHelper.php @@ -48,6 +48,7 @@ public static function get(ContainerInterface $container, string $id, ContainerC CreateService::class => new CreateService( $container->get(ConnectionInterface::class), $config->useTablePrefix, + $config->newMigrationTransactional, ), ConsoleMigrationInformer::class => new ConsoleMigrationInformer(), CreateCommand::class => new CreateCommand( diff --git a/tests/Support/Templates/transactionMode.php b/tests/Support/Templates/transactionMode.php new file mode 100644 index 00000000..08df500a --- /dev/null +++ b/tests/Support/Templates/transactionMode.php @@ -0,0 +1,11 @@ + 'transactional', + false => 'nontransactional', + null => 'template default', +}; From 1886394d4b8e60c165b2133d2461c8e7918c40f4 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 15 Oct 2026 00:19:58 +0300 Subject: [PATCH 2/7] Quote null in transaction mode PHPDoc --- bin/yii-db-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/yii-db-migration.php b/bin/yii-db-migration.php index 3dc6e679..a10e544f 100644 --- a/bin/yii-db-migration.php +++ b/bin/yii-db-migration.php @@ -41,7 +41,7 @@ 'newMigrationPath' => '', /** - * Whether generated migrations use transactions. Set to null to keep template defaults. + * Whether generated migrations use transactions. Set to `null` to keep template defaults. */ 'newMigrationTransactional' => null, From b9fd33a8085ce06648460907a8ed49f3f5308c1c Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 15 Oct 2026 00:37:00 +0300 Subject: [PATCH 3/7] Simplify transactional migration documentation --- docs/guide/en/transactional-migrations.md | 66 +++++++++++++---------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/docs/guide/en/transactional-migrations.md b/docs/guide/en/transactional-migrations.md index ef6a8806..e5844166 100644 --- a/docs/guide/en/transactional-migrations.md +++ b/docs/guide/en/transactional-migrations.md @@ -1,8 +1,12 @@ # Transactional migrations -A migration implementing `Yiisoft\Db\Migration\TransactionalMigrationInterface` runs in a transaction. Applying the -migration and recording it in history share a transaction. Reverting the migration and removing its history record also -share a transaction. Each migration has its own transaction. +A migration that implements `Yiisoft\Db\Migration\TransactionalMigrationInterface` runs in a transaction. +The migrator uses a separate transaction for each migration's `up()` or `down()` method, including the history update. + +Some database operations commit automatically or cannot run in a transaction. +The interface does not change these limits. + +## Create a migration To generate a transactional migration, use `--transaction`: @@ -16,25 +20,27 @@ To generate a migration without a transaction, use `--no-transaction`: ./yii migrate:create post --command=table --no-transaction ``` -These options work with all generator commands: `create`, `table`, `dropTable`, `addColumn`, `dropColumn`, and `junction`. -Use `php bin/console` for Symfony or `./vendor/bin/yii-db-migration` for the standalone tool instead of `./yii`. +Both options work with all generator commands: +`create`, `table`, `dropTable`, `addColumn`, `dropColumn`, and `junction`. +For Symfony, replace `./yii` with `php bin/console`. For the standalone tool, use `./vendor/bin/yii-db-migration`. ## Default transaction mode -The nullable `newMigrationTransactional` setting controls the mode for newly generated migrations: +Set `newMigrationTransactional` to choose the default mode for new migrations: | Value | Behavior | | --- | --- | | `true` | Generate transactional migrations. | | `false` | Generate migrations without transactions. | -| `null` | Keep each template's default. | +| `null` | Use the template's default. | -With `null`, the built-in `table` and `junction` templates generate transactional migrations. The `create`, `dropTable`, -`addColumn`, and `dropColumn` templates generate migrations without transactions. This preserves the existing behavior. +By default, the built-in `table` and `junction` templates generate transactional migrations. +The other built-in templates generate migrations without transactions. -An explicit CLI option overrides the configured mode. If neither specifies a mode, the template chooses its default. +CLI options override `newMigrationTransactional`. +If no option is set and the setting is `null`, the generator uses the template's default. -For Yii Console, configure the default database in `config/console/params.php`: +For Yii Console, set the mode for the default database in `config/console/params.php`: ```php 'yiisoft/db-migration' => [ @@ -43,7 +49,7 @@ For Yii Console, configure the default database in `config/console/params.php`: ], ``` -For the standalone tool, set `'newMigrationTransactional' => true` in the root `yii-db-migration.php` configuration file. +For the standalone tool, set `'newMigrationTransactional' => true` in the project's root `yii-db-migration.php` file. For Symfony, configure the `CreateService` constructor argument in `config/services.yml`: @@ -54,14 +60,15 @@ services: $transactional: true ``` -When constructing the service directly, use `new CreateService($db, transactional: true)`. A call to -`CreateService::run()` can override this mode with its `transactional` argument without changing subsequent calls. +For direct PHP use, pass `transactional: true` to the `CreateService` constructor. +Pass the `transactional` argument to `CreateService::run()` to override the mode for one call. ## Named databases -Each additional database has its own `newMigrationTransactional` setting. It does not inherit the default database's -setting. For Yii Console, add the setting to the corresponding entry in `databases`. For standalone or Symfony usage, -pass it as a `DatabaseSet` constructor argument: +Each named database has its own `newMigrationTransactional` setting. It does not inherit the default database's setting. + +For Yii Console, add the setting to the database's entry in `databases`. +For standalone or Symfony usage, pass it to the `DatabaseSet` constructor: ```php new DatabaseSet( @@ -77,19 +84,20 @@ The CLI option overrides the setting for the selected database: ./yii migrate:create update_places --db=maps --no-transaction ``` -See [Multiple databases](multiple-databases.md) for connection configuration. +See [Multiple databases](multiple-databases.md) to configure the connections. + +## Change an existing migration + +Generation settings do not change existing migrations. -## Existing migrations and custom templates +To enable transactions, add `TransactionalMigrationInterface` to the migration's `implements` list. +To disable transactions, remove it. Keep `RevertibleMigrationInterface` if the migration supports `down()`. -Generation settings do not change existing migrations. Add or remove `TransactionalMigrationInterface` in an existing -migration's implemented interfaces to change its transaction mode. Keep `RevertibleMigrationInterface` if the migration -supports `down()`. +## Custom templates -Custom templates registered through `CreateService::setTemplate()` or `setTemplates()` receive a nullable `$transactional` -variable. It contains the per-call override, otherwise the configured mode. If both are `null`, the template can choose -its own default, for example with `$transactional ??= false`. To honor the setting, conditionally emit the import and -implementation of `TransactionalMigrationInterface`. Existing custom templates that ignore this variable keep their -previous behavior. +Templates set with `CreateService::setTemplate()` or `CreateService::setTemplates()` receive `$transactional`. +This variable contains the mode for the current call, or the configured mode if no override is set. +If the value is `null`, choose a template default, for example with `$transactional ??= false`. -Transactions remain subject to the database's restrictions. Some schema operations implicitly commit or cannot run in a -transaction; adding the interface cannot make these operations rollbackable. +Include the import and implementation of `TransactionalMigrationInterface` only when `$transactional` is `true`. +Templates that ignore this variable keep their existing behavior. From a09ea3abeef76b1b16e00a0d4fc3923917c7ed58 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 15 Oct 2026 01:16:48 +0300 Subject: [PATCH 4/7] Fix test dependency and file assertions (#367) --- composer.json | 1 + tests/Driver/Sqlite/CreateCommandTest.php | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index ca4b7e4b..9316aae8 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "yiisoft/injector": "^1.2" }, "require-dev": { + "ext-tokenizer": "*", "bamarni/composer-bin-plugin": "^1.9.1", "friendsofphp/php-cs-fixer": "^3.95.1", "phpunit/phpunit": "^10.5.63", diff --git a/tests/Driver/Sqlite/CreateCommandTest.php b/tests/Driver/Sqlite/CreateCommandTest.php index eb8a279c..878ebd8d 100644 --- a/tests/Driver/Sqlite/CreateCommandTest.php +++ b/tests/Driver/Sqlite/CreateCommandTest.php @@ -74,13 +74,17 @@ public function testTransactionOverrideDoesNotAffectLaterRuns(): void $this->assertSame(Command::SUCCESS, $firstExitCode); $this->assertSame(Command::SUCCESS, $secondExitCode); + $firstFiles = glob($path . '/*First.php'); + $secondFiles = glob($path . '/*Second.php'); + $this->assertCount(1, $firstFiles); + $this->assertCount(1, $secondFiles); $this->assertStringContainsString( 'TransactionalMigrationInterface', - file_get_contents(glob($path . '/*First.php')[0]), + file_get_contents($firstFiles[0]), ); $this->assertStringNotContainsString( 'TransactionalMigrationInterface', - file_get_contents(glob($path . '/*Second.php')[0]), + file_get_contents($secondFiles[0]), ); } From 7841bfbe00b696ab4d0ec596de192baf15624ac0 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 18 Oct 2026 15:31:01 +0300 Subject: [PATCH 5/7] Update CHANGELOG.md Co-authored-by: Sergei Tigrov --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88ed9e2d..749b2211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ - New #367: Add configurable transaction mode and `--transaction` / `--no-transaction` options for migration generation (@samdark) -- New #363: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark) - New #363, #373: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark, @vjik) - New #360, #366, #371: Add support for additional named databases with separate migration history and `--db` From b3229f6d1a8a35f6b4551390a3a18e183b6729ea Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 18 Oct 2026 16:01:25 +0300 Subject: [PATCH 6/7] Rename migration option to --transactional --- CHANGELOG.md | 2 +- README.md | 2 +- docs/guide/en/transactional-migrations.md | 10 +++++----- src/Command/CreateCommand.php | 4 ++-- tests/Common/Command/AbstractDatabaseCommandsTest.php | 4 ++-- tests/Driver/Sqlite/CreateCommandTest.php | 10 +++++----- tests/Migration/BinTest.php | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 749b2211..835b1aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.1.2 under development -- New #367: Add configurable transaction mode and `--transaction` / `--no-transaction` options for migration generation +- New #367: Add configurable transaction mode and `--transactional` / `--no-transactional` options for migration generation (@samdark) - New #363, #373: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark, @vjik) diff --git a/README.md b/README.md index ccd83d74..4ae6757f 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ migrate:up Applies new migrations. The create command allows defining fields for the table being created. -Use `--transaction` or `--no-transaction` to choose the transaction mode when creating a migration. +Use `--transactional` or `--no-transactional` to choose the transaction mode when creating a migration. See [Transactional migrations](docs/guide/en/transactional-migrations.md) to configure the default mode. Use `--db=name` to select one of [multiple databases](docs/guide/en/multiple-databases.md). diff --git a/docs/guide/en/transactional-migrations.md b/docs/guide/en/transactional-migrations.md index 146ae10e..af34c831 100644 --- a/docs/guide/en/transactional-migrations.md +++ b/docs/guide/en/transactional-migrations.md @@ -8,16 +8,16 @@ The interface does not change these limits. ## Create a migration -To generate a transactional migration, use `--transaction`: +To generate a transactional migration, use `--transactional`: ```shell -./yii migrate:create update_posts --transaction +./yii migrate:create update_posts --transactional ``` -To generate a migration without a transaction, use `--no-transaction`: +To generate a migration without a transaction, use `--no-transactional`: ```shell -./yii migrate:create post --command=table --no-transaction +./yii migrate:create post --command=table --no-transactional ``` Both options work with all generator commands: @@ -81,7 +81,7 @@ new DatabaseConfig( The CLI option overrides the setting for the selected database: ```shell -./yii migrate:create update_places --db=maps --no-transaction +./yii migrate:create update_places --db=maps --no-transactional ``` See [Multiple databases](multiple-databases.md) to configure the connections. diff --git a/src/Command/CreateCommand.php b/src/Command/CreateCommand.php index 8745630b..75dd6a09 100644 --- a/src/Command/CreateCommand.php +++ b/src/Command/CreateCommand.php @@ -94,7 +94,7 @@ protected function configure(): void ->addOption('path', null, InputOption::VALUE_REQUIRED, 'Path to migration directory.') ->addOption('namespace', 'ns', InputOption::VALUE_REQUIRED, 'Migration file namespace.') ->addOption( - 'transaction', + 'transactional', null, InputOption::VALUE_NEGATABLE, 'Generate a transactional migration. If omitted, use the configured mode or template default.', @@ -208,7 +208,7 @@ protected function executeForDatabase( /** @var string|null $tableComment */ $tableComment = $input->getOption('table-comment'); /** @var bool|null $transactional */ - $transactional = $input->getOption('transaction'); + $transactional = $input->getOption('transactional'); $content = $context->createService->run( $command, diff --git a/tests/Common/Command/AbstractDatabaseCommandsTest.php b/tests/Common/Command/AbstractDatabaseCommandsTest.php index 911d044d..c17a4901 100644 --- a/tests/Common/Command/AbstractDatabaseCommandsTest.php +++ b/tests/Common/Command/AbstractDatabaseCommandsTest.php @@ -277,8 +277,8 @@ public static function transactionModes(): array return [ 'configured on' => ['create', true, [], true], 'configured off' => ['table', false, [], false], - 'override on' => ['create', false, ['--transaction' => true], true], - 'override off' => ['table', true, ['--no-transaction' => true], false], + 'override on' => ['create', false, ['--transactional' => true], true], + 'override off' => ['table', true, ['--no-transactional' => true], false], ]; } diff --git a/tests/Driver/Sqlite/CreateCommandTest.php b/tests/Driver/Sqlite/CreateCommandTest.php index 878ebd8d..49788f2a 100644 --- a/tests/Driver/Sqlite/CreateCommandTest.php +++ b/tests/Driver/Sqlite/CreateCommandTest.php @@ -40,10 +40,10 @@ public static function transactionModes(): array 'table default' => ['table', null, [], true], 'configured on' => ['create', true, [], true], 'configured off' => ['table', false, [], false], - 'override on' => ['create', null, ['--transaction' => true], true], - 'override off' => ['table', null, ['--no-transaction' => true], false], - 'override configured on' => ['create', true, ['--no-transaction' => true], false], - 'override configured off' => ['table', false, ['--transaction' => true], true], + 'override on' => ['create', null, ['--transactional' => true], true], + 'override off' => ['table', null, ['--no-transactional' => true], false], + 'override configured on' => ['create', true, ['--no-transactional' => true], false], + 'override configured off' => ['table', false, ['--transactional' => true], true], ]; } @@ -69,7 +69,7 @@ public function testTransactionOverrideDoesNotAffectLaterRuns(): void $path = MigrationHelper::useMigrationsPath($this->container); $command = $this->createCommand($this->container); - $firstExitCode = $command->execute(['name' => 'First', '--transaction' => true]); + $firstExitCode = $command->execute(['name' => 'First', '--transactional' => true]); $secondExitCode = $command->execute(['name' => 'Second']); $this->assertSame(Command::SUCCESS, $firstExitCode); diff --git a/tests/Migration/BinTest.php b/tests/Migration/BinTest.php index 4a5d689d..5196d2ce 100644 --- a/tests/Migration/BinTest.php +++ b/tests/Migration/BinTest.php @@ -62,8 +62,8 @@ public static function transactionModes(): array return [ 'configured on' => ['create', true, [], true], 'configured off' => ['table', false, [], false], - 'override on' => ['create', false, ['--transaction'], true], - 'override off' => ['table', true, ['--no-transaction'], false], + 'override on' => ['create', false, ['--transactional'], true], + 'override off' => ['table', true, ['--no-transactional'], false], ]; } From 1944c68ade31188eed45ed4120e2b82c557779c7 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sun, 18 Oct 2026 16:04:30 +0300 Subject: [PATCH 7/7] fix changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 835b1aaf..5f39f5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ## 2.1.2 under development -- New #367: Add configurable transaction mode and `--transactional` / `--no-transactional` options for migration generation - (@samdark) +- New #367: Add configurable transaction mode and `--transactional` / `--no-transactional` options for migration + generation (@samdark) - New #363, #373: Add `migrate:mark` to move migration history to a version without executing migrations (@samdark, @vjik) - New #360, #366, #371: Add support for additional named databases with separate migration history and `--db`