Merge branch 'QA_5_2'

Signed-off-by: Maurício Meneghini Fauth <mauricio@mfauth.net>
This commit is contained in:
Maurício Meneghini Fauth 2026-04-22 20:41:40 -03:00
commit 171a972ce6
No known key found for this signature in database
GPG Key ID: 6A16FD38AFC89CC8
9 changed files with 155 additions and 120 deletions

View File

@ -462,6 +462,8 @@ AJAX.registerTeardown('sql.js', function () {
$(document).off('click', '#sqlquery');
$(document).off('click', 'input.sqlbutton');
$('#fieldsSelect').off('dblclick');
$('#simulateDmlModal').modal('hide');
});
/**

View File

@ -32,7 +32,6 @@
<ul class="list-group list-group-flush">
{% if has_server_selection %}
<li id="li_select_server" class="list-group-item">
{{ get_image('s_host') }}
{{ server_selection|raw }}
</li>
{% endif %}

View File

@ -1,7 +1,13 @@
{% if not_only_options %}
<form class="disableAjax" method="post" action="{{ form_action|raw }}">
{{ get_hidden_fields([]) }}
<label for="select_server" class="form-label">{{ t('Current server:') }}</label>
<label for="select_server" class="form-label">
{% if has_label_icon %}
{{ get_icon('s_host', t('Current server:')) }}
{% else %}
{{ t('Current server:') }}
{% endif %}
</label>
<select id="select_server" class="form-select autosubmit" name="server">
<option value="">({{ t('Servers') }}) ...</option>
{% for server in servers.select %}

View File

@ -114,7 +114,7 @@ final class HomeController implements InvocableController
)
);
if ($hasServerSelection) {
$serverSelection = Select::render(true);
$serverSelection = Select::render(true, true);
}
if (Current::$server > 0) {

View File

@ -50,11 +50,12 @@ final class SimulateDml
}
// Execute the query and get the number of matched rows.
$matchedRows = $this->executeMatchedRowQuery($matchedRowsQuery);
$matchedRows = $this->executeMatchedRowQuery($matchedRowsQuery['count']);
$selectQuery = $matchedRowsQuery['select'];
$matchedRowsUrl = Url::getFromRoute('/sql', [
'db' => Current::$database,
'sql_query' => $matchedRowsQuery,
'sql_signature' => Core::signSqlQuery($matchedRowsQuery),
'sql_query' => $selectQuery,
'sql_signature' => Core::signSqlQuery($selectQuery),
]);
return [
@ -72,20 +73,20 @@ final class SimulateDml
private function executeMatchedRowQuery(string $matchedRowQuery): int
{
$this->dbi->selectDb(Current::$database);
$result = $this->dbi->tryQuery($matchedRowQuery);
if ($result === false) {
$count = $this->dbi->fetchValue($matchedRowQuery);
if ($count === false) {
return 0;
}
return (int) $result->numRows();
return (int) $count;
}
/**
* Transforms a DELETE query into SELECT statement.
*
* @return string SQL query
* @psalm-return array{select: string, count: string} SQL queries
*/
private function getSimulatedDeleteQuery(Parser $parser, DeleteStatement $statement): string
private function getSimulatedDeleteQuery(Parser $parser, DeleteStatement $statement): array
{
$tableReferences = Query::getTables($statement);
Assert::count($tableReferences, 1, 'No joins allowed in simulation query');
@ -98,15 +99,22 @@ final class SimulateDml
: ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
$limit = $statement->limit === null ? '' : ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
return 'SELECT * FROM ' . $tableReferences[0] . $where . $order . $limit;
return [
'select' => 'SELECT * FROM (' .
'SELECT * FROM ' . $tableReferences[0] . $where . $order . $limit .
') AS pma_tmp',
'count' => 'SELECT COUNT(*) FROM (' .
'SELECT 1 FROM ' . $tableReferences[0] . $where . $order . $limit .
') AS pma_tmp',
];
}
/**
* Transforms a UPDATE query into SELECT statement.
*
* @return string SQL query
* @psalm-return array{select: string, count: string} SQL queries
*/
private function getSimulatedUpdateQuery(Parser $parser, UpdateStatement $statement): string
private function getSimulatedUpdateQuery(Parser $parser, UpdateStatement $statement): array
{
$tableReferences = Query::getTables($statement);
Assert::count($tableReferences, 1, 'No joins allowed in simulation query');
@ -123,7 +131,7 @@ final class SimulateDml
}
$oldColumns[] = Util::backquote($column);
$values[$column] = $set->value . ' AS ' . ($newColumns[] = Util::backquote($column . ' `new`'));
$values[$column] = $set->value . ' AS ' . ($newColumns[] = Util::backquote($column . ' *new*'));
}
$condition = Query::getClause($statement, $parser->list, 'WHERE');
@ -133,10 +141,18 @@ final class SimulateDml
: ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
$limit = $statement->limit === null ? '' : ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
return 'SELECT *' .
' FROM (' .
'SELECT *, ' . implode(', ', $values) . ' FROM ' . $tableReferences[0] . $where . $order . $limit .
') AS `pma_tmp`' .
' WHERE NOT (' . implode(', ', $oldColumns) . ') <=> (' . implode(', ', $newColumns) . ')';
return [
'select' => 'SELECT *' .
' FROM (' .
'SELECT *, ' . implode(', ', $values) . ' FROM ' . $tableReferences[0] . $where . $order . $limit .
') AS `pma_tmp`' .
' WHERE NOT (' . implode(', ', $oldColumns) . ') <=> (' . implode(', ', $newColumns) . ')',
'count' => 'SELECT COUNT(*)' .
' FROM (' .
'SELECT ' . implode(', ', $oldColumns) . ', ' . implode(', ', $values) .
' FROM ' . $tableReferences[0] . $where . $order . $limit .
') AS `pma_tmp`' .
' WHERE NOT (' . implode(', ', $oldColumns) . ') <=> (' . implode(', ', $newColumns) . ')',
];
}
}

