Skip to content

Feature wishlist / roadmap: parser-level safety (read-only/stacked-query guards, validated Identifier, static analyzer), DoS limits & migration ergonomics #7

@kakserpom

Description

@kakserpom

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions