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
2 changes: 1 addition & 1 deletion .claude/skills/adapter-dbconnection/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ConnectionPool → writer + readers[], health checks, load balancing, failove
```
Connections always injected — never created internally by consumers.

**Lazy:** `mysql()`/`postgres()`/`sqlite()` open no socket and no file. The connection opens on first access (`pdo()`, a query, a transaction); a request without DB access opens no connection. A connection error is thrown there (same `RuntimeException`), not at creation. `isConnected()` is `false` until the first access; `connect()` connects eagerly; an explicit `disconnect()` is not reopened implicitly. SQLite validates the path and applies its PRAGMAs (foreign_keys, WAL, …) at the first open and after every `reconnect()`; for `:memory:` `reconnect()` keeps an open handle (a new handle would be an empty database), only an explicit `disconnect()` followed by `reconnect()` yields a fresh one. A failed pool health check names its cause: `RuntimeException('Writer connection health check failed: <cause>', previous: $e)` (same for readers).
**Lazy:** `mysql()`/`postgres()`/`sqlite()` open no socket and no file. The connection opens on first access (`pdo()`, a query, a transaction); a request without DB access opens no connection. A connection error is thrown there (same `RuntimeException`), not at creation. `isConnected()` is `false` until the first access; `connect()` connects eagerly; an explicit `disconnect()` is not reopened implicitly. SQLite validates the path and applies its PRAGMAs (foreign_keys, WAL, …) at the first open and after every `reconnect()` (a failing PRAGMA, e.g. a file that is not a database, is a `RuntimeException('Could not apply SQLite PRAGMAs: …', previous: PDOException)` and leaves the connection closed); for `:memory:` `reconnect()` keeps an open handle (a new handle would be an empty database), only an explicit `disconnect()` followed by `reconnect()` yields a fresh one. A failed pool health check names its cause: `RuntimeException('Writer connection health check failed: <cause>', previous: $e)` (same for readers).

## FACTORY SIGNATURES
```php
Expand Down
11 changes: 8 additions & 3 deletions src/Connection/SqLite.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public function reconnect(): void
/**
* Validates the path, opens the file and applies the PRAGMAs.
*
* @throws RuntimeException On invalid path or connection error
* @throws RuntimeException On invalid path, connection error or PRAGMA failure (previous: the PDOException);
* the handle is discarded on a PRAGMA failure
*/
protected function createPdoConnection(): void
{
Expand All @@ -65,9 +66,13 @@ protected function createPdoConnection(): void

try {
$this->applySqliteOptimizations();
} catch (\Throwable $e) {
} catch (\PDOException $e) {
$this->pdo = null;
throw $e;
throw new RuntimeException(
'Could not apply SQLite PRAGMAs: ' . $e->getMessage(),
(int) $e->getCode(),
$e
);
}
}

Expand Down
61 changes: 57 additions & 4 deletions tests/Unit/LazyConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,12 @@ public function testSqliteMemoryReconnectKeepsTheExistingDatabase(): void
$this->assertSame(1, (int) $connection->pdo()->query('SELECT COUNT(*) FROM t')->fetchColumn());
}

public function testSqliteMemoryPoolHealthCheckKeepsTheDatabase(): void
/**
* Healthy path only: a connected :memory: handle passes `SELECT 1`, so the
* health check never reaches its reconnect branch and the database stays.
* The reconnect branch itself is covered by the next test.
*/
public function testSqliteMemoryPoolHealthCheckOnHealthyHandleKeepsTheDatabase(): void
{
$writer = (new ConnectionFactory())->sqlite();
$pool = new ConnectionPool($writer, config: new ConnectionPoolConfig(
Expand All @@ -211,6 +216,57 @@ public function testSqliteMemoryPoolHealthCheckKeepsTheDatabase(): void
$this->assertSame(1, (int) $pool->getWriter()->pdo()->query('SELECT COUNT(*) FROM t')->fetchColumn());
}

/**
* An explicitly disconnected connection makes pdo() throw, which sends the
* health check into its reconnect branch (ConnectionPool::performHealthCheck).
* For :memory: that reconnect opens a fresh, empty database: the old one
* died with the explicit disconnect, so nothing is lost by the check itself.
*/
public function testSqliteMemoryPoolHealthCheckReconnectsAfterExplicitDisconnect(): void
{
$writer = (new ConnectionFactory())->sqlite();
$pool = new ConnectionPool($writer, config: new ConnectionPoolConfig(
validateConnections: true,
healthCheckCacheTtl: 0,
));

$pool->getWriter()->pdo()->exec('CREATE TABLE t (id INTEGER)');
$writer->disconnect();
$this->assertFalse($writer->isConnected());

$healthy = $pool->getWriter();

$this->assertSame($writer, $healthy);
$this->assertTrue($writer->isConnected());
$this->assertSame(0, (int) $healthy->pdo()->query(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 't'"
)->fetchColumn());
}

public function testSqlitePragmaFailureIsWrappedAsRuntimeExceptionAndLeavesNoHandle(): void
{
$dir = sys_get_temp_dir() . '/lazy_sqlite_' . uniqid();
mkdir($dir);
$file = $dir . '/garbage.db';
file_put_contents($file, str_repeat('this is not a sqlite database ', 200));

try {
$connection = (new ConnectionFactory())->sqlite($file);

try {
$connection->pdo();
$this->fail('Expected RuntimeException for a PRAGMA failure');
} catch (RuntimeException $e) {
$this->assertStringContainsString('Could not apply SQLite PRAGMAs', $e->getMessage());
$this->assertInstanceOf(\PDOException::class, $e->getPrevious());
}

$this->assertFalse($connection->isConnected());
} finally {
$this->removeDir($dir);
}
}

public function testSqliteMemoryReconnectAfterExplicitDisconnectOpensFreshDatabase(): void
{
$connection = (new ConnectionFactory())->sqlite();
Expand All @@ -220,9 +276,6 @@ public function testSqliteMemoryReconnectAfterExplicitDisconnectOpensFreshDataba
$connection->reconnect();

$this->assertTrue($connection->isConnected());
$this->assertSame(1, (int) $connection->pdo()->query(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'nothing'"
)->fetchColumn() + 1);
$this->assertSame(0, (int) $connection->pdo()->query(
"SELECT COUNT(*) FROM sqlite_master WHERE name = 't'"
)->fetchColumn());
Expand Down
Loading