ceibacore / ceiba-db
CeibaDB — Lightweight PDO query builder and database abstraction layer for Ceiba Core ecosystem
Requires
- php: ^8.1 || ^8.2 || ^8.3 || ^8.4
- ext-pdo: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Lightweight, zero-dependency PDO connection manager and fluent query builder for PHP 8.1+. Built for multi-tenant applications in the Ceiba Core ecosystem. MySQL/MariaDB and SQLite are covered by the test suite; PostgreSQL has driver-aware quoting and DSN but is not yet validated against a live server.
Installation
composer require ceibacore/ceiba-db
Connecting
$db = CeibaDB::getInstance([ 'driver' => 'mysql', // mysql | mariadb | pgsql | postgres | sqlite 'host' => '127.0.0.1', 'port' => 3306, 'db' => 'my_database', 'username' => 'db_user', 'password' => 'secret', 'charset' => 'utf8mb4', // MySQL/MariaDB 'prefix' => 'cb_', // optional table prefix 'timeout' => 5, // optional, seconds // 'unix_socket' => '/run/mysqld/mysqld.sock', // 'persistent' => true, // 'options' => [PDO::MYSQL_ATTR_SSL_CA => '/path/ca.pem'], ]);
One instance is cached per configuration. In long-running workers serving many tenants, release instances you no longer need:
CeibaDB::forget($tenantConfig); // drop one cached connection CeibaDB::instanceCount(); // monitor cached connections $db->disconnect(); // close now; reopens lazily on next use
Query builder
$users = $db->query('users') ->select(['id', 'name', 'email']) // raw select list (aggregates allowed) ->where(['status' => 1]) // array form ->where('created_at', '>=', '2026-01-01') // operator form ->whereIn('role', ['admin', 'editor']) ->whereNull('deleted_at') ->orderBy('name')->orderBy('id', 'DESC') // accumulates ->limit(20, 40) // LIMIT 20 OFFSET 40 ->get(); $db->query('users')->where(['id' => 5])->first(); // ?array $db->query('users')->where('status', 1)->count(); $db->query('users')->pluck('email'); foreach ($db->query('logs')->cursor() as $row) { /* one row at a time */ }
where(['col' => null]) means IS NULL; where('col', '!=', null) means IS NOT NULL. Supported operators: =, !=, <>, <, <=, >, >=, LIKE, NOT LIKE.
Writing
$id = $db->query('users')->insert(['name' => 'Jane', 'email' => 'jane@example.com']); // 0 for UUID keys $db->query('users')->insertBatch($rows); // values matched by column name $db->query('users')->where(['id' => $id])->update(['name' => 'Jane Smith']); $db->query('accounts')->where(['id' => 1])->decrement('balance', 100); $db->query('users')->where(['id' => $id])->delete();
update(), delete(), increment() and decrement() refuse to run without a WHERE condition.
Security model
- Values are always bound parameters, with explicit types (
bool→0/1,null,DateTimeInterface, backed enums,Stringable). - Column arguments accept identifiers only:
col,table.col, or an already quoted name. Anything else throwsInvalidArgumentException. select(),join(),whereRaw()andorderByRaw()take raw SQL by design: never pass user input to them.- Insert/update keys are validated as identifiers, but choosing which columns a user may write is the caller's job (allowlist them).
join()does not add the table prefix: pass the full table name.
Transactions
$db->transaction(function (CeibaDB $db) { $db->query('orders')->insert([...]); $db->transaction(fn (CeibaDB $db) => $db->query('audit')->insert([...])); // SAVEPOINT }); // Retry deadlocks / serialization failures up to 3 times (outermost transaction only). $db->transaction(fn (CeibaDB $db) => $db->query('stock')->where(['sku' => $sku])->decrement('qty'), 3);
Observability
CeibaDB::listen(function (array $event) { // ['sql' => ..., 'params' => count, 'durationMs' => float, 'error' => ?class] if ($event['durationMs'] > 200) { error_log('Slow query: ' . $event['sql']); } });
Parameter values are never included in events.
Testing
composer test
Set CEIBADB_TEST_MYSQL_HOST, CEIBADB_TEST_MYSQL_PORT, CEIBADB_TEST_MYSQL_DB, CEIBADB_TEST_MYSQL_USER and CEIBADB_TEST_MYSQL_PASS to also run the MySQL/MariaDB contract tests. For tests, CeibaDB::setInstance($pdo) makes every getInstance() return that single instance; call CeibaDB::resetInstances() in tearDown().
Sharing a host connection
When several packages use CeibaDB with different prefixes, usePdoResolver() makes all of them reuse one connection while each keeps its own configuration:
// Laravel: every CeibaDB instance runs on the framework connection (and its transactions). CeibaDB::usePdoResolver(fn (array $config) => DB::connection()->getPdo());
Call CeibaDB::usePdoResolver(null) (or resetInstances()) to return to dedicated connections.
Backward compatibility
LemurDB and LemurQuery remain available as aliases of CeibaDB and CeibaQuery.
License
MIT. See LICENSE.