Merge pull request #18583 from MoonE/fix-simulate-query

Fix simulate query count
This commit is contained in:
Maurício Meneghini Fauth 2023-08-26 16:53:32 -03:00 committed by GitHub
commit 69cc0a2ff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 125 additions and 195 deletions

View File

@ -761,7 +761,7 @@ AJAX.registerOnload('sql.js', function () {
for (var i = 0; i < len; i++) {
dialogContent += '<strong>' + Messages.strSQLQuery +
'</strong>' + response.sql_data[i].sql_query +
Messages.strMatchedRows +
Messages.strAffectedRows +
' <a href="' + response.sql_data[i].matched_rows_url +
'">' + response.sql_data[i].matched_rows + '</a><br>';
if (i < len - 1) {

View File

@ -118,7 +118,6 @@ final class JavaScriptMessagesController
/* For Simulate DML*/
'strSimulateDML' => __('Simulate query'),
'strMatchedRows' => __('Matched rows:'),
'strSQLQuery' => __('SQL query:'),
/* Charts */

View File

@ -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) . ')';
}
}

View File

@ -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<string, string>}
* >
* }
* >|null $aliases
*/
protected function doExportForQuery(
DatabaseInterface $dbi,

View File

@ -4535,16 +4535,6 @@ parameters:
count: 1
path: libraries/classes/Import/Ajax.php
-
message: "#^Argument of an invalid type array\\<PhpMyAdmin\\\\SqlParser\\\\Components\\\\SetOperation\\>\\|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\\<string, mixed\\>\\.$#"
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\\<string, mixed\\>\\.$#"
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

View File

@ -7943,39 +7943,6 @@
<code>$_SESSION[$GLOBALS['SESSION_KEY']]</code>
</PossiblyInvalidArrayOffset>
</file>
<file src="libraries/classes/Import/SimulateDml.php">
<MixedArgument occurrences="1">
<code>$set-&gt;value</code>
</MixedArgument>
<MixedArgumentTypeCoercion occurrences="1">
<code>$columns</code>
</MixedArgumentTypeCoercion>
<MixedAssignment occurrences="2">
<code>$columns[]</code>
<code>$set</code>
</MixedAssignment>
<MixedOperand occurrences="1">
<code>$set-&gt;column</code>
</MixedOperand>
<MixedPropertyFetch occurrences="2">
<code>$set-&gt;column</code>
<code>$set-&gt;value</code>
</MixedPropertyFetch>
<PossiblyNullArgument occurrences="6">
<code>$parser-&gt;list</code>
<code>$parser-&gt;list</code>
<code>$parser-&gt;list</code>
<code>$parser-&gt;list</code>
<code>$parser-&gt;list</code>
<code>$parser-&gt;list</code>
</PossiblyNullArgument>
<PossiblyNullIterator occurrences="1">
<code>$statement-&gt;set</code>
</PossiblyNullIterator>
<RedundantCondition occurrences="1">
<code>empty($diff)</code>
</RedundantCondition>
</file>
<file src="libraries/classes/Index.php">
<DocblockTypeContradiction occurrences="1">
<code>$this-&gt;packed === null</code>
@ -9706,12 +9673,9 @@
</PossiblyUndefinedVariable>
</file>
<file src="libraries/classes/Plugins/Export/ExportJson.php">
<MixedArgument occurrences="1">
<code>$col_as</code>
</MixedArgument>
<MixedAssignment occurrences="1">
<code>$col_as</code>
</MixedAssignment>
<MixedArgumentTypeCoercion occurrences="1">
<code>$aliases</code>
</MixedArgumentTypeCoercion>
<MixedOperand occurrences="4">
<code>$crlf</code>
<code>$crlf</code>
@ -16413,71 +16377,36 @@
</PossiblyUndefinedMethod>
</file>
<file src="test/classes/Stubs/DbiDummy.php">
<InvalidDocblock occurrences="2">
<code>private $dummyQueries = [];</code>
<code>private $filoQueries = [];</code>
</InvalidDocblock>
<MissingPropertyType occurrences="2">
<code>$dummyQueries</code>
<code>$filoQueries</code>
</MissingPropertyType>
<MixedArgument occurrences="5">
<MixedArgument occurrences="3">
<code>$query_data['columns'] ?? []</code>
<code>$query_data['result']</code>
<code>$query_data['result']</code>
<code>$this-&gt;dummyQueries</code>
<code>$this-&gt;filoQueries</code>
</MixedArgument>
<MixedArrayAccess occurrences="12">
<code>$query['used']</code>
<MixedArrayAccess occurrences="2">
<code>$query_data['columns'][$key]</code>
<code>$query_data['result'][$query_data['pos']]</code>
<code>$this-&gt;dummyQueries[$i]</code>
<code>$this-&gt;dummyQueries[$i]</code>
<code>$this-&gt;dummyQueries[$i]['result']</code>
<code>$this-&gt;dummyQueries[$result - self::OFFSET_GLOBAL]</code>
<code>$this-&gt;filoQueries[$i]</code>
<code>$this-&gt;filoQueries[$i]</code>
<code>$this-&gt;filoQueries[$i]</code>
<code>$this-&gt;filoQueries[$i]['result']</code>
<code>$this-&gt;filoQueries[$result]</code>
</MixedArrayAccess>
<MixedArrayAssignment occurrences="4">
<code>$this-&gt;dummyQueries[$i]</code>
<code>$this-&gt;filoQueries[$i]</code>
<code>$this-&gt;filoQueries[$i]</code>
<code>$this-&gt;filoQueries[]</code>
</MixedArrayAssignment>
<MixedArrayOffset occurrences="2">
<code>$query_data['result'][$query_data['pos']]</code>
<code>$ret[$query_data['columns'][$key]]</code>
</MixedArrayOffset>
<MixedAssignment occurrences="6">
<code>$query</code>
<MixedAssignment occurrences="4">
<code>$query_data['pos']</code>
<code>$ret</code>
<code>$ret[$query_data['columns'][$key]]</code>
<code>$unUsed[]</code>
<code>$val</code>
</MixedAssignment>
<MixedInferredReturnType occurrences="3">
<MixedInferredReturnType occurrences="2">
<code>?array</code>
<code>array</code>
<code>int|numeric-string</code>
</MixedInferredReturnType>
<MixedOperand occurrences="1">
<code>$query_data['pos']</code>
</MixedOperand>
<MixedReturnStatement occurrences="4">
<MixedReturnStatement occurrences="2">
<code>$cached_affected_rows ?? 0</code>
<code>$ret</code>
<code>$this-&gt;dummyQueries[$result - self::OFFSET_GLOBAL]</code>
<code>$this-&gt;filoQueries[$result]</code>
</MixedReturnStatement>
<MixedReturnTypeCoercion occurrences="2">
<code>$unUsed</code>
<code>array[]</code>
</MixedReturnTypeCoercion>
</file>
<file src="test/classes/Stubs/DummyResult.php">
<MixedReturnTypeCoercion occurrences="6">

View File

@ -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<string, array{string, string, int}>
*/
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,
],
];
}

View File

@ -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<string|float|int|null>>|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<array<string|float|int|null>>|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<int, array<int, array{string: string}|bool|int|string|null>|bool>|bool $result
* @phpstan-param list<array<string|float|int|null>>|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'],