View File

@ -24,7 +24,7 @@ class Select
*
* @param bool $notOnlyOptions whether to include form tags or not
*/
public static function render(bool $notOnlyOptions): string
public static function render(bool $notOnlyOptions, bool $hasLabelIcon = false): string
{
$config = Config::getInstance();
// Show as list?
@ -93,6 +93,7 @@ class Select
'not_only_options' => $notOnlyOptions,
'servers' => $servers,
'form_action' => $formAction,
'has_label_icon' => $hasLabelIcon,
]);
}
}

View File

@ -708,8 +708,17 @@ class Sql
// Removes LIMIT clause that might have been added
$statement->limit = null;
if ($changeExpression) {
$statement->expr[0] = new Expression();
// Replace SELECT expressions with 1 to avoid
// "Duplicate column name" errors when aliases shadow
// existing column names (e.g. SELECT id, name AS id).
// Only safe when there's no GROUP BY, DISTINCT, or UNION
// since those depend on the actual expressions.
if (
! $statementInfo->flags->isGroup
&& ! $statementInfo->flags->distinct
&& ! $statementInfo->flags->union
) {
$statement->expr = [new Expression()];
$statement->expr[0]->expr = '1';
}

View File

@ -30,9 +30,9 @@ final class SimulateDmlControllerTest extends AbstractTestCase
/**
* @psalm-param list<
* array{
* simulated: string,
* columns: list<non-empty-string>,
* result: list<non-empty-list<string|int|null>>,
* count: string,
* select: string,
* affected_rows: int,
* }
* > $expectedPerQuery
*/
@ -44,7 +44,7 @@ final class SimulateDmlControllerTest extends AbstractTestCase
$dummyDbi = $this->createDbiDummy();
foreach ($expectedPerQuery as $expected) {
$dummyDbi->addSelectDb('PMA');
$dummyDbi->addResult($expected['simulated'], $expected['result'], $expected['columns']);
$dummyDbi->addResult($expected['count'], [[$expected['affected_rows']]]);
}
$dbi = $this->createDatabaseInterface($dummyDbi);
@ -69,13 +69,14 @@ final class SimulateDmlControllerTest extends AbstractTestCase
foreach ($expectedPerQuery as $idx => $expectedData) {
/** @var DeleteStatement|UpdateStatement $statement */
$statement = $parser->statements[$idx];
$selectQuery = $expectedData['select'];
$expected = [
'sql_query' => Generator::formatSql($statement->build()),
'matched_rows' => count($expectedData['result']),
'matched_rows' => $expectedData['affected_rows'],
'matched_rows_url' => Url::getFromRoute('/sql', [
'db' => 'PMA',
'sql_query' => $expectedData['simulated'],
'sql_signature' => Core::signSqlQuery($expectedData['simulated']),
'sql_query' => $selectQuery,
'sql_signature' => Core::signSqlQuery($selectQuery),
]),
];
@ -88,9 +89,9 @@ final class SimulateDmlControllerTest extends AbstractTestCase
* array{
* string,
* list<array{
* simulated: string,
* columns: list<non-empty-string>,
* result: list<non-empty-list<string|int|null>>,
* count: string,
* select: string,
* affected_rows: int,
* }>
* }
* >
@ -110,13 +111,17 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE t SET `b` = NULL, a = a ORDER BY id DESC LIMIT 3',
[
[
'simulated' =>
'SELECT * FROM (' .
'SELECT *, a AS `a ``new```, NULL AS `b ``new``` FROM `t` ORDER BY id DESC LIMIT 3' .
'count' =>
'SELECT COUNT(*) FROM (' .
'SELECT `a`, `b`, a AS `a *new*`, NULL AS `b *new*` FROM `t` ORDER BY id DESC LIMIT 3' .
') AS `pma_tmp`' .
' WHERE NOT (`a`, `b`) <=> (`a ``new```, `b ``new```)',
'columns' => ['id', 'a', 'b', 'a `new`', 'b `new`'],
'result' => [[5, 2, 'test', 2, null]],
' WHERE NOT (`a`, `b`) <=> (`a *new*`, `b *new*`)',
'select' =>
'SELECT * FROM (' .
'SELECT *, a AS `a *new*`, NULL AS `b *new*` FROM `t` ORDER BY id DESC LIMIT 3' .
') AS `pma_tmp`' .
' WHERE NOT (`a`, `b`) <=> (`a *new*`, `b *new*`)',
'affected_rows' => 1,
],
],
],
@ -124,15 +129,15 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE `t` SET `a` = 20 WHERE `id` > 4',
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `a`, 20 AS `a *new*` FROM `t` WHERE `id` > 4) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, 20 AS `a ``new``` FROM `t` WHERE `id` > 4) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a ``new```)',
'columns' => ['id', 'a', 'b', 'a `new`'],
'result' => [
[5, 2, 'test', 20],
[6, 2, null, 20],
],
' FROM (SELECT *, 20 AS `a *new*` FROM `t` WHERE `id` > 4) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'affected_rows' => 2,
],
],
],
@ -140,12 +145,15 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE `t` SET `a` = 20 WHERE 0',
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `a`, 20 AS `a *new*` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, 20 AS `a ``new``` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a ``new```)',
'columns' => ['id', 'a', 'b', 'a `new`'],
'result' => [],
' FROM (SELECT *, 20 AS `a *new*` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'affected_rows' => 0,
],
],
],
@ -153,16 +161,15 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE `t` SET `a` = 2',
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `a`, 2 AS `a *new*` FROM `t`) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, 2 AS `a ``new``` FROM `t`) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a ``new```)',
'columns' => ['id', 'a', 'b', 'a `new`'],
'result' => [
[2, 1, null, 2],
[3, 1, null, 2],
[4, 1, null, 2],
],
' FROM (SELECT *, 2 AS `a *new*` FROM `t`) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'affected_rows' => 3,
],
],
],
@ -170,16 +177,15 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE `t` SET `id` = 20 ORDER BY `id` ASC LIMIT 3',
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `id`, 20 AS `id *new*` FROM `t` ORDER BY `id` ASC LIMIT 3) AS `pma_tmp`' .
' WHERE NOT (`id`) <=> (`id *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, 20 AS `id ``new``` FROM `t` ORDER BY `id` ASC LIMIT 3) AS `pma_tmp`' .
' WHERE NOT (`id`) <=> (`id ``new```)',
'columns' => ['id', 'a', 'b', 'id `new`'],
'result' => [
[1, 2, 'test', 20],
[2, 1, null, 20],
[3, 1, null, 20],
],
' FROM (SELECT *, 20 AS `id *new*` FROM `t` ORDER BY `id` ASC LIMIT 3) AS `pma_tmp`' .
' WHERE NOT (`id`) <=> (`id *new*`)',
'affected_rows' => 3,
],
],
],
@ -187,12 +193,15 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE `t` SET `id` = 2, `id` = 1 WHERE `id` = 1',
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `id`, 1 AS `id *new*` FROM `t` WHERE `id` = 1) AS `pma_tmp`' .
' WHERE NOT (`id`) <=> (`id *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, 1 AS `id ``new``` FROM `t` WHERE `id` = 1) AS `pma_tmp`' .
' WHERE NOT (`id`) <=> (`id ``new```)',
'columns' => ['id', 'a', 'b', 'id `new`'],
'result' => [],
' FROM (SELECT *, 1 AS `id *new*` FROM `t` WHERE `id` = 1) AS `pma_tmp`' .
' WHERE NOT (`id`) <=> (`id *new*`)',
'affected_rows' => 0,
],
],
],
@ -200,12 +209,9 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'DELETE FROM `t` WHERE `id` > 4',
[
[
'simulated' => 'SELECT * FROM `t` WHERE `id` > 4',
'columns' => ['id', 'a', 'b'],
'result' => [
[5, 2, 'test'],
[6, 2, null],
],
'count' => 'SELECT COUNT(*) FROM (SELECT 1 FROM `t` WHERE `id` > 4) AS pma_tmp',
'select' => 'SELECT * FROM (SELECT * FROM `t` WHERE `id` > 4) AS pma_tmp',
'affected_rows' => 2,
],
],
],
@ -213,9 +219,9 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'DELETE FROM `t` WHERE 0',
[
[
'simulated' => 'SELECT * FROM `t` WHERE 0',
'columns' => ['id', 'a', 'b'],
'result' => [],
'count' => 'SELECT COUNT(*) FROM (SELECT 1 FROM `t` WHERE 0) AS pma_tmp',
'select' => 'SELECT * FROM (SELECT * FROM `t` WHERE 0) AS pma_tmp',
'affected_rows' => 0,
],
],
],
@ -223,13 +229,9 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'DELETE FROM `t` ORDER BY `id` ASC LIMIT 3',
[
[
'simulated' => 'SELECT * FROM `t` ORDER BY `id` ASC LIMIT 3',
'columns' => ['id', 'a', 'b'],
'result' => [
[1, 2, 'test'],
[2, 1, null],
[3, 1, null],
],
'count' => 'SELECT COUNT(*) FROM (SELECT 1 FROM `t` ORDER BY `id` ASC LIMIT 3) AS pma_tmp',
'select' => 'SELECT * FROM (SELECT * FROM `t` ORDER BY `id` ASC LIMIT 3) AS pma_tmp',
'affected_rows' => 3,
],
],
],
@ -237,31 +239,20 @@ final class SimulateDmlControllerTest extends AbstractTestCase
'UPDATE `t` SET `b` = `a`; DELETE FROM `t` WHERE 1',
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `b`, `a` AS `b *new*` FROM `t`) AS `pma_tmp`' .
' WHERE NOT (`b`) <=> (`b *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, `a` AS `b ``new``` FROM `t`) AS `pma_tmp`' .
' WHERE NOT (`b`) <=> (`b ``new```)',
'columns' => ['id', 'a', 'b', 'b `new`'],
'result' => [
[1, 2, 2, 'test'],
[2, 1, 1, null],
[3, 1, 1, null],
[4, 1, 1, null],
[5, 2, 2, 'test'],
[6, 2, 2, null],
],
' FROM (SELECT *, `a` AS `b *new*` FROM `t`) AS `pma_tmp`' .
' WHERE NOT (`b`) <=> (`b *new*`)',
'affected_rows' => 6,
],
[
'simulated' => 'SELECT * FROM `t` WHERE 1',
'columns' => ['id', 'a', 'b'],
'result' => [
[1, 2, 'test'],
[2, 1, null],
[3, 1, null],
[4, 1, null],
[5, 2, 'test'],
[6, 2, null],
],
'count' => 'SELECT COUNT(*) FROM (SELECT 1 FROM `t` WHERE 1) AS pma_tmp',
'select' => 'SELECT * FROM (SELECT * FROM `t` WHERE 1) AS pma_tmp',
'affected_rows' => 6,
],
],
],
@ -269,12 +260,15 @@ final class SimulateDmlControllerTest extends AbstractTestCase
"UPDATE `t` SET `a` = 20 -- oops\nWHERE 0",
[
[
'simulated' =>
'count' =>
'SELECT COUNT(*)' .
' FROM (SELECT `a`, 20 AS `a *new*` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'select' =>
'SELECT *' .
' FROM (SELECT *, 20 AS `a ``new``` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a ``new```)',
'columns' => ['id', 'a', 'b', 'a `new`'],
'result' => [],
' FROM (SELECT *, 20 AS `a *new*` FROM `t` WHERE 0) AS `pma_tmp`' .
' WHERE NOT (`a`) <=> (`a *new*`)',
'affected_rows' => 0,
],
],
],

View File

@ -375,14 +375,14 @@ class SqlTest extends AbstractTestCase
['max_rows' => 10, 'pos' => 4],
20,
42,
false,
'SELECT COUNT(*) FROM (SELECT 1 FROM company_users WHERE subquery_case = 0 ) as cnt',
],
[
'SELECT ( as c2 FROM company_users WHERE working_count = 0',// Invalid query
['max_rows' => 10],
20,
20,
],
[
'SELECT DISTINCT country_id FROM city;',
@ -408,6 +408,14 @@ class SqlTest extends AbstractTestCase
false,
'SELECT COUNT(*) FROM (SELECT 1 FROM t1 WHERE id <> 0 ) as cnt',
],
'duplicate column alias should not break count' => [
'SELECT id, name AS id FROM company_users WHERE working_count = 0',
['max_rows' => 10, 'pos' => 0],
25,
384,
false,
'SELECT COUNT(*) FROM (SELECT 1 FROM company_users WHERE working_count = 0 ) as cnt',
],
];
}