diff --git a/UPGRADE.md b/UPGRADE.md index ccf82bf725..358393dabf 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -5,6 +5,9 @@ to enable wrapping all migrations in a single transaction. To disable it, you can use the `--no-all-or-nothing` option instead. Both options override the configuration value. +## Migration classes +- It is now possible to add SQL statements, that needs to be executed after changes made to schema object. Use + `addDeferredSql` method in your migrations for this purpose. # Upgrade to 3.6 diff --git a/docs/en/reference/migration-classes.rst b/docs/en/reference/migration-classes.rst index 707150a61d..bd94de1b77 100644 --- a/docs/en/reference/migration-classes.rst +++ b/docs/en/reference/migration-classes.rst @@ -188,7 +188,7 @@ addSql You can use the ``addSql`` method within the ``up`` and ``down`` methods. Internally the ``addSql`` calls are passed to the executeQuery method in the DBAL. This means that you can use the power of prepared statements easily and that you don't need to copy paste the same query with different parameters. You can just pass those different parameters -to the addSql method as parameters. +to the addSql method as parameters. These queries are executed before changes applied to ``$schema``. .. code-block:: php @@ -205,6 +205,30 @@ to the addSql method as parameters. } } +addDeferredSql +~~~~~~~~~~~~~~ + +Works just like the ``addSql`` method, but queries are deferred to be executed after changes to ``$schema`` were +planned. + +.. code-block:: php + + public function up(Schema $schema): void + { + $schema->getTable('user')->addColumn('happy', 'boolean')->setDefault(false); + + $users = [ + ['name' => 'mike', 'id' => 1], + ['name' => 'jwage', 'id' => 2], + ['name' => 'ocramius', 'id' => 3], + ]; + + foreach ($users as $user) { + // Use addDeferredSql since "happy" is new column, that will not yet be present in schema if called by using addSql + $this->addDeferredSql('UPDATE user SET happy = true WHERE name = :name AND id = :id', $user); + } + } + write ~~~~~ diff --git a/src/AbstractMigration.php b/src/AbstractMigration.php index e13045c88f..f5888e59e2 100644 --- a/src/AbstractMigration.php +++ b/src/AbstractMigration.php @@ -37,6 +37,9 @@ abstract class AbstractMigration /** @var Query[] */ private array $plannedSql = []; + /** @var Query[] */ + private array $deferredSql = []; + private bool $frozen = false; public function __construct(Connection $connection, private readonly LoggerInterface $logger) @@ -135,12 +138,36 @@ protected function addSql( $this->plannedSql[] = new Query($sql, $params, $types); } + /** + * Adds SQL queries which should be executed after schema changes + * + * @param mixed[] $params + * @param mixed[] $types + */ + protected function addDeferredSql( + string $sql, + array $params = [], + array $types = [], + ): void { + if ($this->frozen) { + throw FrozenMigration::new(); + } + + $this->deferredSql[] = new Query($sql, $params, $types); + } + /** @return Query[] */ public function getSql(): array { return $this->plannedSql; } + /** @return Query[] */ + public function getDeferredSql(): array + { + return $this->deferredSql; + } + public function freeze(): void { $this->frozen = true; diff --git a/src/Version/DbalExecutor.php b/src/Version/DbalExecutor.php index 57712e4790..d9be11dfd8 100644 --- a/src/Version/DbalExecutor.php +++ b/src/Version/DbalExecutor.php @@ -145,6 +145,10 @@ private function executeMigration( $this->addSql(new Query($sql)); } + foreach ($migration->getDeferredSql() as $deferredSqlQuery) { + $this->addSql($deferredSqlQuery); + } + $migration->freeze(); if (count($this->sql) !== 0) { diff --git a/tests/AbstractMigrationTest.php b/tests/AbstractMigrationTest.php index 25e58bf3da..aa3bb58b0b 100644 --- a/tests/AbstractMigrationTest.php +++ b/tests/AbstractMigrationTest.php @@ -51,6 +51,13 @@ public function testAddSql(): void self::assertEquals([new Query('SELECT 1', [1], [2])], $this->migration->getSql()); } + public function testAddDeferredSql(): void + { + $this->migration->exposedAddDeferredSql('SELECT 2', [1], [2]); + + self::assertEquals([new Query('SELECT 2', [1], [2])], $this->migration->getDeferredSql()); + } + public function testThrowFrozenMigrationException(): void { $this->expectException(FrozenMigration::class); @@ -60,6 +67,15 @@ public function testThrowFrozenMigrationException(): void $this->migration->exposedAddSql('SELECT 1', [1], [2]); } + public function testThrowFrozenMigrationExceptionOnDeferredAdd(): void + { + $this->expectException(FrozenMigration::class); + $this->expectExceptionMessage('The migration is frozen and cannot be edited anymore.'); + + $this->migration->freeze(); + $this->migration->exposedAddDeferredSql('SELECT 2', [1], [2]); + } + public function testWarnIfOutputMessage(): void { $this->migration->warnIf(true, 'Warning was thrown'); diff --git a/tests/MigratorTest.php b/tests/MigratorTest.php index 58b492f19d..a14f9b1ca3 100644 --- a/tests/MigratorTest.php +++ b/tests/MigratorTest.php @@ -15,8 +15,10 @@ use Doctrine\Migrations\Metadata\Storage\MetadataStorage; use Doctrine\Migrations\MigratorConfiguration; use Doctrine\Migrations\ParameterFormatter; +use Doctrine\Migrations\Provider\DBALSchemaDiffProvider; use Doctrine\Migrations\Provider\SchemaDiffProvider; use Doctrine\Migrations\Tests\Stub\Functional\MigrateNotTouchingTheSchema; +use Doctrine\Migrations\Tests\Stub\Functional\MigrateWithDeferredSql; use Doctrine\Migrations\Tests\Stub\Functional\MigrationThrowsError; use Doctrine\Migrations\Tests\Stub\NonTransactional\MigrationNonTransactional; use Doctrine\Migrations\Version\DbalExecutor; @@ -73,6 +75,32 @@ public function testGetSql(): void ); } + public function testQueriesOrder(): void + { + $this->config->addMigrationsDirectory('DoctrineMigrations\\', __DIR__ . '/Stub/migrations-empty-folder'); + + $conn = $this->getSqliteConnection(); + $migrator = $this->createTestMigrator( + schemaDiff: new DBALSchemaDiffProvider($conn->createSchemaManager(), $conn->getDatabasePlatform()), + ); + + $migration = new MigrateWithDeferredSql($conn, $this->logger); + $plan = new MigrationPlan(new Version(MigrateWithDeferredSql::class), $migration, Direction::UP); + $planList = new MigrationPlanList([$plan], Direction::UP); + + $sql = $migrator->migrate($planList, $this->migratorConfiguration); + + self::assertArrayHasKey(MigrateWithDeferredSql::class, $sql); + self::assertSame( + [ + 'SELECT 1', + 'CREATE TABLE test (id INTEGER NOT NULL)', + 'INSERT INTO test(id) VALUES(123)', + ], + array_map(strval(...), $sql[MigrateWithDeferredSql::class]), + ); + } + public function testEmptyPlanShowsMessage(): void { $migrator = $this->createTestMigrator(); @@ -84,7 +112,7 @@ public function testEmptyPlanShowsMessage(): void self::assertStringContainsString('No migrations', $this->logger->records[0]['message']); } - protected function createTestMigrator(): DbalMigrator + protected function createTestMigrator(SchemaDiffProvider|null $schemaDiff = null): DbalMigrator { $eventManager = new EventManager(); $eventDispatcher = new EventDispatcher($this->conn, $eventManager); @@ -94,7 +122,7 @@ protected function createTestMigrator(): DbalMigrator $stopwatch = new Stopwatch(); $paramFormatter = $this->createMock(ParameterFormatter::class); $storage = $this->createMock(MetadataStorage::class); - $schemaDiff = $this->createMock(SchemaDiffProvider::class); + $schemaDiff ??= $this->createMock(SchemaDiffProvider::class); return new DbalMigrator( $this->conn, diff --git a/tests/Stub/AbstractMigrationStub.php b/tests/Stub/AbstractMigrationStub.php index 70179ba414..b3b4f0bd20 100644 --- a/tests/Stub/AbstractMigrationStub.php +++ b/tests/Stub/AbstractMigrationStub.php @@ -35,4 +35,13 @@ public function exposedAddSql(string $sql, array $params = [], array $types = [] { $this->addSql($sql, $params, $types); } + + /** + * @param int[] $params + * @param int[] $types + */ + public function exposedAddDeferredSql(string $sql, array $params = [], array $types = []): void + { + $this->addDeferredSql($sql, $params, $types); + } } diff --git a/tests/Stub/Functional/MigrateWithDeferredSql.php b/tests/Stub/Functional/MigrateWithDeferredSql.php new file mode 100644 index 0000000000..0e20443fe7 --- /dev/null +++ b/tests/Stub/Functional/MigrateWithDeferredSql.php @@ -0,0 +1,23 @@ +addDeferredSql('INSERT INTO test(id) VALUES(123)'); + + // Executed after queries from addSql() + $schema->createTable('test')->addColumn('id', 'integer'); + + // First to be executed + $this->addSql('SELECT 1'); + } +}