Skip to content

Commit f32fa4e

Browse files
committed
Rework sqlite fk pragma handling
Signed-off-by: Matthew Peveler <[email protected]>
1 parent d550d79 commit f32fa4e

File tree

5 files changed

+103
-60
lines changed

5 files changed

+103
-60
lines changed

src/Phinx/Db/Adapter/AdapterInterface.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,13 @@ public function rollbackTransaction(): void;
275275
*/
276276
public function execute(string $sql, array $params = []): int;
277277

278+
/**
279+
* Function to be called before executing any migration actions.
280+
*
281+
* @return array
282+
*/
283+
public function preExecuteActions(): array;
284+
278285
/**
279286
* Executes a list of migration actions for the given table
280287
*
@@ -284,6 +291,15 @@ public function execute(string $sql, array $params = []): int;
284291
*/
285292
public function executeActions(Table $table, array $actions): void;
286293

294+
/**
295+
* Function to be called after executing any migration actions.
296+
*
297+
* @param array $tableNames List of table names that were affected by the actions
298+
* @param array $preOptions Options that were set before executing the actions
299+
* @return void
300+
*/
301+
public function postExecuteActions(array $tableNames, array $preOptions): void;
302+
287303
/**
288304
* Returns a new Query object
289305
*

src/Phinx/Db/Adapter/PdoAdapter.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,14 @@ public function changeComment(Table $table, $newComment): void
10011001
*/
10021002
abstract protected function getChangeCommentInstructions(Table $table, ?string $newComment): AlterInstructions;
10031003

1004+
/**
1005+
* {@inheritDoc}
1006+
*/
1007+
public function preExecuteActions(): array
1008+
{
1009+
return [];
1010+
}
1011+
10041012
/**
10051013
* {@inheritDoc}
10061014
*
@@ -1126,4 +1134,12 @@ public function executeActions(Table $table, array $actions): void
11261134

11271135
$this->executeAlterSteps($table->getName(), $instructions);
11281136
}
1137+
1138+
/**
1139+
* {@inheritDoc}
1140+
*/
1141+
public function postExecuteActions(array $tableNames, array $preOptions): void
1142+
{
1143+
1144+
}
11291145
}

src/Phinx/Db/Adapter/SQLiteAdapter.php

