-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Preserve check constraint during column move #19105
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,26 +8,24 @@ | |
use PhpMyAdmin\DatabaseInterface; | ||
use PhpMyAdmin\Message; | ||
use PhpMyAdmin\ResponseRenderer; | ||
use PhpMyAdmin\Table; | ||
use PhpMyAdmin\SqlParser\Components\CreateDefinition; | ||
use PhpMyAdmin\SqlParser\Parser; | ||
use PhpMyAdmin\SqlParser\Statements\CreateStatement; | ||
use PhpMyAdmin\Template; | ||
use PhpMyAdmin\Util; | ||
|
||
use function __; | ||
use function array_diff; | ||
use function array_keys; | ||
use function array_search; | ||
use function array_splice; | ||
use function assert; | ||
use function count; | ||
use function implode; | ||
use function in_array; | ||
use function is_array; | ||
use function mb_strtoupper; | ||
use function sprintf; | ||
use function str_replace; | ||
|
||
final class MoveColumnsController extends AbstractController | ||
{ | ||
/** @var Table The table object */ | ||
private $tableObj; | ||
|
||
/** @var DatabaseInterface */ | ||
private $dbi; | ||
|
||
|
@@ -40,123 +38,29 @@ public function __construct( | |
) { | ||
parent::__construct($response, $template, $db, $table); | ||
$this->dbi = $dbi; | ||
$this->tableObj = $this->dbi->getTable($this->db, $this->table); | ||
} | ||
|
||
public function __invoke(): void | ||
{ | ||
if (! isset($_POST['move_columns']) || ! is_array($_POST['move_columns']) || ! $this->response->isAjax()) { | ||
$moveColumns = $_POST['move_columns'] ?? null; | ||
$previewSql = $_REQUEST['preview_sql'] ?? null; | ||
if (! is_array($moveColumns) || ! $this->response->isAjax()) { | ||
$this->response->setRequestStatus(false); | ||
|
||
return; | ||
} | ||
|
||
$this->dbi->selectDb($this->db); | ||
$createTableSql = $this->dbi->getTable($this->db, $this->table)->showCreate(); | ||
$sql_query = $this->generateAlterTableSql($createTableSql, $moveColumns); | ||
|
||
/** | ||
* load the definitions for all columns | ||
*/ | ||
$columns = $this->dbi->getColumnsFull($this->db, $this->table); | ||
$column_names = array_keys($columns); | ||
$changes = []; | ||
|
||
// @see https://mariadb.com/kb/en/library/changes-improvements-in-mariadb-102/#information-schema | ||
$usesLiteralNull = $this->dbi->isMariaDB() && $this->dbi->getVersion() >= 100200; | ||
$defaultNullValue = $usesLiteralNull ? 'NULL' : null; | ||
// move columns from first to last | ||
for ($i = 0, $l = count($_POST['move_columns']); $i < $l; $i++) { | ||
$column = $_POST['move_columns'][$i]; | ||
// is this column already correctly placed? | ||
if ($column_names[$i] == $column) { | ||
continue; | ||
} | ||
|
||
// it is not, let's move it to index $i | ||
$data = $columns[$column]; | ||
$extracted_columnspec = Util::extractColumnSpec($data['Type']); | ||
if (isset($data['Extra']) && $data['Extra'] === 'on update CURRENT_TIMESTAMP') { | ||
$extracted_columnspec['attribute'] = $data['Extra']; | ||
unset($data['Extra']); | ||
} | ||
|
||
$timeType = $data['Type'] === 'timestamp' || $data['Type'] === 'datetime'; | ||
$timeDefault = $data['Default'] === 'CURRENT_TIMESTAMP' || $data['Default'] === 'current_timestamp()'; | ||
$current_timestamp = $timeType && $timeDefault; | ||
|
||
$uuidType = $data['Type'] === 'uuid'; | ||
$uuidDefault = $data['Default'] === 'UUID' || $data['Default'] === 'uuid()'; | ||
$uuid = $uuidType && $uuidDefault; | ||
|
||
// @see https://mariadb.com/kb/en/library/information-schema-columns-table/#examples | ||
if ($data['Null'] === 'YES' && in_array($data['Default'], [$defaultNullValue, null])) { | ||
$default_type = 'NULL'; | ||
} elseif ($current_timestamp) { | ||
$default_type = 'CURRENT_TIMESTAMP'; | ||
} elseif ($uuid) { | ||
$default_type = 'UUID'; | ||
} elseif ($data['Default'] === null) { | ||
$default_type = 'NONE'; | ||
} else { | ||
$default_type = 'USER_DEFINED'; | ||
} | ||
|
||
$virtual = [ | ||
'VIRTUAL', | ||
'PERSISTENT', | ||
'VIRTUAL GENERATED', | ||
'STORED GENERATED', | ||
]; | ||
$data['Virtuality'] = ''; | ||
$data['Expression'] = ''; | ||
if (isset($data['Extra']) && in_array($data['Extra'], $virtual)) { | ||
$data['Virtuality'] = str_replace(' GENERATED', '', $data['Extra']); | ||
$expressions = $this->tableObj->getColumnGenerationExpression($column); | ||
$data['Expression'] = is_array($expressions) ? $expressions[$column] : null; | ||
} | ||
|
||
$changes[] = 'CHANGE ' . Table::generateAlter( | ||
$column, | ||
$column, | ||
mb_strtoupper($extracted_columnspec['type']), | ||
$extracted_columnspec['spec_in_brackets'], | ||
$extracted_columnspec['attribute'], | ||
$data['Collation'] ?? '', | ||
$data['Null'] === 'YES' ? 'YES' : 'NO', | ||
$default_type, | ||
$current_timestamp ? '' : $data['Default'], | ||
isset($data['Extra']) && $data['Extra'] !== '' ? $data['Extra'] | ||
: false, | ||
isset($data['COLUMN_COMMENT']) && $data['COLUMN_COMMENT'] !== '' | ||
? $data['COLUMN_COMMENT'] : false, | ||
$data['Virtuality'], | ||
$data['Expression'], | ||
$i === 0 ? '-first' : $column_names[$i - 1] | ||
); | ||
// update current column_names array, first delete old position | ||
for ($j = 0, $ll = count($column_names); $j < $ll; $j++) { | ||
if ($column_names[$j] != $column) { | ||
continue; | ||
} | ||
|
||
unset($column_names[$j]); | ||
} | ||
|
||
// insert moved column | ||
array_splice($column_names, $i, 0, $column); | ||
} | ||
|
||
if (empty($changes) && ! isset($_REQUEST['preview_sql'])) { // should never happen | ||
if ($sql_query === null) { | ||
$this->response->setRequestStatus(false); | ||
|
||
return; | ||
} | ||
|
||
// query for moving the columns | ||
$sql_query = sprintf( | ||
'ALTER TABLE %s %s', | ||
Util::backquote($this->table), | ||
implode(', ', $changes) | ||
); | ||
|
||
if (isset($_REQUEST['preview_sql'])) { // preview sql | ||
if ($previewSql) { | ||
$this->response->addJSON( | ||
'sql_data', | ||
$this->template->render('preview_sql', ['query_data' => $sql_query]) | ||
|
@@ -178,6 +82,65 @@ public function __invoke(): void | |
__('The columns have been moved successfully.') | ||
); | ||
$this->response->addJSON('message', $message); | ||
$this->response->addJSON('columns', $column_names); | ||
$this->response->addJSON('columns', $moveColumns); | ||
} | ||
|
||
/** | ||
* @param array<int,mixed> $moveColumns | ||
* @psalm-param list<mixed> $moveColumns | ||
*/ | ||
private function generateAlterTableSql(string $createTableSql, array $moveColumns): ?string | ||
{ | ||
$parser = new Parser($createTableSql); | ||
/** @var CreateStatement $statement */ | ||
$statement = $parser->statements[0]; | ||
/** @var CreateDefinition[] $fields */ | ||
$fields = $statement->fields; | ||
$columns = []; | ||
foreach ($fields as $field) { | ||
if ($field->name === null) { | ||
continue; | ||
} | ||
|
||
$columns[$field->name] = $field; | ||
} | ||
|
||
$columnNames = array_keys($columns); | ||
// Ensure the columns from client match the columns from the table | ||
if ( | ||
count($columnNames) !== count($moveColumns) || | ||
array_diff($columnNames, $moveColumns) !== [] | ||
) { | ||
return null; | ||
} | ||
Comment on lines
+109
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an added sanity check that the columns of the request match the columns from the CREATE TABLE statement. |
||
|
||
$changes = []; | ||
|
||
// move columns from first to last | ||
/** @psalm-var list<string> $moveColumns */ | ||
foreach ($moveColumns as $i => $columnName) { | ||
// is this column already correctly placed? | ||
if ($columnNames[$i] == $columnName) { | ||
continue; | ||
} | ||
|
||
$changes[] = | ||
'CHANGE ' . Util::backquote($columnName) . ' ' . CreateDefinition::build($columns[$columnName]) . | ||
($i === 0 ? ' FIRST' : ' AFTER ' . Util::backquote($columnNames[$i - 1])); | ||
|
||
// Move column to its new position | ||
/** @var int $j */ | ||
$j = array_search($columnName, $columnNames, true); | ||
array_splice($columnNames, $j, 1); | ||
array_splice($columnNames, $i, 0, $columnName); | ||
} | ||
|
||
if ($changes === []) { | ||
return null; | ||
} | ||
Comment on lines
+138
to
+140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If no changes detected now doesn't return incomplete statement without any column changes. Calls to this controller were already prevented when pressing ok, but not on preview. |
||
|
||
assert($statement->name !== null, 'Alter table statement has no name'); | ||
|
||
return 'ALTER TABLE ' . Util::backquote($statement->name->table) . "\n " . implode(",\n ", $changes); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't make a request when nothing was changed. The controller now would return an error in this case. Before, an invalid alter table statement was returned without any column changes.