Feature wishlist / roadmap
A prioritized list of additions, written from the angle of (a) using this in a large legacy PHP codebase as a migration target and (b) leaning harder into the one thing this library uniquely has that PDO/Zend_Db/mysqli don't: a real SQL AST. Most "safe SQL" footguns are decidable from the AST you already build — so the highest-leverage features are parser-level guarantees, not more sugar.
Grouped by impact. Happy to send PRs.
Tier 1 — parser-level safety (the unique edge)
1. Statement-kind allowlist / read-only enforcement
Since the preprocessor already parses to an AST, expose a per-driver/per-query policy that rejects statement kinds at parse time:
->readOnly() driver mode that only permits SELECT/WITH ... SELECT and rejects any DML/DDL — perfect for read-replica routing and reporting/BI endpoints (least privilege enforced in-process, not just via DB grants).
->allowKinds([Select, Insert, Update]) allowlist.
This contains the blast radius of any injection that does slip through and stops "reporting endpoint accidentally runs DELETE" classes of bugs.
2. Guaranteed multi-statement (stacked-query) rejection
Make it an explicit, documented guarantee that a single query()/execute() cannot smuggle stacked statements (SELECT 1; DROP TABLE users). Reject ;-separated statements at the AST level unless an explicit ->allowMultiStatement() opt-in is set. Right now this depends on the underlying driver's protocol behavior; surfacing it as a parser guarantee + opt-in is much stronger and testable.
3. Standalone validated Identifier / Column / Table types
quoteIdentifier() and ByClause/SelectClause are great, but a low-level validated value type (mirroring Hostname/Path in php-hardened-rs) helps incremental migration of legacy dynamic-SQL code:
Identifier::from($userInput) throws unless it matches a safe charset / exists in an allowlist.
- Optional schema-aware validation: check the identifier against
describeTable() metadata so an unknown column is rejected before it ever reaches SQL.
This gives a single obvious primitive to replace the hand-rolled preg_replace('/[^\w."]+/', ...) column sanitizers that legacy codebases are full of.
4. Static analyzer / PHPStan + Psalm extension
The killer parallel to Rust sqlx's compile-time checking: a lint pass that finds SQL string literals in PHP source, runs them through the same AST parser, and reports at CI time:
- malformed placeholders, type-placeholder/argument-count mismatches,
- string concatenation into a query string (the actual injection smell),
- unknown columns/tables when a schema snapshot is provided.
Even a standalone sqlx-lint CLI (no PHPStan dependency) would be hugely valuable for migrations — point it at a directory, get a report of every risky query.
Tier 2 — DoS / resource safety
5. Per-query limits
->timeout($ms) statement timeout (map to PG statement_timeout / MySQL MAX_EXECUTION_TIME / MSSQL).
->maxRows($n) hard cap that aborts a runaway result set.
- Optional
EXPLAIN-cost guard: reject queries whose planner cost exceeds a threshold (catches accidental cartesian joins / unindexed scans before they hit prod).
6. Bind-parameter count / payload guards
Cap the number of expanded IN (...) parameters (a huge ?ia array can blow past the driver's bind limit — e.g. Postgres 65535). Fail fast with a clear ParameterException instead of a cryptic driver error, or auto-chunk with an opt-in.
Tier 3 — migration ergonomics & adoption
7. SQLite driver
PG/MySQL/MSSQL are covered; adding SQLite would make unit testing trivial (in-memory DB) and lower the adoption barrier significantly.
8. PDO / Zend_Db / mysqli compatibility shim
A thin adapter exposing a PDO-shaped surface (or a Zend_Db_Adapter-compatible one) that routes through the preprocessor. Lets a legacy codebase swap the adapter and immediately get the safe parser + pooling without rewriting every call site — then migrate to the native fluent API incrementally.
9. Fiber-based async / true concurrency
SQLx is async under the hood; expose that to PHP 8.1+ fibers (and/or a ReactPHP/phpDaemon integration — fits your existing ecosystem) so an app can run N queries concurrently from one request. This is a capability PDO fundamentally cannot offer and would be a headline differentiator.
10. Migrations runner
Wrap sqlx::migrate! into a PHP-callable migration runner (up/down, checksums, status). One fewer external tool for teams adopting the library.
Tier 4 — observability & mapping
11. OpenTelemetry spans + metrics
onQuery() is a good hook; add first-class OTel span emission (db.statement, rows, duration) and Prometheus counters/histograms so query telemetry is turnkey.
12. Result hydration into typed PHP DTOs
Map rows into user classes via constructor/attributes (->queryAllInto(User::class)), with typed casting driven by describeTable() metadata — reduces the manual array-to-object boilerplate that pushes people back to ORMs.
13. Result-level PII redaction hook
An opt-in column redactor (mask PAN/Luhn, emails, tokens) applied to result sets and to the SQL shown in profiling/logs — compliance-friendly (PCI/PII) and pairs well with onQuery().
14. Bulk ingest fast path
Expose Postgres COPY / MySQL LOAD DATA LOCAL as a safe, parameter-validated bulk-insert API for high-volume ingestion, distinct from insertMany().
Notes
- Items 1–4 are the strategic ones: they turn "we parse an AST" from an internal implementation detail into user-visible security guarantees that no PDO-based stack can match. The static analyzer (4) in particular is what makes this library compelling for auditing and migrating an existing codebase, not just writing new queries.
- Happy to contribute PRs — if you flag which Tier-1 items you'd accept I'll start there.
Feature wishlist / roadmap
A prioritized list of additions, written from the angle of (a) using this in a large legacy PHP codebase as a migration target and (b) leaning harder into the one thing this library uniquely has that PDO/Zend_Db/mysqli don't: a real SQL AST. Most "safe SQL" footguns are decidable from the AST you already build — so the highest-leverage features are parser-level guarantees, not more sugar.
Grouped by impact. Happy to send PRs.
Tier 1 — parser-level safety (the unique edge)
1. Statement-kind allowlist / read-only enforcement
Since the preprocessor already parses to an AST, expose a per-driver/per-query policy that rejects statement kinds at parse time:
->readOnly()driver mode that only permitsSELECT/WITH ... SELECTand rejects any DML/DDL — perfect for read-replica routing and reporting/BI endpoints (least privilege enforced in-process, not just via DB grants).->allowKinds([Select, Insert, Update])allowlist.This contains the blast radius of any injection that does slip through and stops "reporting endpoint accidentally runs DELETE" classes of bugs.
2. Guaranteed multi-statement (stacked-query) rejection
Make it an explicit, documented guarantee that a single
query()/execute()cannot smuggle stacked statements (SELECT 1; DROP TABLE users). Reject;-separated statements at the AST level unless an explicit->allowMultiStatement()opt-in is set. Right now this depends on the underlying driver's protocol behavior; surfacing it as a parser guarantee + opt-in is much stronger and testable.3. Standalone validated
Identifier/Column/TabletypesquoteIdentifier()andByClause/SelectClauseare great, but a low-level validated value type (mirroringHostname/Pathin php-hardened-rs) helps incremental migration of legacy dynamic-SQL code:Identifier::from($userInput)throws unless it matches a safe charset / exists in an allowlist.describeTable()metadata so an unknown column is rejected before it ever reaches SQL.This gives a single obvious primitive to replace the hand-rolled
preg_replace('/[^\w."]+/', ...)column sanitizers that legacy codebases are full of.4. Static analyzer / PHPStan + Psalm extension
The killer parallel to Rust sqlx's compile-time checking: a lint pass that finds SQL string literals in PHP source, runs them through the same AST parser, and reports at CI time:
Even a standalone
sqlx-lintCLI (no PHPStan dependency) would be hugely valuable for migrations — point it at a directory, get a report of every risky query.Tier 2 — DoS / resource safety
5. Per-query limits
->timeout($ms)statement timeout (map to PGstatement_timeout/ MySQLMAX_EXECUTION_TIME/ MSSQL).->maxRows($n)hard cap that aborts a runaway result set.EXPLAIN-cost guard: reject queries whose planner cost exceeds a threshold (catches accidental cartesian joins / unindexed scans before they hit prod).6. Bind-parameter count / payload guards
Cap the number of expanded
IN (...)parameters (a huge?iaarray can blow past the driver's bind limit — e.g. Postgres 65535). Fail fast with a clearParameterExceptioninstead of a cryptic driver error, or auto-chunk with an opt-in.Tier 3 — migration ergonomics & adoption
7. SQLite driver
PG/MySQL/MSSQL are covered; adding SQLite would make unit testing trivial (in-memory DB) and lower the adoption barrier significantly.
8. PDO / Zend_Db / mysqli compatibility shim
A thin adapter exposing a PDO-shaped surface (or a
Zend_Db_Adapter-compatible one) that routes through the preprocessor. Lets a legacy codebase swap the adapter and immediately get the safe parser + pooling without rewriting every call site — then migrate to the native fluent API incrementally.9. Fiber-based async / true concurrency
SQLx is async under the hood; expose that to PHP 8.1+ fibers (and/or a ReactPHP/phpDaemon integration — fits your existing ecosystem) so an app can run N queries concurrently from one request. This is a capability PDO fundamentally cannot offer and would be a headline differentiator.
10. Migrations runner
Wrap
sqlx::migrate!into a PHP-callable migration runner (up/down, checksums, status). One fewer external tool for teams adopting the library.Tier 4 — observability & mapping
11. OpenTelemetry spans + metrics
onQuery()is a good hook; add first-class OTel span emission (db.statement, rows, duration) and Prometheus counters/histograms so query telemetry is turnkey.12. Result hydration into typed PHP DTOs
Map rows into user classes via constructor/attributes (
->queryAllInto(User::class)), with typed casting driven bydescribeTable()metadata — reduces the manual array-to-object boilerplate that pushes people back to ORMs.13. Result-level PII redaction hook
An opt-in column redactor (mask PAN/Luhn, emails, tokens) applied to result sets and to the SQL shown in profiling/logs — compliance-friendly (PCI/PII) and pairs well with
onQuery().14. Bulk ingest fast path
Expose Postgres
COPY/ MySQLLOAD DATA LOCALas a safe, parameter-validated bulk-insert API for high-volume ingestion, distinct frominsertMany().Notes