Skip to content

Commit

Permalink
Fix recreating indices for renamed/removed columns.
Browse files Browse the repository at this point in the history
refs #2129
  • Loading branch information
ndm2 committed Nov 1, 2022
1 parent 8891e74 commit 1c9f9e9
Show file tree
Hide file tree
Showing 2 changed files with 694 additions and 27 deletions.
244 changes: 217 additions & 27 deletions src/Phinx/Db/Adapter/SQLiteAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,11 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter
return $newState + $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down Expand Up @@ -793,6 +797,178 @@ protected function getDeclaringIndexSql(string $tableName, string $indexName): s
return $sql;
}

/**
* Obtains index and trigger information for a table.
*
* They will be stored in the state as arrays under the `indices` and `triggers`
* keys accordingly.
*
* Index columns defined as expressions, as for example in `ON (ABS(id), other)`,
* will appear as `null`, so for the given example the columns for the index would
* look like `[null, 'other']`.
*
* @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
* @param string $tableName The name of table being processed
* @return \Phinx\Db\Util\AlterInstructions
*/
protected function bufferIndicesAndTriggers(AlterInstructions $instructions, string $tableName): AlterInstructions
{
$instructions->addPostStep(function (array $state) use ($tableName): array {
$state['indices'] = [];
$state['triggers'] = [];

$rows = $this->fetchAll(
sprintf(
"
SELECT *
FROM sqlite_master
WHERE
(`type` = 'index' OR `type` = 'trigger')
AND tbl_name = %s
AND sql IS NOT NULL
",
$this->quoteValue($tableName)
)
);

$schema = $this->getSchemaName($tableName, true)['schema'];

foreach ($rows as $row) {
switch ($row['type']) {
case 'index':
$info = $this->fetchAll(
sprintf('PRAGMA %sindex_info(%s)', $schema, $this->quoteValue($row['name']))
);

$columns = array_map(
function ($column) {
if ($column === null) {
return null;
}

return strtolower($column);
},
array_column($info, 'name')
);
$hasExpressions = in_array(null, $columns, true);

$index = [
'columns' => $columns,
'hasExpressions' => $hasExpressions,
];

$state['indices'][] = $index + $row;
break;

case 'trigger':
$state['triggers'][] = $row;
break;
}
}

return $state;
});

return $instructions;
}

/**
* Filters out indices that reference a removed column.
*
* @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
* @param string $columnName The name of the removed column
* @return \Phinx\Db\Util\AlterInstructions
*/
protected function filterIndicesForRemovedColumn(
AlterInstructions $instructions,
string $columnName
): AlterInstructions {
$instructions->addPostStep(function (array $state) use ($columnName): array {
foreach ($state['indices'] as $key => $index) {
if (
!$index['hasExpressions'] &&
in_array(strtolower($columnName), $index['columns'], true)
) {
unset($state['indices'][$key]);
}
}

return $state;
});

return $instructions;
}

/**
* Updates indices that reference a renamed column.
*
* @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify
* @param string $oldColumnName The old column name
* @param string $newColumnName The new column name
* @return \Phinx\Db\Util\AlterInstructions
*/
protected function updateIndicesForRenamedColumn(
AlterInstructions $instructions,
string $oldColumnName,
string $newColumnName
): AlterInstructions {
$instructions->addPostStep(function (array $state) use ($oldColumnName, $newColumnName): array {
foreach ($state['indices'] as $key => $index) {
if (
!$index['hasExpressions'] &&
in_array(strtolower($oldColumnName), $index['columns'], true)
) {
$pattern = '
/
(INDEX.+?ON\s.+?)
(\(\s*|,\s*) # opening parenthesis or comma
(?:`|"|\[)? # optional opening quote
(%s) # column name
(?:`|"|\])? # optional closing quote
(\s+COLLATE\s+.+?)? # optional collation
(\s+(?:ASC|DESC))? # optional order
(\s*,|\s*\)) # comma or closing parenthesis
/isx';

$newColumnName = $this->quoteColumnName($newColumnName);

$state['indices'][$key]['sql'] = preg_replace(
sprintf($pattern, preg_quote($oldColumnName, '/')),
"\\1\\2$newColumnName\\4\\5\\6",
$index['sql']
);
}
}

return $state;
});

return $instructions;
}

/**
* Recreates indices and triggers.
*
* @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to process
* @return \Phinx\Db\Util\AlterInstructions
*/
protected function recreateIndicesAndTriggers(AlterInstructions $instructions): AlterInstructions
{
$instructions->addPostStep(function (array $state): array {
foreach ($state['indices'] as $index) {
$this->execute($index['sql']);
}

foreach ($state['triggers'] as $trigger) {
$this->execute($trigger['sql']);
}

return $state;
});

return $instructions;
}

/**
* Copies all the data from a tmp table to another table
*
Expand Down Expand Up @@ -832,31 +1008,13 @@ protected function copyAndDropTmpTable(AlterInstructions $instructions, string $
$state['selectColumns']
);

$rows = $this->fetchAll(
sprintf(
"
SELECT *
FROM sqlite_master
WHERE
(`type` = 'index' OR `type` = 'trigger')
AND tbl_name = %s
AND sql IS NOT NULL
",
$this->quoteValue($tableName)
)
);

$this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName)));
$this->execute(sprintf(
'ALTER TABLE %s RENAME TO %s',
$this->quoteTableName($state['tmpTableName']),
$this->quoteTableName($tableName)
));

foreach ($rows as $row) {
$this->execute($row['sql']);
}

return $state;
});

Expand Down Expand Up @@ -969,7 +1127,12 @@ protected function getRenameColumnInstructions(string $tableName, string $column
return $newState + $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->updateIndicesForRenamedColumn($instructions, $columnName, $newColumnName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down Expand Up @@ -998,7 +1161,11 @@ protected function getChangeColumnInstructions(string $tableName, string $column
return $newState + $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down Expand Up @@ -1030,7 +1197,12 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa
return $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->filterIndicesForRemovedColumn($instructions, $columnName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down Expand Up @@ -1306,7 +1478,11 @@ protected function getAddPrimaryKeyInstructions(Table $table, string $column): A
return compact('selectColumns', 'writeColumns') + $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand All @@ -1316,7 +1492,8 @@ protected function getAddPrimaryKeyInstructions(Table $table, string $column): A
*/
protected function getDropPrimaryKeyInstructions(Table $table, string $column): AlterInstructions
{
$instructions = $this->beginAlterByCopyTable($table->getName());
$tableName = $table->getName();
$instructions = $this->beginAlterByCopyTable($tableName);

$instructions->addPostStep(function ($state) {
$search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/";
Expand All @@ -1335,7 +1512,12 @@ protected function getDropPrimaryKeyInstructions(Table $table, string $column):
return $newState + $state;
});

return $this->copyAndDropTmpTable($instructions, $table->getName());
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->filterIndicesForRemovedColumn($instructions, $column);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down Expand Up @@ -1383,7 +1565,11 @@ protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreig
return compact('selectColumns', 'writeColumns') + $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down Expand Up @@ -1441,7 +1627,11 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr
return $newState + $state;
});

return $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->bufferIndicesAndTriggers($instructions, $tableName);
$instructions = $this->copyAndDropTmpTable($instructions, $tableName);
$instructions = $this->recreateIndicesAndTriggers($instructions);

return $instructions;
}

/**
Expand Down
Loading

0 comments on commit 1c9f9e9

Please sign in to comment.