hiblaphp/sqlite
Composer 安装命令:
composer require hiblaphp/sqlite
包简介
Non Blocking Sqlite3 Client
README 文档
README
A modern, async-first, high-performance SQLite3 client for PHP with true non-blocking I/O via isolated worker processes, robust connection pooling, and smart backpressure streaming.
Features
| Feature | Status | Notes |
|---|---|---|
| True Non-Blocking I/O | Supported | Achieved via isolated IPC worker processes. The event loop is never blocked by SQLite disk I/O. |
| Sync Fallback | Supported | Transparently falls back to synchronous execution if proc_open is disabled or forced via config. |
| Lazy connection pooling | Supported | No worker processes spawned until the first query. |
| Parameterized queries | Supported | SQL-injection safe via prepared statements. |
Named parameters (:name) |
Supported | Parsed client-side, works with query(), prepare(), and all transaction methods. |
Positional ? parameters |
Supported | Supported natively. |
| Prepared statements | Supported | Explicit lifecycle control with prepare() and close(). |
| Statement caching | Supported | Per-connection LRU cache, eliminates repeated PREPARE round-trips over IPC. |
| Streaming results | Supported | Row-by-row delivery with IPC backpressure. Safely stream millions of rows with stable memory usage. |
| Transactions | Supported | High-level transaction() with auto commit/rollback and retry, low-level beginTransaction(). |
| Savepoints | Supported | savepoint(), rollbackTo(), releaseSavepoint(). |
| Query cancellation | Supported | Configurable cancellation logic (kill_worker_on_cancel) to drop queries or terminate OS process trees. |
| Self-healing workers | Supported | Workers auto-exit on memory bloat (memory_limit_mb) and are transparently respawned by the pool. |
| Boot protection | Supported | "Thundering herd" retry logic prevents -wal and -shm locking collisions on high-concurrency boot. |
| Health checks | Supported | healthCheck() pings idle workers and evicts stale ones. |
| Pool stats | Supported | $client->stats for live pool and worker introspection. |
hiblaphp/sql contracts |
Supported | Fully implements SqlClientInterface, drivers are swappable. |
Contents
Getting started
Configuration
Core API
Advanced features
- Connection pooling
- Worker lifecycle & memory management
- Query cancellation
- Sync Fallback
- Reset Connection & Caching
- Platform Notes & Quirks
Working with responses
Development
Meta
Installation
This package is currently in beta. Before installing, ensure your
composer.jsonallows beta releases:
{
"minimum-stability": "beta",
"prefer-stable": true
}
composer require hiblaphp/sqlite
Requirements:
- PHP 8.4+
- The
sqlite3PHP extension
Quick start
use Hibla\Sqlite\SqliteClient; use function Hibla\await; // The client is lazy. No workers are spawned until the first query. $client = new SqliteClient('sqlite:///path/to/database.sqlite'); // Simple query $users = await($client->query('SELECT * FROM users WHERE active = ?', [true])); echo $users->rowCount; // Named parameters $user = await( $client->query( 'SELECT * FROM users WHERE email = :email AND status = :status', ['email' => 'alice@example.com', 'status' => 'active'] ) ); // Prepared statement (recommended for repeated execution) $stmt = await( $client->prepare('SELECT * FROM users WHERE email = :email') ); $result = await($stmt->execute(['email' => 'alice@example.com'])); await($stmt->close()); // Streaming large result sets $stream = await($client->stream('SELECT * FROM logs ORDER BY id DESC')); foreach ($stream as $row) { processLog($row); }
How it works: The IPC Architecture
The native PHP sqlite3 extension is inherently blocking. When you execute a query, it halts the entire PHP process until the disk I/O completes.
To achieve true non-blocking, asynchronous behaviour, SqliteClient relies on network sockets. SQLite doesn't have a network server. To solve this, hiblaphp/sqlite uses Isolated IPC Worker Daemons.
When an async connection is required, the library spawns a background PHP CLI process (proc_open) dedicated to that connection. Commands (queries, prepare, stream) are sent from the parent event loop to the worker via standard input (STDIN) using Newline-Delimited JSON (NDJSON). The worker executes the blocking SQLite call and streams the results back to the parent via STDOUT.
This architecture guarantees that your main application's event loop never blocks on disk I/O, allowing you to run thousands of concurrent tasks while a pool of background workers safely handles the SQLite database.
If
proc_openis disabled in your environment, or if you explicitly request it, the client transparently degrades to aSyncConnectionfallback, executing queries normally in the main thread.
hiblaphp/sql contracts
SqliteClient fully implements the hiblaphp/sql contract package, defining the common interfaces shared across all Hibla database drivers:
| Interface | Implemented by |
|---|---|
SqlClientInterface |
SqliteClient |
PreparedStatement |
ManagedPreparedStatement, TransactionPreparedStatement |
Transaction |
Transaction |
Result |
Result (implements SqliteResult) |
RowStream |
SqliteRowStream, SyncRowStream |
This means you can type-hint against SqlClientInterface in your application code and swap the underlying driver (e.g., to Postgres or MySQL) without changing your business logic.
SqliteConfig
SqliteConfig is the canonical, immutable connection-level configuration object. All three config formats accepted by SqliteClient (DSN string, associative array, and SqliteConfig directly) are normalised to this type internally.
Construction
Direct constructor:
use Hibla\Sqlite\ValueObjects\SqliteConfig; $config = new SqliteConfig( database: '/var/data/production.sqlite', busyTimeout: 5000, journalMode: 'WAL', memoryLimitMB: 64, );
Array parser:
$config = SqliteConfig::fromArray([ 'database' => '/var/data/production.sqlite', 'busy_timeout' => 5000, 'journal_mode' => 'WAL', 'foreign_keys' => true, 'kill_worker_on_cancel' => false, 'force_sync' => false, 'memory_limit_mb' => 128, ]);
URI / DSN string:
$config = SqliteConfig::fromUri( 'sqlite:///var/data/production.sqlite?busy_timeout=5000&journal_mode=WAL&memory_limit_mb=128' ); // For in-memory testing: $memoryConfig = SqliteConfig::fromUri('sqlite:///:memory:');
Properties
| Property | Type | Default | Description |
|---|---|---|---|
database |
string |
required | Path to the .sqlite file, or :memory:. |
busyTimeout |
int |
5000 |
Milliseconds SQLite will wait for a lock before throwing a LockWaitTimeoutException (SQLITE_BUSY). |
journalMode |
string |
'WAL' |
PRAGMA journal_mode. Write-Ahead Logging (WAL) is highly recommended for concurrent access. |
foreignKeys |
bool |
true |
PRAGMA foreign_keys. Enforces relational constraints. |
killWorkerOnCancel |
bool |
false |
Action taken on promise cancellation. See Query cancellation. |
memoryLimitMB |
int |
128 |
Max RAM the background worker daemon can consume before auto-restarting. See Worker lifecycle. |
resetConnection |
bool |
false |
Drops temp tables and resets PRAGMAs mimicking DISCARD ALL when released to the pool. |
forceSync |
bool |
false |
If true, bypasses background IPC workers entirely and runs standard blocking SQLite in the main thread. |
connectTimeout |
int |
10 |
Seconds before a connect attempt is aborted. |
The SqliteClient
use Hibla\Sqlite\SqliteClient; $client = new SqliteClient( config: 'sqlite:///database.sqlite', minConnections: 0, maxConnections: 10, idleTimeout: 60, maxLifetime: 3600, statementCacheSize: 256, enableStatementCache: true, maxWaiters: 100, acquireTimeout: 10.0, deleteDatabaseOnShutdown: false, );
Constructor parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
$config |
SqliteConfig|array|string |
required | Database configuration. |
$minConnections |
int |
0 |
Minimum number of workers to keep alive. |
$maxConnections |
int |
10 |
Hard cap on the number of concurrent worker processes. |
$idleTimeout |
int |
60 |
Seconds a worker can remain idle before being killed to save RAM. |
$maxLifetime |
int |
3600 |
Maximum seconds an async worker may live before it is safely rotated out to prevent memory fragmentation. |
$statementCacheSize |
int |
256 |
Maximum number of prepared statements to cache per connection. |
$enableStatementCache |
bool |
true |
When enabled, query($sql, $params) reuses server-side statement handles instead of sending PREPARE commands over IPC repeatedly. |
$maxWaiters |
int |
0 |
Maximum number of callers that may queue waiting for a free worker before a PoolException is thrown immediately. 0 means unlimited. |
$acquireTimeout |
float |
10.0 |
Maximum seconds to wait for a free worker. |
$deleteDatabaseOnShutdown |
bool |
false |
If true, cleanly unlinks the .sqlite, -wal, and -shm files when the pool is closed. Excellent for ephemeral testing environments. |
$onConnect |
callable|null |
null |
Optional hook invoked when a new worker is spawned. |
Making queries
Queries with parameters (prepared statements)
When parameters are provided, the library automatically uses a prepared statement. Parameters are safely bound natively using their corresponding SQLITE3_* types.
Note on booleans: SQLite has no native boolean type. The library automatically normalizes PHP
true/falseto1and0when binding parameters.
Positional ? placeholders:
$result = await( $client->query( 'SELECT id, name FROM users WHERE created_at > ? AND status = ?', [$since, 'active'] ) );
Named :name placeholders:
The library features a robust, SQL-string-aware parser that safely converts named parameters into positional parameters under the hood, ensuring compatibility with all SQLite syntax constraints.
$result = await( $client->query( 'SELECT id, name FROM users WHERE created_at > :since AND status = :status', ['since' => $since, 'status' => 'active'] ) );
Convenience methods
// Returns affected row count $count = await($client->execute('UPDATE users SET status = ?', ['active'])); // Returns the last inserted row ID (via SQLite3::lastInsertRowID) $newId = await($client->executeGetId('INSERT INTO users (name) VALUES (:name)', ['name' => 'Alice'])); // Returns first row as associative array, or null $user = await($client->fetchOne('SELECT * FROM users WHERE id = ?', [$userId])); // Returns value of first column from first row $name = await($client->fetchValue('SELECT name FROM users WHERE id = ?', [$userId]));
Prepared statements
Use explicit prepared statements when you need to execute the exact same query many times in a loop, avoiding repeated parsing and IPC overhead.
$stmt = await( $client->prepare('SELECT * FROM products WHERE category_id = :categoryId') ); $electronics = await($stmt->execute(['categoryId' => 1])); $clothing = await($stmt->execute(['categoryId' => 2])); await($stmt->close());
SqliteClient::query()handles statement preparation and LRU caching for you transparently. Explicitprepare()is best for micro-optimizations within tight loops.
Streaming results
Streaming allows you to process multi-gigabyte result sets without running out of RAM. The hiblaphp/sqlite streaming engine features IPC Backpressure. If your PHP loop processes rows slower than the worker reads them, the worker is automatically paused, ensuring the IPC pipe buffer never overflows.
$stream = await( $client->stream('SELECT * FROM huge_audit_log ORDER BY id DESC', bufferSize: 200) ); foreach ($stream as $row) { processLog($row); }
To cancel a stream before it is fully consumed:
foreach ($stream as $row) { if (shouldStop($row)) { $stream->cancel(); // Signals the worker to abort the query cleanly break; } }
Transactions
High-level API: transaction()
The transaction() method automatically handles BEGIN, commit, rollback, and retry logic. The callback is implicitly wrapped in a Fiber via async().
$result = await( $client->transaction(function (TransactionInterface $tx) use ($from, $to) { await($tx->execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [$from])); await($tx->execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [$to])); return 'Transfer completed'; }) );
If any await() inside the callback throws, the client automatically issues a ROLLBACK and re-throws the exception.
Automatic retry
Because SQLite uses database-level locks, concurrent writes often encounter SQLITE_BUSY ("database is locked"). The transaction() method automatically retries the entire callback on deadlocks (DeadlockException) and lock wait timeouts (LockWaitTimeoutException).
Pass withAttempts() to enable retry:
use Hibla\Sql\TransactionOptions; await( $client->transaction( function (TransactionInterface $tx) { // Write logic here }, TransactionOptions::default()->withAttempts(5) ) );
Low-level API: beginTransaction()
$tx = await($client->beginTransaction()); try { await($tx->execute('UPDATE ...')); await($tx->commit()); } catch (\Throwable $e) { await($tx->rollback()); throw $e; }
Tainted state & Savepoints
If any query inside a transaction throws, the transaction is marked tainted. Subsequent queries or attempts to commit() will immediately throw a TransactionException without contacting the database.
You must either rollback() or use rollbackTo(string $savepoint) to clear the tainted state.
await( $client->transaction(function (TransactionInterface $tx) { await($tx->savepoint('sp1')); try { await($tx->execute('INVALID SQL')); } catch (\Throwable $e) { // Clears the tainted state, allowing the transaction to continue await($tx->rollbackTo('sp1')); } }) );
Worker lifecycle & memory management
Because PHP is not designed to be a long-running daemon, background workers can suffer from memory fragmentation over time.
hiblaphp/sqlite implements self-healing enterprise memory management:
- Garbage Collection: Workers run
gc_collect_cycles()every 1,000 queries. - Memory Limits: If a worker exceeds
$config->memoryLimitMB(default 128MB), it cleanly closes its database handle and exits. ThePoolManagerinstantly detects the exit, silently spawns a fresh replacement worker, and routes the next query without dropping any client promises. - Thundering Herd Protection: When the pool boots up and spawns multiple workers concurrently, they coordinate file creation to prevent
-waland-shmlocking collisions.
Query cancellation
SQLite queries cannot be easily interrupted once handed off to the native C extension. You have two choices when calling $promise->cancel() on a pending query, controlled by killWorkerOnCancel:
killWorkerOnCancel = false(Default): The promise is rejected with aCancelledExceptionon the client side immediately. The worker is allowed to finish the query in the background, and is then recycled back into the pool. This is safe and prevents orphaned processes.killWorkerOnCancel = true: The library usesproc_terminateto violently kill the entire OS process tree for that specific worker. The query is instantly aborted, and the pool spawns a fresh worker to replace it. Use this if you frequently run runaway multi-minute analytic queries that must be stopped instantly to save CPU.
Sync Fallback
If you deploy to an environment where proc_open is disabled (e.g., restrictive shared hosting), the library detects this via SystemHelper::isAsyncSupported() and transparently falls back to SyncConnection.
In this mode, SQLite commands execute normally in the main thread. The API remains exactly the same, meaning you do not have to change a single line of your application code to support constrained environments.
You can also force this mode manually:
$config = new SqliteConfig('sqlite.db', forceSync: true);
Reset Connection & Caching
If resetConnection is enabled, the pool mimics PostgreSQL's DISCARD ALL behavior when a worker is released. It automatically:
- Drops all
TEMPtables andTEMPviews. - Clears session-scoped dangerous PRAGMAs (e.g.,
read_uncommitted,defer_foreign_keys). - Triggers
PRAGMA shrink_memory. - Clears the client-side statement cache.
This guarantees absolute isolation between requests in a persistent web server environment.
Platform Notes & Quirks
Native SQLite URIs on Windows
The underlying SQLite C library supports native URI filenames (e.g., file:database.sqlite?cache=shared), which allows passing connection-level pragmas directly in the file path.
However, PHP's ext-sqlite3 extension on Windows does not support this. If you attempt to pass a native file: URI to the database configuration property on Windows, PHP's internal path resolution will fail before reaching SQLite, resulting in:
Exception: Unable to expand filepath
The Solution:
Do not use native SQLite file: URIs. Instead, always use Hibla's built-in DSN configuration string (sqlite://...).
Hibla's DSN parser safely extracts query parameters (like timeouts and journal modes) and applies them via discrete PRAGMA commands, passing a completely clean, OS-safe file path to the native extension. This guarantees identical behavior across Linux, macOS, and Windows.
// ❌ WRONG: Fails on Windows PHP due to native ext-sqlite3 path resolution bugs $client = new SqliteClient([ 'database' => 'file:/var/data/db.sqlite?mode=ro&cache=shared' ]); // ✅ CORRECT: Hibla parses the DSN, handles the config, and passes a safe path to SQLite $client = new SqliteClient('sqlite:///var/data/db.sqlite?busy_timeout=5000&journal_mode=WAL');
Result inspection
$result = await($client->query('SELECT * FROM users')); echo $result->rowCount; // int, rows in result set echo $result->affectedRows; // int, rows affected by INSERT/UPDATE/DELETE echo $result->lastInsertId; // int, last insert row ID echo $result->connectionId; // int, unique ID of the worker connection echo $result->columnCount; // int, number of columns echo $result->columns; // list<string> of column names foreach ($result as $row) { echo $row['name']; } $row = $result->fetchOne(); // first row as associative array, or null $all = $result->fetchAll(); // all rows as array of associative arrays $col = $result->fetchColumn('name'); // all values from a named column
Development
git clone https://github.com/hiblaphp/sqlite.git
cd sqlite
composer install
Running tests
The test suite runs against an ephemeral in-memory/temp-file database.
composer test
Static analysis & Formatting
composer analyze composer format
License
MIT License. See LICENSE for more information.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-20