From 31406d7ef8166d19b0831f7f7047bec5eeb10dde Mon Sep 17 00:00:00 2001 From: Mark Story Date: Sat, 8 Nov 2025 23:45:46 -0500 Subject: [PATCH 1/2] Add first pass at migration table storage abstraction Extract the existing logic into another object. We could define an interface or extend this class for the new table storage. The Adapter layer can pick one table or the other based on config, and we can have both available at the same time for the upgrade process. --- src/Db/Adapter/AbstractAdapter.php | 109 +++-------- src/Db/Adapter/MigrationsTableStorage.php | 210 ++++++++++++++++++++++ src/Db/Adapter/PostgresAdapter.php | 1 - 3 files changed, 230 insertions(+), 90 deletions(-) create mode 100644 src/Db/Adapter/MigrationsTableStorage.php diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 4917a237..5424dd1d 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -129,21 +129,7 @@ public function setConnection(Connection $connection): AdapterInterface if (!$this->hasTable($this->getSchemaTableName())) { $this->createSchemaTable(); } else { - $table = new Table($this->getSchemaTableName(), [], $this); - if (!$table->hasColumn('migration_name')) { - $table - ->addColumn( - 'migration_name', - 'string', - ['limit' => 100, 'after' => 'version', 'default' => null, 'null' => true], - ) - ->save(); - } - if (!$table->hasColumn('breakpoint')) { - $table - ->addColumn('breakpoint', 'boolean', ['default' => false, 'null' => false]) - ->save(); - } + $this->migrationsTable()->upgradeTable(); } return $this; @@ -357,26 +343,7 @@ public function hasColumn(string $tableName, string $columnName): bool */ public function createSchemaTable(): void { - try { - $options = [ - 'id' => false, - 'primary_key' => 'version', - ]; - - $table = new Table($this->getSchemaTableName(), $options, $this); - $table->addColumn('version', 'biginteger', ['null' => false]) - ->addColumn('migration_name', 'string', ['limit' => 100, 'default' => null, 'null' => true]) - ->addColumn('start_time', 'timestamp', ['default' => null, 'null' => true]) - ->addColumn('end_time', 'timestamp', ['default' => null, 'null' => true]) - ->addColumn('breakpoint', 'boolean', ['default' => false, 'null' => false]) - ->save(); - } catch (Exception $exception) { - throw new InvalidArgumentException( - 'There was a problem creating the schema table: ' . $exception->getMessage(), - (int)$exception->getCode(), - $exception, - ); - } + $this->migrationsTable()->createTable(); } /** @@ -815,6 +782,18 @@ public function getVersions(): array return array_keys($rows); } + /** + * Get the migrations table storage implementation. + * + * @return \Migrations\Db\Adapter\MigrationsTableStorage + * @internal + */ + protected function migrationsTable(): MigrationsTableStorage + { + // TODO Use configure/auto-detect which implmentation to use. + return new MigrationsTableStorage($this, $this->getSchemaTableName()); + } + /** * {@inheritDoc} * @@ -832,10 +811,7 @@ public function getVersionLog(): array default: throw new RuntimeException('Invalid version_order configuration option'); } - $query = $this->getSelectBuilder(); - $query->select('*') - ->from($this->getSchemaTableName()) - ->orderBy($orderBy); + $query = $this->migrationsTable()->getVersions($orderBy); // This will throw an exception if doing a --dry-run without any migrations as phinxlog // does not exist, so in that case, we can just expect to trivially return empty set @@ -862,24 +838,10 @@ public function getVersionLog(): array public function migrated(MigrationInterface $migration, string $direction, string $startTime, string $endTime): AdapterInterface { if (strcasecmp($direction, MigrationInterface::UP) === 0) { - $query = $this->getInsertBuilder(); - $query->insert(['version', 'migration_name', 'start_time', 'end_time', 'breakpoint']) - ->into($this->getSchemaTableName()) - ->values([ - 'version' => (string)$migration->getVersion(), - 'migration_name' => substr($migration->getName(), 0, 100), - 'start_time' => $startTime, - 'end_time' => $endTime, - 'breakpoint' => 0, - ]); - $this->executeQuery($query); + $this->migrationsTable()->recordUp($migration, $startTime, $endTime); } else { // down - $query = $this->getDeleteBuilder(); - $query->delete() - ->from($this->getSchemaTableName()) - ->where(['version' => $migration->getVersion()]); - $this->executeQuery($query); + $this->migrationsTable()->recordDown($migration); } return $this; @@ -890,19 +852,7 @@ public function migrated(MigrationInterface $migration, string $direction, strin */ public function toggleBreakpoint(MigrationInterface $migration): AdapterInterface { - $params = [ - $migration->getVersion(), - ]; - $this->query( - sprintf( - 'UPDATE %1$s SET %2$s = CASE %2$s WHEN true THEN false ELSE true END, %4$s = %4$s WHERE %3$s = ?;', - $this->quoteTableName($this->getSchemaTableName()), - $this->quoteColumnName('breakpoint'), - $this->quoteColumnName('version'), - $this->quoteColumnName('start_time'), - ), - $params, - ); + $this->migrationsTable()->toggleBreakpoint($migration); return $this; } @@ -912,17 +862,7 @@ public function toggleBreakpoint(MigrationInterface $migration): AdapterInterfac */ public function resetAllBreakpoints(): int { - $query = $this->getUpdateBuilder(); - $query->update($this->getSchemaTableName()) - ->set([ - 'breakpoint' => 0, - 'start_time' => $query->identifier('start_time'), - ]) - ->where([ - 'breakpoint !=' => 0, - ]); - - return $this->executeQuery($query); + return $this->migrationsTable()->resetAllBreakpoints(); } /** @@ -954,16 +894,7 @@ public function unsetBreakpoint(MigrationInterface $migration): AdapterInterface */ protected function markBreakpoint(MigrationInterface $migration, bool $state): AdapterInterface { - $query = $this->getUpdateBuilder(); - $query->update($this->getSchemaTableName()) - ->set([ - 'breakpoint' => (int)$state, - 'start_time' => $query->identifier('start_time'), - ]) - ->where([ - 'version' => $migration->getVersion(), - ]); - $this->executeQuery($query); + $this->migrationsTable()->markBreakpoint($migration, $state); return $this; } diff --git a/src/Db/Adapter/MigrationsTableStorage.php b/src/Db/Adapter/MigrationsTableStorage.php new file mode 100644 index 00000000..ba7cdc2d --- /dev/null +++ b/src/Db/Adapter/MigrationsTableStorage.php @@ -0,0 +1,210 @@ +schemaTableName; + } + + /** + * Gets all the migration versions. + * + * @param array $orderBy The order by clause. + * @return \Cake\Database\Query\SelectQuery + */ + public function getVersions(array $orderBy): SelectQuery + { + $query = $this->adapter->getSelectBuilder(); + $query->select('*') + ->from($this->getSchemaTableName()) + ->orderBy($orderBy); + + return $query; + } + + /** + * Records that a migration was run in the database. + * + * @param \Migrations\MigrationInterface $migration Migration + * @param string $startTime Start time + * @param string $endTime End time + * @return void + */ + public function recordUp(MigrationInterface $migration, string $startTime, string $endTime): void + { + $query = $this->adapter->getInsertBuilder(); + $query->insert(['version', 'migration_name', 'start_time', 'end_time', 'breakpoint']) + ->into($this->getSchemaTableName()) + ->values([ + 'version' => (string)$migration->getVersion(), + 'migration_name' => substr($migration->getName(), 0, 100), + 'start_time' => $startTime, + 'end_time' => $endTime, + 'breakpoint' => 0, + ]); + $this->adapter->executeQuery($query); + } + + /** + * Removes the record of a migration having been run. + * + * @param \Migrations\MigrationInterface $migration Migration + * @return void + */ + public function recordDown(MigrationInterface $migration): void + { + $query = $this->adapter->getDeleteBuilder(); + $query->delete() + ->from($this->getSchemaTableName()) + ->where(['version' => (string)$migration->getVersion()]); + $this->adapter->executeQuery($query); + } + + /** + * Toggles the breakpoint state of a migration. + * + * @param \Migrations\MigrationInterface $migration Migration + * @return void + */ + public function toggleBreakpoint(MigrationInterface $migration): void + { + $params = [ + $migration->getVersion(), + ]; + $this->adapter->query( + sprintf( + 'UPDATE %1$s SET %2$s = CASE %2$s WHEN true THEN false ELSE true END, %4$s = %4$s WHERE %3$s = ?;', + $this->adapter->quoteTableName($this->getSchemaTableName()), + $this->adapter->quoteColumnName('breakpoint'), + $this->adapter->quoteColumnName('version'), + $this->adapter->quoteColumnName('start_time'), + ), + $params, + ); + } + + /** + * Resets all breakpoints. + * + * @return int The number of affected rows. + */ + public function resetAllBreakpoints(): int + { + $query = $this->adapter->getUpdateBuilder(); + $query->update($this->getSchemaTableName()) + ->set([ + 'breakpoint' => 0, + 'start_time' => $query->identifier('start_time'), + ]) + ->where([ + 'breakpoint !=' => 0, + ]); + + return $this->adapter->executeQuery($query); + } + + /** + * Marks a migration as a breakpoint or not depending on $state. + * + * @param \Migrations\MigrationInterface $migration Migration + * @param bool $state The breakpoint state to set. + * @return void + */ + public function markBreakpoint(MigrationInterface $migration, bool $state): void + { + $query = $this->adapter->getUpdateBuilder(); + $query->update($this->getSchemaTableName()) + ->set([ + 'breakpoint' => (int)$state, + 'start_time' => $query->identifier('start_time'), + ]) + ->where([ + 'version' => $migration->getVersion(), + ]); + $this->adapter->executeQuery($query); + } + + /** + * Creates the migration storage table + * + * @return void + * @throws \InvalidArgumentException When there is a problem creating the table. + */ + public function createTable(): void + { + try { + $options = [ + 'id' => false, + 'primary_key' => 'version', + ]; + + $table = new Table($this->getSchemaTableName(), $options, $this->adapter); + $table->addColumn('version', 'biginteger', ['null' => false]) + ->addColumn('migration_name', 'string', ['limit' => 100, 'default' => null, 'null' => true]) + ->addColumn('start_time', 'timestamp', ['default' => null, 'null' => true]) + ->addColumn('end_time', 'timestamp', ['default' => null, 'null' => true]) + ->addColumn('breakpoint', 'boolean', ['default' => false, 'null' => false]) + ->save(); + } catch (Exception $exception) { + throw new InvalidArgumentException( + 'There was a problem creating the schema table: ' . $exception->getMessage(), + (int)$exception->getCode(), + $exception, + ); + } + } + + /** + * Upgrades the migration storage table + * + * @return void + */ + public function upgradeTable(): void + { + $table = new Table($this->getSchemaTableName(), [], $this->adapter); + if (!$table->hasColumn('migration_name')) { + $table + ->addColumn( + 'migration_name', + 'string', + ['limit' => 100, 'after' => 'version', 'default' => null, 'null' => true], + ) + ->save(); + } + if (!$table->hasColumn('breakpoint')) { + $table + ->addColumn('breakpoint', 'boolean', ['default' => false, 'null' => false]) + ->save(); + } + } +} diff --git a/src/Db/Adapter/PostgresAdapter.php b/src/Db/Adapter/PostgresAdapter.php index 9f792f1e..feac84b8 100644 --- a/src/Db/Adapter/PostgresAdapter.php +++ b/src/Db/Adapter/PostgresAdapter.php @@ -945,7 +945,6 @@ protected function getForeignKeySqlDefinition(ForeignKey $foreignKey, string $ta */ public function createSchemaTable(): void { - // Create the public/custom schema if it doesn't already exist if ($this->hasSchema($this->getGlobalSchemaName()) === false) { $this->createSchema($this->getGlobalSchemaName()); } From 0678475053233a77740894cab1ca28aee1fb28cb Mon Sep 17 00:00:00 2001 From: Mark Story Date: Mon, 17 Nov 2025 22:53:53 -0500 Subject: [PATCH 2/2] Fix cs and stan --- src/Db/Adapter/AbstractAdapter.php | 2 -- src/Db/Adapter/MigrationsTableStorage.php | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Db/Adapter/AbstractAdapter.php b/src/Db/Adapter/AbstractAdapter.php index 5424dd1d..92a1e022 100644 --- a/src/Db/Adapter/AbstractAdapter.php +++ b/src/Db/Adapter/AbstractAdapter.php @@ -19,7 +19,6 @@ use Cake\Database\Schema\SchemaDialect; use Cake\I18n\Date; use Cake\I18n\DateTime; -use Exception; use InvalidArgumentException; use Migrations\Config\Config; use Migrations\Db\Action\AddColumn; @@ -37,7 +36,6 @@ use Migrations\Db\AlterInstructions; use Migrations\Db\InsertMode; use Migrations\Db\Literal; -use Migrations\Db\Table; use Migrations\Db\Table\CheckConstraint; use Migrations\Db\Table\Column; use Migrations\Db\Table\ForeignKey; diff --git a/src/Db/Adapter/MigrationsTableStorage.php b/src/Db/Adapter/MigrationsTableStorage.php index ba7cdc2d..9c3a1b20 100644 --- a/src/Db/Adapter/MigrationsTableStorage.php +++ b/src/Db/Adapter/MigrationsTableStorage.php @@ -1,4 +1,18 @@