Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce addDeferredSql method in migration class #1478

Open
wants to merge 1 commit into
base: 4.0.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion docs/en/reference/migration-classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
~~~~~

Expand Down
27 changes: 27 additions & 0 deletions src/AbstractMigration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/Version/DbalExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions tests/AbstractMigrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand Down
32 changes: 30 additions & 2 deletions tests/MigratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions tests/Stub/AbstractMigrationStub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
23 changes: 23 additions & 0 deletions tests/Stub/Functional/MigrateWithDeferredSql.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Doctrine\Migrations\Tests\Stub\Functional;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

class MigrateWithDeferredSql extends AbstractMigration
{
public function up(Schema $schema): void
{
// Last to be executed
$this->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');
}
}
Loading