3 declare(strict_types
=1);
5 namespace PhpMyAdmin\Import
;
8 use PhpMyAdmin\Current
;
9 use PhpMyAdmin\DatabaseInterface
;
11 use PhpMyAdmin\SqlParser\Parser
;
12 use PhpMyAdmin\SqlParser\Statements\DeleteStatement
;
13 use PhpMyAdmin\SqlParser\Statements\UpdateStatement
;
14 use PhpMyAdmin\SqlParser\Utils\Query
;
17 use Webmozart\Assert\Assert
;
19 use function array_key_exists
;
20 use function array_reverse
;
23 final class SimulateDml
25 public function __construct(private DatabaseInterface
$dbi)
29 public function getError(): string
31 return $this->dbi
->getError();
35 * Find the matching rows for UPDATE/DELETE query.
37 * @return array<string, int|string>
38 * @psalm-return array{
41 * matched_rows_url: string
44 public function getMatchedRows(
47 DeleteStatement|UpdateStatement
$statement,
49 if ($statement instanceof DeleteStatement
) {
50 $matchedRowsQuery = $this->getSimulatedDeleteQuery($parser, $statement);
52 $matchedRowsQuery = $this->getSimulatedUpdateQuery($parser, $statement);
55 // Execute the query and get the number of matched rows.
56 $matchedRows = $this->executeMatchedRowQuery($matchedRowsQuery);
57 $matchedRowsUrl = Url
::getFromRoute('/sql', [
58 'db' => Current
::$database,
59 'sql_query' => $matchedRowsQuery,
60 'sql_signature' => Core
::signSqlQuery($matchedRowsQuery),
64 'sql_query' => Html\Generator
::formatSql($query),
65 'matched_rows' => $matchedRows,
66 'matched_rows_url' => $matchedRowsUrl,
71 * Executes the matched_row_query and returns the resultant row count.
73 * @param string $matchedRowQuery SQL query
75 private function executeMatchedRowQuery(string $matchedRowQuery): int
77 $this->dbi
->selectDb(Current
::$database);
78 $result = $this->dbi
->tryQuery($matchedRowQuery);
79 if ($result === false) {
83 return (int) $result->numRows();
87 * Transforms a DELETE query into SELECT statement.
89 * @return string SQL query
91 private function getSimulatedDeleteQuery(Parser
$parser, DeleteStatement
$statement): string
93 $tableReferences = Query
::getTables($statement);
94 Assert
::count($tableReferences, 1, 'No joins allowed in simulation query');
95 Assert
::notNull($parser->list, 'Parser list not set');
97 $condition = Query
::getClause($statement, $parser->list, 'WHERE');
98 $where = $condition === '' ?
'' : ' WHERE ' . $condition;
99 $order = $statement->order
=== null ||
$statement->order
=== []
101 : ' ORDER BY ' . Query
::getClause($statement, $parser->list, 'ORDER BY');
102 $limit = $statement->limit
=== null ?
'' : ' LIMIT ' . Query
::getClause($statement, $parser->list, 'LIMIT');
104 return 'SELECT * FROM ' . $tableReferences[0] . $where . $order . $limit;
108 * Transforms a UPDATE query into SELECT statement.
110 * @return string SQL query
112 private function getSimulatedUpdateQuery(Parser
$parser, UpdateStatement
$statement): string
114 $tableReferences = Query
::getTables($statement);
115 Assert
::count($tableReferences, 1, 'No joins allowed in simulation query');
116 Assert
::isNonEmptyList($statement->set
, 'SET statements missing');
117 Assert
::notNull($parser->list, 'Parser list not set');
122 foreach (array_reverse($statement->set
) as $set) {
123 $column = Util
::unQuote($set->column
);
124 if (array_key_exists($column, $values)) {
128 $oldColumns[] = Util
::backquote($column);
129 $values[$column] = $set->value
. ' AS ' . ($newColumns[] = Util
::backquote($column . ' `new`'));
132 $condition = Query
::getClause($statement, $parser->list, 'WHERE');
133 $where = $condition === '' ?
'' : ' WHERE ' . $condition;
134 $order = $statement->order
=== null ||
$statement->order
=== []
136 : ' ORDER BY ' . Query
::getClause($statement, $parser->list, 'ORDER BY');
137 $limit = $statement->limit
=== null ?
'' : ' LIMIT ' . Query
::getClause($statement, $parser->list, 'LIMIT');
141 'SELECT *, ' . implode(', ', $values) . ' FROM ' . $tableReferences[0] . $where . $order . $limit .
143 ' WHERE NOT (' . implode(', ', $oldColumns) . ') <=> (' . implode(', ', $newColumns) . ')';