Lines changed: 64 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,57 +1034,52 @@ protected function recreateIndicesAndTriggers(AlterInstructions $instructions):
10341034
* the given table, and of those tables whose constraints are
10351035
* targeting it.
10361036
*
1037-
* @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to process
1038-
* @param string $tableName The name of the table for which to check constraints.
1037+
* @param string|array<string> $tableName The name of the table for which to check constraints.
10391038
* @return \Phinx\Db\Util\AlterInstructions
10401039
*/
1041-
protected function validateForeignKeys(AlterInstructions $instructions, string $tableName): AlterInstructions
1040+
protected function validateForeignKeys(string|array $tableNames): void
10421041
{
1043-
$instructions->addPostStep(function ($state) use ($tableName) {
1044-
$tablesToCheck = [
1045-
$tableName,
1046-
];
1047-
1048-
$otherTables = $this
1049-
->query(
1050-
"SELECT name FROM sqlite_master WHERE type = 'table' AND name != ?",
1051-
[$tableName],
1052-
)
1053-
->fetchAll();
1054-
1055-
foreach ($otherTables as $otherTable) {
1056-
$foreignKeyList = $this->getTableInfo($otherTable['name'], 'foreign_key_list');
1057-
foreach ($foreignKeyList as $foreignKey) {
1058-
if (strcasecmp($foreignKey['table'], $tableName) === 0) {
1059-
$tablesToCheck[] = $otherTable['name'];
1060-
break;
1061-
}
1062-
}
1063-
}
1064-
1065-
$tablesToCheck = array_unique(array_map('strtolower', $tablesToCheck));
1042+
if (!is_array($tableNames)) {
1043+
$tableNames = [$tableNames];
1044+
}
10661045

1067-
foreach ($tablesToCheck as $tableToCheck) {
1068-
$schema = $this->getSchemaName($tableToCheck, true)['schema'];
1046+
$tablesToCheck = $tableNames;
10691047

1070-
$stmt = $this->query(
1071-
sprintf('PRAGMA %sforeign_key_check(%s)', $schema, $this->quoteTableName($tableToCheck)),
1072-
);
1073-
$row = $stmt->fetch();
1074-
$stmt->closeCursor();
1048+
$otherTables = $this
1049+
->query(
1050+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT IN (" . implode(',',array_fill(0, count($tableNames), '?')) . ')',
1051+
$tableNames
1052+
)
1053+
->fetchAll();
10751054

1076-
if (is_array($row)) {
1077-
throw new RuntimeException(sprintf(
1078-
'Integrity constraint violation: FOREIGN KEY constraint on `%s` failed.',
1079-
$tableToCheck,
1080-
));
1055+
foreach ($otherTables as $otherTable) {
1056+
$foreignKeyList = $this->getTableInfo($otherTable['name'], 'foreign_key_list');
1057+
foreach ($foreignKeyList as $foreignKey) {
1058+
if (in_array(strtolower($foreignKey['table']), $tableNames)) {
1059+
$tablesToCheck[] = $otherTable['name'];
1060+
break;
10811061
}
10821062
}
1063+
}
10831064

1084-
return $state;
1085-
});
1065+
$tablesToCheck = array_unique(array_map('strtolower', $tablesToCheck));
10861066

1087-
return $instructions;
1067+
foreach ($tablesToCheck as $tableToCheck) {
1068+
$schema = $this->getSchemaName($tableToCheck, true)['schema'];
1069+
1070+
$stmt = $this->query(
1071+
sprintf('PRAGMA %sforeign_key_check(%s)', $schema, $this->quoteTableName($tableToCheck)),
1072+
);
1073+
$row = $stmt->fetch();
1074+
$stmt->closeCursor();
1075+
1076+
if (is_array($row)) {
1077+
throw new RuntimeException(sprintf(
1078+
'Integrity constraint violation: FOREIGN KEY constraint on `%s` failed.',
1079+
$tableToCheck,
1080+
));
1081+
}
1082+
}
10881083
}
10891084

10901085
/**
@@ -1239,7 +1234,6 @@ protected function endAlterByCopyTable(
12391234
string $tableName,
12401235
?string $renamedOrRemovedColumnName = null,
12411236
?string $newColumnName = null,
1242-
bool $validateForeignKeys = true,
12431237
): AlterInstructions {
12441238
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
12451239

@@ -1251,26 +1245,9 @@ protected function endAlterByCopyTable(
12511245
}
12521246
}
12531247

1254-
$foreignKeysEnabled = (bool)$this->fetchRow('PRAGMA foreign_keys')['foreign_keys'];
1255-
1256-
if ($foreignKeysEnabled) {
1257-
$instructions->addPostStep('PRAGMA foreign_keys = OFF');
1258-
}
1259-
12601248
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
12611249
$instructions = $this->recreateIndicesAndTriggers($instructions);
12621250

1263-
if ($foreignKeysEnabled) {
1264-
$instructions->addPostStep('PRAGMA foreign_keys = ON');
1265-
}
1266-
1267-
if (
1268-
$foreignKeysEnabled &&
1269-
$validateForeignKeys
1270-
) {
1271-
$instructions = $this->validateForeignKeys($instructions, $tableName);
1272-
}
1273-
12741251
return $instructions;
12751252
}
12761253

@@ -1661,7 +1638,7 @@ protected function getDropPrimaryKeyInstructions(Table $table, string $column):
16611638
return $newState + $state;
16621639
});
16631640

1664-
return $this->endAlterByCopyTable($instructions, $tableName, null, null, false);
1641+
return $this->endAlterByCopyTable($instructions, $tableName, null, null);
16651642
}
16661643

16671644
/**
@@ -2013,4 +1990,31 @@ public function getDecoratedConnection(): Connection
20131990

20141991
return $this->decoratedConnection = $this->buildConnection(SqliteDriver::class, $options);
20151992
}
1993+
1994+
/**
1995+
* {@inheritDoc}
1996+
*/
1997+
public function preExecuteActions(): array
1998+
{
1999+
$foreignKeysEnabled = (bool)$this->fetchRow('PRAGMA foreign_keys')['foreign_keys'];
2000+
2001+
if ($foreignKeysEnabled) {
2002+
$this->execute('PRAGMA foreign_keys = OFF');
2003+
}
2004+
2005+
return [
2006+
'foreignKeysEnabled' => $foreignKeysEnabled,
2007+
];
2008+
}
2009+
2010+
/**
2011+
* {@inheritDoc}
2012+
*/
2013+
public function postExecuteActions(array $tableNames, array $preOptions): void
2014+
{
2015+
if ($preOptions['foreignKeysEnabled']) {
2016+
$this->execute('PRAGMA foreign_keys = ON');
2017+
$this->validateForeignKeys($tableNames);
2018+
}
2019+
}
20162020
}

src/Phinx/Db/Plan/Plan.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,21 @@ protected function inverseUpdatesSequence(): array
143143
*/
144144
public function execute(AdapterInterface $executor): void
145145
{
146+
$preOptions = $executor->preExecuteActions();
147+
146148
foreach ($this->tableCreates as $newTable) {
147149
$executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
148150
}
149151

152+
$tables = [];
150153
foreach ($this->updatesSequence() as $updates) {
151154
foreach ($updates as $update) {
155+
$tables[] = $update->getTable()->getName();
152156
$executor->executeActions($update->getTable(), $update->getActions());
153157
}
154158
}
159+
160+
$executor->postExecuteActions(array_unique($tables), $preOptions);
155161
}
156162

157163
/**

tests/Phinx/Db/Adapter/SQLiteAdapterTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2387,6 +2387,7 @@ public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange
23872387
*/
23882388
public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange()
23892389
{
2390+
/** @var \Phinx\Db\Adapter\AdapterInterface&\PHPUnit\Framework\MockObject\MockObject */
23902391
$adapter = $this
23912392
->getMockBuilder(SQLiteAdapter::class)
23922393
->setConstructorArgs([SQLITE_DB_CONFIG, new ArrayInput([]), new NullOutput()])

0 commit comments

Comments
 (0)