Skip to content

Allow individual migrations to opt out of transactions #2424

Description

@Bilge

Feature request

Please provide a supported way for an individual migration to opt out of Phinx's automatic transaction wrapper.

Compelling PostgreSQL use case

PostgreSQL forbids CREATE INDEX CONCURRENTLY and DROP INDEX CONCURRENTLY inside a transaction block. Concurrent index creation is essential when indexing a large live table because ordinary CREATE INDEX blocks writes for the duration of the build.

In a concrete production case, the table contains about 204 million rows and an 11 GiB heap. Building a small BRIN index concurrently took 5 minutes 13 seconds. An ordinary index build would prevent ingestion from writing to that table for the entire operation.

Phinx 0.16.12, and the current 0.x branch, unconditionally call beginTransaction() for every migration whenever the adapter reports transaction support:

https://github.com/cakephp/phinx/blob/0.x/src/Phinx/Migration/Manager/Environment.php#L89-L92

A straightforward migration therefore fails:

public function up(): void
{
    $this->execute(
        'CREATE INDEX CONCURRENTLY events_date_brin_idx '
        . 'ON events USING brin (date)'
    );
}
ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block

Current workaround

The migration must manually interfere with Phinx's transaction lifecycle:

$this->execute('COMMIT');
$this->execute('CREATE INDEX CONCURRENTLY ...');
$this->execute('BEGIN');

This couples application migrations to Environment::executeMigration() internals, complicates failure handling, and risks breaking migration bookkeeping, dry runs, or future Phinx changes. A connection failure can also leave the workaround unable to restore the transaction Phinx expects.

Running the index separately from Phinx is also undesirable: deployment then has two schema-change systems whose state can diverge.

Proposed API

A per-migration method with a backwards-compatible default would be sufficient:

public function isTransactional(): bool
{
    return false;
}

Environment::executeMigration() would wrap the migration only when both conditions hold:

$transactional = $adapter->hasTransactions() && $migration->isTransactional();

A marker interface, attribute, or protected property would also solve the problem. The important requirement is that the decision belongs to the migration rather than requiring the migration to issue COMMIT and BEGIN itself.

Expected behavior

  • Existing migrations remain transactional by default.
  • Opting out applies consistently to up(), down(), and change().
  • Adapters without transaction support are unaffected.
  • The migration is recorded only after its body completes successfully.
  • Documentation warns that non-transactional DDL can leave partial state, such as PostgreSQL's invalid index after an interrupted concurrent build, and that migrations must be retry-safe.

Prior art

  • Doctrine Migrations exposes this through isTransactional().
  • Rails supports disable_ddl_transaction! for concurrent indexes.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions