SimulateDml.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin\Import;
  4. use PhpMyAdmin\Core;
  5. use PhpMyAdmin\DatabaseInterface;
  6. use PhpMyAdmin\Html;
  7. use PhpMyAdmin\SqlParser\Parser;
  8. use PhpMyAdmin\SqlParser\Statement;
  9. use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
  10. use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
  11. use PhpMyAdmin\SqlParser\Utils\Query;
  12. use PhpMyAdmin\Url;
  13. use PhpMyAdmin\Util;
  14. use Webmozart\Assert\Assert;
  15. use function array_key_exists;
  16. use function array_reverse;
  17. use function implode;
  18. final class SimulateDml
  19. {
  20. /** @var DatabaseInterface */
  21. private $dbi;
  22. public function __construct(DatabaseInterface $dbi)
  23. {
  24. $this->dbi = $dbi;
  25. }
  26. public function getError(): string
  27. {
  28. return $this->dbi->getError();
  29. }
  30. /**
  31. * Find the matching rows for UPDATE/DELETE query.
  32. *
  33. * @param DeleteStatement|UpdateStatement $statement
  34. *
  35. * @return array<string, int|string>|null
  36. * @psalm-return array{
  37. * sql_query: string,
  38. * matched_rows: int,
  39. * matched_rows_url: string
  40. * }
  41. */
  42. public function getMatchedRows(Parser $parser, Statement $statement): array
  43. {
  44. if ($statement instanceof DeleteStatement) {
  45. $matchedRowsQuery = $this->getSimulatedDeleteQuery($parser, $statement);
  46. } else {
  47. $matchedRowsQuery = $this->getSimulatedUpdateQuery($parser, $statement);
  48. }
  49. // Execute the query and get the number of matched rows.
  50. $matchedRows = $this->executeMatchedRowQuery($matchedRowsQuery);
  51. $matchedRowsUrl = Url::getFromRoute('/sql', [
  52. 'db' => $GLOBALS['db'],
  53. 'sql_query' => $matchedRowsQuery,
  54. 'sql_signature' => Core::signSqlQuery($matchedRowsQuery),
  55. ]);
  56. return [
  57. 'sql_query' => Html\Generator::formatSql($statement->build()),
  58. 'matched_rows' => $matchedRows,
  59. 'matched_rows_url' => $matchedRowsUrl,
  60. ];
  61. }
  62. /**
  63. * Executes the matched_row_query and returns the resultant row count.
  64. *
  65. * @param string $matchedRowQuery SQL query
  66. */
  67. private function executeMatchedRowQuery(string $matchedRowQuery): int
  68. {
  69. $this->dbi->selectDb($GLOBALS['db']);
  70. $result = $this->dbi->tryQuery($matchedRowQuery);
  71. if (! $result) {
  72. return 0;
  73. }
  74. return (int) $result->numRows();
  75. }
  76. /**
  77. * Transforms a DELETE query into SELECT statement.
  78. *
  79. * @return string SQL query
  80. */
  81. private function getSimulatedDeleteQuery(Parser $parser, DeleteStatement $statement): string
  82. {
  83. $tableReferences = Query::getTables($statement);
  84. Assert::count($tableReferences, 1, 'No joins allowed in simulation query');
  85. Assert::notNull($parser->list, 'Parser list not set');
  86. $condition = Query::getClause($statement, $parser->list, 'WHERE');
  87. $where = $condition === '' ? '' : ' WHERE ' . $condition;
  88. $order = $statement->order === null || $statement->order === []
  89. ? ''
  90. : ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
  91. $limit = $statement->limit === null ? '' : ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
  92. return 'SELECT * FROM ' . $tableReferences[0] . $where . $order . $limit;
  93. }
  94. /**
  95. * Transforms a UPDATE query into SELECT statement.
  96. *
  97. * @return string SQL query
  98. */
  99. private function getSimulatedUpdateQuery(Parser $parser, UpdateStatement $statement): string
  100. {
  101. $tableReferences = Query::getTables($statement);
  102. Assert::count($tableReferences, 1, 'No joins allowed in simulation query');
  103. Assert::isNonEmptyList($statement->set, 'SET statements missing');
  104. Assert::notNull($parser->list, 'Parser list not set');
  105. $values = [];
  106. $newColumns = [];
  107. $oldColumns = [];
  108. foreach (array_reverse($statement->set) as $set) {
  109. $column = Util::unQuote($set->column);
  110. if (array_key_exists($column, $values)) {
  111. continue;
  112. }
  113. $oldColumns[] = Util::backquote($column);
  114. $values[$column] = $set->value . ' AS ' . ($newColumns[] = Util::backquote($column . ' `new`'));
  115. }
  116. $condition = Query::getClause($statement, $parser->list, 'WHERE');
  117. $where = $condition === '' ? '' : ' WHERE ' . $condition;
  118. $order = $statement->order === null || $statement->order === []
  119. ? ''
  120. : ' ORDER BY ' . Query::getClause($statement, $parser->list, 'ORDER BY');
  121. $limit = $statement->limit === null ? '' : ' LIMIT ' . Query::getClause($statement, $parser->list, 'LIMIT');
  122. return 'SELECT *' .
  123. ' FROM (' .
  124. 'SELECT *, ' . implode(', ', $values) . ' FROM ' . $tableReferences[0] . $where . $order . $limit .
  125. ') AS `pma_tmp`' .
  126. ' WHERE NOT (' . implode(', ', $oldColumns) . ') <=> (' . implode(', ', $newColumns) . ')';
  127. }
  128. }