diff --git a/js/src/sql.js b/js/src/sql.js
index 6131adc4c2..7aa461ce3e 100644
--- a/js/src/sql.js
+++ b/js/src/sql.js
@@ -761,7 +761,7 @@ AJAX.registerOnload('sql.js', function () {
for (var i = 0; i < len; i++) {
dialogContent += '' + Messages.strSQLQuery +
'' + response.sql_data[i].sql_query +
- Messages.strMatchedRows +
+ Messages.strAffectedRows +
' ' + response.sql_data[i].matched_rows + '
';
if (i < len - 1) {
diff --git a/libraries/classes/Controllers/JavaScriptMessagesController.php b/libraries/classes/Controllers/JavaScriptMessagesController.php
index d56736bea8..36ca1e9379 100644
--- a/libraries/classes/Controllers/JavaScriptMessagesController.php
+++ b/libraries/classes/Controllers/JavaScriptMessagesController.php
@@ -118,7 +118,6 @@ final class JavaScriptMessagesController
/* For Simulate DML*/
'strSimulateDML' => __('Simulate query'),
- 'strMatchedRows' => __('Matched rows:'),
'strSQLQuery' => __('SQL query:'),
/* Charts */
diff --git a/libraries/classes/Import/SimulateDml.php b/libraries/classes/Import/SimulateDml.php
index 106ed70222..8508429e97 100644
--- a/libraries/classes/Import/SimulateDml.php
+++ b/libraries/classes/Import/SimulateDml.php
@@ -13,7 +13,11 @@ use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
use PhpMyAdmin\SqlParser\Utils\Query;
use PhpMyAdmin\Url;
+use PhpMyAdmin\Util;
+use Webmozart\Assert\Assert;
+use function array_key_exists;
+use function array_reverse;
use function implode;
final class SimulateDml
@@ -71,21 +75,13 @@ final class SimulateDml
* Executes the matched_row_query and returns the resultant row count.
*
* @param string $matchedRowQuery SQL query
- *
- * @return int|string
- * @psalm-return int|numeric-string
*/
- private function executeMatchedRowQuery(string $matchedRowQuery)
+ private function executeMatchedRowQuery(string $matchedRowQuery): int
{
$this->dbi->selectDb($GLOBALS['db']);
- // Execute the query.
- $result = $this->dbi->tryQuery($matchedRowQuery);
- if (! $result) {
- return 0;
- }
- // Count the number of rows in the result set.
- return $result->numRows();
+ // Execute the query.
+ return (int) $this->dbi->fetchValue($matchedRowQuery);
}
/**
@@ -96,22 +92,19 @@ final class SimulateDml
private function getSimulatedDeleteQuery(Parser $parser, DeleteStatement $statement): string
{
$tableReferences = Query::getTables($statement);
- $where = Query::getClause($statement, $parser->list, 'WHERE');
- if (empty($where)) {
- $where = '1';
- }
+ Assert::count($tableReferences, 1, 'No joins allowed in simulation query');
+ Assert::notNull($parser->list, 'Parser list not set');
- $orderAndLimit = '';
- if (! empty($statement->order)) {
- $orderAndLimit .= ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
- }
+ $condition = Query::getClause($statement, $parser->list, 'WHERE');
+ $where = $condition === '' ? '' : ' WHERE ' . $condition;
+ $order = $statement->order === null || $statement->order === []
+ ? ''
+ : ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
+ $limit = $statement->limit === null ? '' : ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
- if (! empty($statement->limit)) {
- $orderAndLimit .= ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
- }
-
- return 'SELECT * FROM ' . implode(', ', $tableReferences) .
- ' WHERE ' . $where . $orderAndLimit;
+ return 'SELECT COUNT(*) FROM (' .
+ 'SELECT 1 FROM ' . $tableReferences[0] . $where . $order . $limit .
+ ') AS `pma_tmp`';
}
/**
@@ -122,33 +115,38 @@ final class SimulateDml
private function getSimulatedUpdateQuery(Parser $parser, UpdateStatement $statement): string
{
$tableReferences = Query::getTables($statement);
- $where = Query::getClause($statement, $parser->list, 'WHERE');
- if (empty($where)) {
- $where = '1';
+ Assert::count($tableReferences, 1, 'No joins allowed in simulation query');
+ Assert::isNonEmptyList($statement->set, 'SET statements missing');
+ Assert::notNull($parser->list, 'Parser list not set');
+ $newValues = [];
+ $oldValues = [];
+ $newColumns = [];
+ $oldColumns = [];
+ $i = 0;
+ $handledColumns = [];
+ foreach (array_reverse($statement->set) as $set) {
+ if (array_key_exists($set->column, $handledColumns)) {
+ continue;
+ }
+
+ $handledColumns[$set->column] = true;
+ $oldValues[] = $set->column . ' AS ' . ($oldColumns[] = Util::backquote('o' . $i));
+ $newValues[] = $set->value . ' AS ' . ($newColumns[] = Util::backquote('n' . $i));
+ ++$i;
}
- $columns = [];
- $diff = [];
- foreach ($statement->set as $set) {
- $columns[] = $set->column;
- $diff[] = 'NOT ' . $set->column . ' <=> (' . $set->value . ')';
- }
+ $condition = Query::getClause($statement, $parser->list, 'WHERE');
+ $where = $condition === '' ? '' : ' WHERE ' . $condition;
+ $order = $statement->order === null || $statement->order === []
+ ? ''
+ : ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
+ $limit = $statement->limit === null ? '' : ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
- if (! empty($diff)) {
- $where = '(' . $where . ') AND (' . implode(' OR ', $diff) . ')';
- }
-
- $orderAndLimit = '';
- if (! empty($statement->order)) {
- $orderAndLimit .= ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
- }
-
- if (! empty($statement->limit)) {
- $orderAndLimit .= ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
- }
-
- return 'SELECT ' . implode(', ', $columns) .
- ' FROM ' . implode(', ', $tableReferences) .
- ' WHERE ' . $where . $orderAndLimit;
+ return 'SELECT COUNT(*)' .
+ ' FROM (SELECT ' . implode(',', $newValues) . ') AS `pma_new`' .
+ ' JOIN (' .
+ 'SELECT ' . implode(', ', $oldValues) . ' FROM ' . $tableReferences[0] . $where . $order . $limit .
+ ') AS `pma_old`' .
+ ' WHERE NOT (' . implode(', ', $newColumns) . ') <=> (' . implode(', ', $oldColumns) . ')';
}
}
diff --git a/libraries/classes/Plugins/Export/ExportJson.php b/libraries/classes/Plugins/Export/ExportJson.php
index a2fe8ed752..401430939b 100644
--- a/libraries/classes/Plugins/Export/ExportJson.php
+++ b/libraries/classes/Plugins/Export/ExportJson.php
@@ -224,15 +224,15 @@ class ExportJson extends ExportPlugin
/**
* Export to JSON
*
- * @phpstan-param array{
- * string: array{
- * 'tables': array{
- * string: array{
- * 'columns': array{string: string}
- * }
- * }
- * }
- * }|array|null $aliases
+ * @phpstan-param array<
+ * string,
+ * array{
+ * tables: array<
+ * string,
+ * array{columns: array}
+ * >
+ * }
+ * >|null $aliases
*/
protected function doExportForQuery(
DatabaseInterface $dbi,
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index 4b3a6b320a..d50ecf14ab 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -4535,16 +4535,6 @@ parameters:
count: 1
path: libraries/classes/Import/Ajax.php
- -
- message: "#^Argument of an invalid type array\\\\|null supplied for foreach, only iterables are supported\\.$#"
- count: 1
- path: libraries/classes/Import/SimulateDml.php
-
- -
- message: "#^Parameter \\#2 \\$list of static method PhpMyAdmin\\\\SqlParser\\\\Utils\\\\Query\\:\\:getClause\\(\\) expects PhpMyAdmin\\\\SqlParser\\\\TokensList, PhpMyAdmin\\\\SqlParser\\\\TokensList\\|null given\\.$#"
- count: 6
- path: libraries/classes/Import/SimulateDml.php
-
-
message: "#^Method PhpMyAdmin\\\\Index\\:\\:__construct\\(\\) has parameter \\$params with no value type specified in iterable type array\\.$#"
count: 1
@@ -5830,11 +5820,6 @@ parameters:
count: 1
path: libraries/classes/Plugins/Export/ExportHtmlword.php
- -
- message: "#^Method PhpMyAdmin\\\\Plugins\\\\Export\\\\ExportJson\\:\\:doExportForQuery\\(\\) has parameter \\$aliases with no value type specified in iterable type array\\.$#"
- count: 1
- path: libraries/classes/Plugins/Export/ExportJson.php
-
-
message: "#^Method PhpMyAdmin\\\\Plugins\\\\Export\\\\ExportJson\\:\\:exportData\\(\\) has parameter \\$aliases with no value type specified in iterable type array\\.$#"
count: 1
@@ -10915,26 +10900,6 @@ parameters:
count: 1
path: test/classes/Stubs/DbiDummy.php
- -
- message: "#^PHPDoc tag @var for property PhpMyAdmin\\\\Tests\\\\Stubs\\\\DbiDummy\\:\\:\\$dummyQueries contains unresolvable type\\.$#"
- count: 1
- path: test/classes/Stubs/DbiDummy.php
-
- -
- message: "#^PHPDoc tag @var for property PhpMyAdmin\\\\Tests\\\\Stubs\\\\DbiDummy\\:\\:\\$filoQueries contains unresolvable type\\.$#"
- count: 1
- path: test/classes/Stubs/DbiDummy.php
-
- -
- message: "#^Property PhpMyAdmin\\\\Tests\\\\Stubs\\\\DbiDummy\\:\\:\\$dummyQueries type has no value type specified in iterable type array\\\\.$#"
- count: 1
- path: test/classes/Stubs/DbiDummy.php
-
- -
- message: "#^Property PhpMyAdmin\\\\Tests\\\\Stubs\\\\DbiDummy\\:\\:\\$filoQueries type has no value type specified in iterable type array\\\\.$#"
- count: 1
- path: test/classes/Stubs/DbiDummy.php
-
-
message: "#^Method PhpMyAdmin\\\\Tests\\\\Stubs\\\\ResponseRenderer\\:\\:getJSONResult\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index ce5b2ef117..859ed96039 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -7943,39 +7943,6 @@
$_SESSION[$GLOBALS['SESSION_KEY']]
-
-
- $set->value
-
-
- $columns
-
-
- $columns[]
- $set
-
-
- $set->column
-
-
- $set->column
- $set->value
-
-
- $parser->list
- $parser->list
- $parser->list
- $parser->list
- $parser->list
- $parser->list
-
-
- $statement->set
-
-
- empty($diff)
-
-
$this->packed === null
@@ -9706,12 +9673,9 @@
-
- $col_as
-
-
- $col_as
-
+
+ $aliases
+
$crlf
$crlf
@@ -16413,71 +16377,36 @@
-
- private $dummyQueries = [];
- private $filoQueries = [];
-
-
- $dummyQueries
- $filoQueries
-
-
+
$query_data['columns'] ?? []
$query_data['result']
$query_data['result']
- $this->dummyQueries
- $this->filoQueries
-
- $query['used']
+
$query_data['columns'][$key]
$query_data['result'][$query_data['pos']]
- $this->dummyQueries[$i]
- $this->dummyQueries[$i]
- $this->dummyQueries[$i]['result']
- $this->dummyQueries[$result - self::OFFSET_GLOBAL]
- $this->filoQueries[$i]
- $this->filoQueries[$i]
- $this->filoQueries[$i]
- $this->filoQueries[$i]['result']
- $this->filoQueries[$result]
-
- $this->dummyQueries[$i]
- $this->filoQueries[$i]
- $this->filoQueries[$i]
- $this->filoQueries[]
-
$query_data['result'][$query_data['pos']]
$ret[$query_data['columns'][$key]]
-
- $query
+
$query_data['pos']
$ret
$ret[$query_data['columns'][$key]]
- $unUsed[]
$val
-
+
?array
- array
int|numeric-string
$query_data['pos']
-
+
$cached_affected_rows ?? 0
$ret
- $this->dummyQueries[$result - self::OFFSET_GLOBAL]
- $this->filoQueries[$result]
-
- $unUsed
- array[]
-
diff --git a/test/classes/Import/SimulateDmlTest.php b/test/classes/Import/SimulateDmlTest.php
index ec76dd73d1..5e23bacab4 100644
--- a/test/classes/Import/SimulateDmlTest.php
+++ b/test/classes/Import/SimulateDmlTest.php
@@ -19,12 +19,13 @@ class SimulateDmlTest extends AbstractTestCase
/**
* @dataProvider providerForTestGetMatchedRows
*/
- public function testGetMatchedRows(string $sqlQuery, string $simulatedQuery): void
+ public function testGetMatchedRows(string $sqlQuery, string $simulatedQuery, int $expectedMatches): void
{
$GLOBALS['db'] = 'PMA';
$object = new SimulateDml($this->dbi);
$parser = new Parser($sqlQuery);
$this->dummyDbi->addSelectDb('PMA');
+ $this->dummyDbi->addResult($simulatedQuery, [[$expectedMatches]], ['COUNT(*)']);
$simulatedData = $object->getMatchedRows($sqlQuery, $parser, $parser->statements[0]);
@@ -35,26 +36,74 @@ class SimulateDmlTest extends AbstractTestCase
]);
$this->assertAllSelectsConsumed();
+ $this->assertAllQueriesConsumed();
$this->assertEquals([
'sql_query' => Generator::formatSql($sqlQuery),
- 'matched_rows' => 2,
+ 'matched_rows' => $expectedMatches,
'matched_rows_url' => $matchedRowsUrl,
], $simulatedData);
}
/**
- * @return string[][]
+ * @return array
*/
public function providerForTestGetMatchedRows(): array
{
return [
'update statement' => [
'UPDATE `table_1` SET `id` = 20 WHERE `id` > 10',
- 'SELECT `id` FROM `table_1` WHERE (`id` > 10) AND (NOT `id` <=> (20))',
+ 'SELECT COUNT(*)' .
+ ' FROM (SELECT 20 AS `n0`) AS `pma_new`' .
+ ' JOIN (SELECT `id` AS `o0` FROM `table_1` WHERE `id` > 10) AS `pma_old`' .
+ ' WHERE NOT (`n0`) <=> (`o0`)',
+ 2,
+ ],
+ 'update statement_false_condition' => [
+ 'UPDATE `table_1` SET `id` = 20 WHERE 0',
+ 'SELECT COUNT(*)' .
+ ' FROM (SELECT 20 AS `n0`) AS `pma_new`' .
+ ' JOIN (SELECT `id` AS `o0` FROM `table_1` WHERE 0) AS `pma_old`' .
+ ' WHERE NOT (`n0`) <=> (`o0`)',
+ 0,
+ ],
+ 'update statement_no_condition' => [
+ 'UPDATE `table_1` SET `id` = 20',
+ 'SELECT COUNT(*)' .
+ ' FROM (SELECT 20 AS `n0`) AS `pma_new`' .
+ ' JOIN (SELECT `id` AS `o0` FROM `table_1`) AS `pma_old`' .
+ ' WHERE NOT (`n0`) <=> (`o0`)',
+ 7,
+ ],
+ 'update order by limit' => [
+ 'UPDATE `table_1` SET `id` = 20 ORDER BY `id` ASC LIMIT 3',
+ 'SELECT COUNT(*)' .
+ ' FROM (SELECT 20 AS `n0`) AS `pma_new`' .
+ ' JOIN (SELECT `id` AS `o0` FROM `table_1` ORDER BY `id` ASC LIMIT 3) AS `pma_old`' .
+ ' WHERE NOT (`n0`) <=> (`o0`)',
+ 3,
+ ],
+ 'update duplicate set' => [
+ 'UPDATE `table_1` SET `id` = 2, `id` = 1 WHERE `id` = 1',
+ 'SELECT COUNT(*)' .
+ ' FROM (SELECT 1 AS `n0`) AS `pma_new`' .
+ ' JOIN (SELECT `id` AS `o0` FROM `table_1` WHERE `id` = 1) AS `pma_old`' .
+ ' WHERE NOT (`n0`) <=> (`o0`)',
+ 0,
],
'delete statement' => [
'DELETE FROM `table_1` WHERE `id` > 10',
- 'SELECT * FROM `table_1` WHERE `id` > 10',
+ 'SELECT COUNT(*) FROM (SELECT 1 FROM `table_1` WHERE `id` > 10) AS `pma_tmp`',
+ 2,
+ ],
+ 'delete statement_false_condition' => [
+ 'DELETE FROM `table_1` WHERE 0',
+ 'SELECT COUNT(*) FROM (SELECT 1 FROM `table_1` WHERE 0) AS `pma_tmp`',
+ 0,
+ ],
+ 'delete statement order by limit' => [
+ 'DELETE FROM `table_1` ORDER BY `id` ASC LIMIT 3',
+ 'SELECT COUNT(*) FROM (SELECT 1 FROM `table_1` ORDER BY `id` ASC LIMIT 3) AS `pma_tmp`',
+ 2,
],
];
}
diff --git a/test/classes/Stubs/DbiDummy.php b/test/classes/Stubs/DbiDummy.php
index d267b0fc33..83201d8b90 100644
--- a/test/classes/Stubs/DbiDummy.php
+++ b/test/classes/Stubs/DbiDummy.php
@@ -50,9 +50,9 @@ class DbiDummy implements DbiExtension
* @var array
* @phpstan-var array{
* 'query': string,
- * 'result': ((int[]|string[]|array{string: string})[])|bool|bool[]|empty-array,
+ * 'result': list>|array{true}|bool,
* 'columns'?: string[],
- * 'metadata'?: object[]|empty-array,
+ * 'metadata'?: object[],
* 'used'?: bool,
* 'pos'?: int
* }[]
@@ -72,9 +72,9 @@ class DbiDummy implements DbiExtension
* @var array
* @phpstan-var array{
* 'query': string,
- * 'result': ((int[]|string[]|array{string: string})[])|bool|bool[]|empty-array,
+ * 'result': list>|bool,
* 'columns'?: string[],
- * 'metadata'?: object[]|empty-array,
+ * 'metadata'?: object[],
* 'pos'?: int
* }[]
*/
@@ -493,7 +493,7 @@ class DbiDummy implements DbiExtension
* @param array|bool $result Expected result
* @param string[] $columns The result columns
* @param object[] $metadata The result metadata
- * @phpstan-param array|bool>|bool $result
+ * @phpstan-param list>|array{true}|bool $result
*/
public function addResult(string $query, $result, array $columns = [], array $metadata = []): void
{
@@ -2228,16 +2228,6 @@ class DbiDummy implements DbiExtension
'columns' => ['Name', 'Engine'],
'result' => [['PMA_table', 'InnoDB']],
],
- [
- 'query' => 'SELECT `id` FROM `table_1` WHERE (`id` > 10) AND (NOT `id` <=> (20))',
- 'columns' => ['id'],
- 'result' => [['11'], ['12']],
- ],
- [
- 'query' => 'SELECT * FROM `table_1` WHERE `id` > 10',
- 'columns' => ['column'],
- 'result' => [['row1'], ['row2']],
- ],
[
'query' => 'SELECT * FROM `PMA`.`table_1` LIMIT 1',
'columns' => ['column'],