PDOdb is a PHP database library for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, and Oracle. You write queries once with a fluent builder. When a query is slow, one call sends the available execution plan, columns, and indexes to a provider you choose, so you can review a suggestion without pasting the schema into chat.
Built on PDO, with no framework dependency. The package requires three PSR interfaces: psr/log, psr/simple-cache, and psr/event-dispatcher.
To try it with SQLite, install PHP 8.4+ with pdo_sqlite, then run:
composer require tommyknocker/pdo-database-class<?php
require 'vendor/autoload.php';
use tommyknocker\pdodb\PdoDb;
$db = new PdoDb('sqlite', ['path' => ':memory:']);
$db->rawQuery('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)');
$id = $db->find()->table('users')->insert(['name' => 'Alice']);
$user = $db->find()->from('users')->where('id', $id)->getOne();
if ($user !== false) {
echo $user['name'];
}Rows are associative arrays by default. asObject() returns stdClass objects instead:
// get() returns an array of objects (or an empty array if no rows match)
$users = $db->find()->from('users')->asObject()->get();
foreach ($users as $user) {
echo $user->name;
}
// getOne() returns an object, or false if no row matches
$user = $db->find()->from('users')->where('id', $id)->asObject()->getOne();
if ($user !== false) {
echo $user->name;
}Compared with raw PDO. Queries are method calls, parameters are bound for you, and helpers cover dates, JSON, aggregates, and strings. The SQL that reaches the server still follows that database's dialect.
Compared with Eloquent or Doctrine. There is no framework to adopt and no ORM unless you want the optional ActiveRecord layer. You can drop to raw SQL. Two things those libraries do not include: query advice grounded in the database plan, and a terminal dashboard for a connection you have already configured.
Several databases in one project. The same builder calls target MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, and Oracle. Where the SQL itself differs, the dialect guide says so.
You already know which query is slow. The open question is whether to change a filter, a join, an index, or nothing. Start with the plan PDOdb can read. That call does not use a network.
$analysis = $db->find()
->from('orders')
->where('status', 'pending')
->orderBy('created_at', 'DESC')
->limit(50)
->explainAdvice();
foreach ($analysis->recommendations as $recommendation) {
echo $recommendation->message . "\n";
}A full scan plus a temporary sort suggests an index on the filter columns, then the sort columns. When that index already exists, the advice says a new one is not indicated. SQL Server does not expose a verified plan through PDO, and a parameterized Oracle EXPLAIN is unavailable. Those results say the plan is missing. They do not invent one.
When you want an explanation of that same query, send the collected plan to your model:
$advice = $db->find()
->from('orders')
->where('status', 'pending')
->orderBy('created_at', 'DESC')
->limit(50)
->explainAiAdvice();
echo $advice->aiAnalysis;PDOdb collects the available plan, the columns, and the existing indexes, then makes one request to the provider configured for your application. You get an explanation and a hypothesis. PDOdb does not create the index or run the model's SQL. Details: query-advice limitations.
On the included SQLite fixture the plan before a matching index looked like this:
SCAN orders
USE TEMP B-TREE FOR ORDER BY
After the index was applied in that fixture, the same ordered page used the index:
SEARCH orders USING INDEX idx_orders_status_created (status=?)
The median SELECT on that synthetic database moved from 4.6860 ms to 0.0200 ms. That is a local measurement of the fixture, not a production promise. A third case in the same demo needs no new index: the existing one already matches the filter and the order. Walkthrough, setup, and the three cases: Understand and improve a query with AI.
In an application. Call explainAiAdvice() on the PdoDb instance you already use. Put the provider, key, and model in that process environment. Configuration reference.
Offline demo, no API key. A Composer installation places the package under vendor/tommyknocker/pdo-database-class/; it does not create examples/ in your project root. To run the demo from a repository checkout, use:
composer install
php examples/07-performance/05-ai-query-advice.phpThe command builds 100,000 synthetic orders in memory, prints real SQLite plans, and checks that each reviewed index change preserves the result.
Each item below is implemented in the library and documented on its own page.
Queries
- Fluent query builder for select, insert, update, and delete
- Filtering, joins, and aggregates, including subqueries
- 80+ SQL helpers for strings, dates, math, JSON, and aggregates
- Window functions:
ROW_NUMBER,RANK,LAG,LEAD - Common table expressions, including recursive CTEs
- LATERAL joins on PostgreSQL, MySQL, and SQL Server (
CROSS APPLY/OUTER APPLY) - UNION, INTERSECT, and EXCEPT
- Full-text search with one API over each dialect's native FTS
- DISTINCT and PostgreSQL DISTINCT ON
- FILTER aggregates, with a MySQL fallback
- Offset and cursor pagination
- Raw SQL when you want the statement yourself
Writes and schema
- INSERT ... SELECT, upserts, and MERGE
- Bulk inserts and file loaders for CSV, XML, and JSON
- DDL builder for tables, columns, indexes, and foreign keys
- Dialect-specific types: MySQL
ENUM/SET, PostgreSQLUUID/JSONB, SQL ServerUNIQUEIDENTIFIER, SQLite type affinity - Migrations and seeds with rollback
- Transactional outbox for reliable events with your own broker or HTTP publisher
- Schema introspection for indexes, foreign keys, and constraints
Speed and scale
- PSR-16 result cache for repeated queries
- Compiled-SQL cache and a prepared-statement pool
- Query profiler and EXPLAIN analysis
- Read/write splitting and sharding
- Batch and streaming reads for large result sets
- Connection retry with exponential backoff
- Transactions, savepoints, and table locks
Application code
- Optional ActiveRecord with relationships, eager and lazy loading, behaviors, transactional saves, and relationship query diagnostics
- Transactional outbox and a consumer inbox for at-least-once delivery, with a step-by-step guide
- Query scopes, macros, and plugins
- PSR-14 events
- Typed exceptions and query diagnostics with sanitized parameters
- JSON, CSV, and XML export of result sets
Tools
- AI query advice from the PHP call, plus CLI reports
- Optional MCP server so an editor can read the development schema
- Terminal dashboard (
pdodb ui) for a database configured for the CLI - CLI for migrations, seeds, outbox worker (
work,status,list,retry,purge), dumps, users, models, and a SQL REPL
The helper calls stay the same. Each dialect receives its own JSON SQL.
<?php
require 'vendor/autoload.php';
use tommyknocker\pdodb\helpers\Db;
use tommyknocker\pdodb\PdoDb;
$db = new PdoDb('sqlite', ['path' => ':memory:']);
$db->rawQuery('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, meta TEXT)');
$db->find()->table('users')->insert([
'name' => 'John',
'meta' => Db::jsonObject(['city' => 'NYC', 'age' => 30]),
]);
$users = $db->find()
->from('users')
->select([
'name',
'city' => Db::jsonGet('meta', ['city']),
])
->where(Db::jsonPath('meta', ['age'], '>', 25))
->get();
foreach ($users as $user) {
echo $user['name'] . ' ' . $user['city'] . PHP_EOL;
}More JSON operations: create and query JSON.
- PHP 8.4 or newer
mbstringand PDO, plus the extension for each database you connect to:pdo_mysql,pdo_pgsql,pdo_sqlite,pdo_sqlsrv(Microsoft ODBC Driver), orpdo_oci- Compatibility targets: MySQL 5.7+ / MariaDB 10.3+, PostgreSQL 9.4+, SQLite 3.38+, SQL Server 2019+ / Azure SQL, Oracle 12c+. Individual features may need a newer server. These targets are not a promise that every historical version is still covered or still supported by its vendor.
CI runs the database suites against:
| Database | CI version |
|---|---|
| MySQL | 8.0 |
| MariaDB | 11.2 |
| PostgreSQL | 16 |
| SQLite | Runner's bundled version |
| SQL Server | 2022 |
| Oracle | XE 21 |
SQLite JSON support:
sqlite3 :memory: "SELECT json_valid('{}')"1 means JSON is available.
composer require tommyknocker/pdo-database-classvendor/bin/pdodb init writes a connection config (.env or config/db.php) and can create migration and model directories. Full option list: CLI tools. Connection arrays, pooling, and read/write splitting: configuration.
Start with the documentation index. Guides are grouped into getting started, the query builder, JSON, advanced features, errors, helpers, and the API reference. A short path for a first connection: installation, first connection, hello world.
Find methods in the API reference and database-specific behavior in dialect differences.
Coming from raw PDO or Eloquent: migration guide. Upgrading (3.0 to 3.1 and 2.x to 3.0): UPGRADE.md. Common failures: troubleshooting.
The examples are included under vendor/tommyknocker/pdo-database-class/examples/ in the Composer package and are also available in a repository checkout. Example catalog.
# From the repository root
php examples/01-basic/02-simple-crud.php
# MySQL (update examples/config.mysql.php with your credentials)
PDODB_DRIVER=mysql php examples/01-basic/02-simple-crud.phpTo check all examples on the available databases, also from the repository root:
./scripts/test-examples.shIs PDOdb an ORM? It is a query builder with an optional ActiveRecord layer.
Can I use raw SQL? Yes. Use rawQuery() or the raw SQL API.
Can I use it with a framework? Yes. PDOdb has no framework dependency and can be used in an existing PHP application.
Does it support transactions? Yes, including savepoints for nested transactions. See transactions.
./vendor/bin/phpunit
./vendor/bin/phpunit --testsuite "MySQL Tests"
./vendor/bin/phpunit --coverage-html coverageMySQL and MariaDB expect a server on localhost:3306, PostgreSQL on 5432, SQL Server on 1433, and Oracle on 1521 (pdo_oci and Oracle Instant Client). SQLite uses :memory:.
Open an issue before a new feature, follow PSR-12, and add tests. Dialect-specific behavior belongs in that dialect's test directory. See CONTRIBUTING.md.
Development and CI use composer install so PHPStan and the other development tools follow composer.lock. The lock file does not constrain applications that require PDOdb as a package. Update dependencies with composer update and include the lock file in that change.
MIT. See LICENSE.
Inspired by ThingEngineer/PHP-MySQLi-Database-Class and the Yii2 framework.