Skip to content

Commit

Permalink
Support for delete, insert, update, replace (#483)
Browse files Browse the repository at this point in the history
Co-authored-by: Jakub Vojacek <jakub@motv.eu>
Co-authored-by: Markus Staab <maggus.staab@googlemail.com>
  • Loading branch information
3 people authored Jan 6, 2023
1 parent c28af8a commit af71a9e
Show file tree
Hide file tree
Showing 32 changed files with 6,928 additions and 6,493 deletions.
326 changes: 163 additions & 163 deletions .phpstan-dba-mysqli.cache

Large diffs are not rendered by default.

328 changes: 164 additions & 164 deletions .phpstan-dba-pdo-mysql.cache

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This extension provides the following features, as long as you [stick to the rul
* [query plan analysis](https://staabm.github.io/2022/08/16/phpstan-dba-query-plan-analysis.html) to detect performance issues
* builtin support for `doctrine/dbal`, `mysqli`, and `PDO`
* API to configure the same features for your custom sql based database access layer
* Opt-In analysis of write queries (since version 0.2.55+)

In case you are using Doctrine ORM, you might use `phpstan-dba` in tandem with [phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine).

Expand Down Expand Up @@ -56,6 +57,7 @@ $config = new RuntimeConfiguration();
// $config->debugMode(true);
// $config->stringifyTypes(true);
// $config->analyzeQueryPlans(true);
// $config->analyzeWriteQueries(true); // requires transaction support in db schema and db driver

// TODO: Put your database credentials here
$mysqli = new mysqli('hostname', 'username', 'password', 'database');
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ If not configured otherwise, the following defaults are used:
- when analyzing a php8+ codebase, [`PDO::ERRMODE_EXCEPTION` error handling](https://www.php.net/manual/en/pdo.error-handling.php) is assumed.
- when analyzing a php8.1+ codebase, [`mysqli_report(\MYSQLI_REPORT_ERROR | \MYSQLI_REPORT_STRICT);` error handling](https://www.php.net/mysqli_report) is assumed.
- the fetch mode defaults to `QueryReflector::FETCH_TYPE_BOTH`, but can be configured using the [`defaultFetchMode`](https://github.com/staabm/phpstan-dba/tree/main/src/QueryReflection/RuntimeConfiguration.php) option.
- only readable queries are analyzed per default. In case your database schema and database driver support transactions, you may consider enabled writable queries using the [`analyzeWriteQueries`](https://github.com/staabm/phpstan-dba/tree/main/src/QueryReflection/RuntimeConfiguration.php) option.
24 changes: 18 additions & 6 deletions src/Analyzer/QueryPlanAnalyzerMysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,26 @@ public function analyze(string $query): QueryPlanResult
$simulatedQuery = 'EXPLAIN '.$query;

if ($this->connection instanceof PDO) {
$stmt = $this->connection->query($simulatedQuery);
$this->connection->beginTransaction();

// @phpstan-ignore-next-line
return $this->buildResult($simulatedQuery, $stmt);
try {
$stmt = $this->connection->query($simulatedQuery);

// @phpstan-ignore-next-line
return $this->buildResult($simulatedQuery, $stmt);
} finally {
$this->connection->rollBack();
}
} else {
$result = $this->connection->query($simulatedQuery);
if ($result instanceof \mysqli_result) {
return $this->buildResult($simulatedQuery, $result);
$this->connection->begin_transaction();

try {
$result = $this->connection->query($simulatedQuery);
if ($result instanceof \mysqli_result) {
return $this->buildResult($simulatedQuery, $result);
}
} finally {
$this->connection->rollback();
}
}

Expand Down
2 changes: 0 additions & 2 deletions src/Ast/ExpressionFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ public function findBindCalls(Expr $expr): array
}

/**
* XXX use astral simpleNameResolver instead.
*
* @param Expr|Variable|MethodCall $node
*
* @return string|null
Expand Down
26 changes: 19 additions & 7 deletions src/DbSchema/SchemaHasherMysql.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,27 @@ public function hashDb(): string

$hash = '';
if ($this->connection instanceof PDO) {
$stmt = $this->connection->query($query);
foreach ($stmt as $row) {
$hash = $row['dbsignature'] ?? '';
$this->connection->beginTransaction();

try {
$stmt = $this->connection->query($query);
foreach ($stmt as $row) {
$hash = $row['dbsignature'] ?? '';
}
} finally {
$this->connection->rollBack();
}
} else {
$result = $this->connection->query($query);
if ($result instanceof \mysqli_result) {
$row = $result->fetch_assoc();
$hash = $row['dbsignature'] ?? '';
$this->connection->begin_transaction();

try {
$result = $this->connection->query($query);
if ($result instanceof \mysqli_result) {
$row = $result->fetch_assoc();
$hash = $row['dbsignature'] ?? '';
}
} finally {
$this->connection->rollback();
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/QueryReflection/MysqliQueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function __construct(mysqli $mysqli)
$this->db->set_charset('utf8');
// enable exception throwing on php <8.1
mysqli_report(\MYSQLI_REPORT_ERROR | \MYSQLI_REPORT_STRICT);
$this->db->autocommit(false);
}

public function validateQueryString(string $queryString): ?Error
Expand Down Expand Up @@ -145,7 +146,11 @@ private function simulateQuery(string $queryString)
return $this->cache[$queryString] = null;
}

$this->db->begin_transaction(\MYSQLI_TRANS_START_READ_ONLY);
if (QueryReflection::getRuntimeConfiguration()->isAnalyzingWriteQueries()) {
$this->db->begin_transaction();
} else {
$this->db->begin_transaction(\MYSQLI_TRANS_START_READ_ONLY);
}

try {
$result = $this->db->query($simulatedQuery);
Expand Down
14 changes: 2 additions & 12 deletions src/QueryReflection/PdoMysqlQueryReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,24 +48,14 @@ protected function simulateQuery(string $queryString)
return $this->cache[$queryString] = null;
}

try {
$this->pdo->beginTransaction();
} catch (PDOException $e) {
// not all drivers may support transactions
throw new \RuntimeException('Failed to start transaction', $e->getCode(), $e);
}
$this->pdo->beginTransaction();

try {
$stmt = $this->pdo->query($simulatedQuery);
} catch (PDOException $e) {
return $this->cache[$queryString] = $e;
} finally {
try {
$this->pdo->rollBack();
} catch (PDOException $e) {
// not all drivers may support transactions
throw new \RuntimeException('Failed to rollback transaction', $e->getCode(), $e);
}
$this->pdo->rollBack();
}

$this->cache[$queryString] = [];
Expand Down
16 changes: 14 additions & 2 deletions src/QueryReflection/QueryReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,20 @@ public static function setupReflector(QueryReflector $reflector, RuntimeConfigur

public function validateQueryString(string $queryString): ?Error
{
if ('SELECT' !== self::getQueryType($queryString)) {
return null;
if (self::getRuntimeConfiguration()->isAnalyzingWriteQueries()) {
if (!\in_array(self::getQueryType($queryString), [
'SELECT',
'INSERT',
'DELETE',
'UPDATE',
'REPLACE',
], true)) {
return null;
}
} else {
if ('SELECT' !== self::getQueryType($queryString)) {
return null;
}
}

// this method cannot validate queries which contain placeholders.
Expand Down
6 changes: 5 additions & 1 deletion src/QueryReflection/QuerySimulation.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ public static function simulate(string $queryString): ?string
if (null === $queryString) {
return null;
}
$queryString .= ' LIMIT 0';

// make sure we don't unnecessarily transfer data, as we are only interested in the statement is succeeding
if ('SELECT' === QueryReflection::getQueryType($queryString)) {
$queryString .= ' LIMIT 0';
}

return $queryString;
}
Expand Down
24 changes: 24 additions & 0 deletions src/QueryReflection/RuntimeConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ final class RuntimeConfiguration
* @var bool
*/
private $stringifyTypes = false;
/**
* @var bool
*/
private $writableQueries = false;
/**
* @var bool|0|positive-int
*/
Expand Down Expand Up @@ -107,6 +111,21 @@ public function stringifyTypes(bool $stringify): self
return $this;
}

/**
* Enables checking of writable queries (INSERT, UPDATE, DELETE,...).
*
* This feature requires a database and a database driver which supports transactions.
* Otherwise, the analysis might lead to data loss!
*
* Also make sure your mysql tables use the InnoDB engine.
*/
public function analyzeWriteQueries(bool $enabled): self
{
$this->writableQueries = $enabled;

return $this;
}

/**
* Enables query plan analysis, which indicates performance problems.
*
Expand Down Expand Up @@ -154,6 +173,11 @@ public function isStringifyTypes(): bool
return $this->stringifyTypes;
}

public function isAnalyzingWriteQueries(): bool
{
return $this->writableQueries;
}

/**
* @return QueryReflector::FETCH_TYPE*
*/
Expand Down
Loading

0 comments on commit af71a9e

Please sign in to comment.