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'],