Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/internal/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public function fetchRecord(string $query, array $params = []): array
public function exec(string $query, array $params = []): void
{
if ($params === []) {
$this->connection->exec($query);
if ($this->connection->exec($query) === false) {
$this->prepareException($this->connection);
}

return;
}

Expand All @@ -62,6 +65,7 @@ public function exec(string $query, array $params = []): void
$this->prepareException($this->connection);
}

// $statement->execute() throws under PDO::ERRMODE_EXCEPTION (PHP 8 default).
$statement->execute($params);
}
}
6 changes: 5 additions & 1 deletion src/internal/Statement.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ public function fetchRecord(string $query, array $params = []): array
public function exec(string $query, array $params = []): void
{
if ($params === []) {
$this->connection->exec($query);
if ($this->connection->exec($query) === false) {
$this->prepareException($this->connection);
}

return;
}

Expand All @@ -52,6 +55,7 @@ public function exec(string $query, array $params = []): void
$this->prepareException($this->connection);
}

// $statement->execute() throws under PDO::ERRMODE_EXCEPTION (PHP 8 default).
$statement->execute($params);
}
}
33 changes: 29 additions & 4 deletions tests/workflow/ConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@

namespace dbschemix\pdo\tests\workflow;

// Note: the branch `return []` in fetchRecord (when execute() returns false) and the
// `$this->prepareException()` branch are not reachable through a real in-memory SQLite
// PDO handle under normal conditions. These two edges are therefore intentionally left
// without direct coverage here, consistent with the strategy used across this test suite.
// Note: the `return []` branch in fetchRecord() (when execute() returns false) and its
// `prepareException()` branch (when prepare() returns false) are not reachable through an
// in-memory SQLite PDO handle under the default ERRMODE_EXCEPTION, and are intentionally
// left without direct coverage here.

use Throwable;
use PDO;
use Testo\Assert;
use Testo\Expect;
use Testo\Lifecycle\BeforeTest;
use Testo\Test;
use dbschemix\core\exception\PrepareException;
use dbschemix\pdo\internal\Connection;
use dbschemix\pdo\internal\Transaction;

Expand Down Expand Up @@ -147,6 +149,29 @@ public function fetchRecordReturnsEmptyArrayOnNoMatch(): void
Assert::blank($data);
}

/**
* exec() without params raises PrepareException when the underlying PDO::exec()
* returns false. Forces the failure by switching the handle to ERRMODE_SILENT and
* passing syntactically invalid SQL.
*
* @throws Throwable
*/
public function execRaisesPrepareExceptionOnFailure(): void
{
// Pattern locks the ErrorInfo format: SQLSTATE (non-numeric token) and a non-empty
// multi-word driver message — keeps the assertion stable across libsqlite versions
// while still distinguishing $errorInfo[0]/[1]/[2] should those indices be swapped.
Expect::exception(PrepareException::class)
->withMessagePattern('/^SQLSTATE\[[A-Z]\w+\]: error: .+ .+/');

$pdo = new PDO(dsn: 'sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
]);
$connection = new Connection($pdo, Transaction::class);

$connection->exec('THIS IS NOT VALID SQL');
}

/**
* Three consecutive exec()-with-params calls each use a fresh prepare()+execute() cycle.
* fetchRecord() must return all three rows, proving no statement handle is reused
Expand Down
25 changes: 25 additions & 0 deletions tests/workflow/StatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use Throwable;
use PDO;
use Testo\Assert;
use Testo\Expect;
use Testo\Lifecycle\AfterTest;
use Testo\Lifecycle\BeforeTest;
use Testo\Test;
use dbschemix\core\connection\TransactionInterface;
use dbschemix\core\exception\PrepareException;
use dbschemix\pdo\internal\Transaction;

#[Test]
Expand Down Expand Up @@ -132,6 +134,29 @@ public function execParamsAreNotInterpolated(): void
Assert::array($data)->hasKeys($maliciousName);
}

/**
* exec() without params raises PrepareException when the underlying PDO::exec()
* returns false. Forces the failure by switching the handle to ERRMODE_SILENT and
* passing syntactically invalid SQL.
*
* @throws Throwable
*/
public function execRaisesPrepareExceptionOnFailure(): void
{
// Pattern locks the ErrorInfo format: SQLSTATE (non-numeric token) and a non-empty
// multi-word driver message — keeps the assertion stable across libsqlite versions
// while still distinguishing $errorInfo[0]/[1]/[2] should those indices be swapped.
Expect::exception(PrepareException::class)
->withMessagePattern('/^SQLSTATE\[[A-Z]\w+\]: error: .+ .+/');

$pdo = new PDO(dsn: 'sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
]);
$transaction = Transaction::begin($pdo);

$transaction->exec('THIS IS NOT VALID SQL');
}

/**
* @throws Throwable
*/
Expand Down
Loading