aura/sqlquery
Independent query builders for MySQL, Postgres, SQLite, and SQL Server that work without any database connection library. Build SQL strings cleanly, with PDO recommended for execution.
The second beta of the 7.x line. It does not add features so much as rework the ? placeholder machinery introduced in beta1 and reverse two of its behaviours. That rework is the most consequential change to the prior assessment:
| Change | Impact on assessment |
|---|---|
A ? given a value is now bound under a generated name (__1__, __2__); getBindValues() returns ['__1__' => 5] instead of [0 => 5] |
Statements can no longer mix ? with named placeholders — which plain PDO rejects on MySQL/PostgreSQL and which pdo_sqlite silently ran with the ? unbound. A real correctness fix. But any code reaching binds by numeric index (getBindValues()[0], bindValue(0, ...)) breaks |
? placeholders in separate where() calls no longer throw (reverses a beta1 behaviour) |
Retires what the prior assessment called the single largest upgrade risk — the multi-? case — for positional bindings. The named-placeholder reuse rule is unchanged |
Value/? count mismatch now throws InvalidArgumentException for any count, not just lists/sub-selects |
Placeholder typos and edit-time drift fail at build instead of at execute or silently |
Values passed with a closure condition now fill the ? its conditions leave unfilled, in reading order, under names allocated before the closure runs |
New, genuinely useful ergonomics for grouped conditions; the ordering rule is subtle and needs a test, not a reading |
?? (PDO's literal-? escape) not treated as a placeholder; ? inside comments and string literals ignored, dialect-aware (MySQL # comments, -- only before whitespace, PostgreSQL E'...' and $$...$$) |
Correctness for raw SQL, and a reminder that this scanning is heuristic and dialect-coupled |
resetWhere()/resetHaving()/resetTables()/resetUnions()/resetWith() drop generated values their clause bound, unless a rendered UNION branch still spells them |
Prevents orphan parameters that PDO rejects at execute. The union carve-out is the exception to understand |
Select::$text_pattern removed; subclasses must override getTextPattern() instead (reading moved to AbstractQuery) |
A subclass written against beta1 that overrode the property is silently ignored — no fatal, no deprecation. Worst failure shape in the release |
New Pgsql\Quoter leaves dollar-quoted strings ($$...$$, $tag$...$tag$) as written; previously where('a = $$t.c$$') compared against "t"."c" (#274) |
Correctness win, PostgreSQL only; other dialects unchanged |
Still in force unless marked:
| Change | Status |
|---|---|
| PHP 8.4 minimum (CI on 8.4/8.5) | Unchanged — still the gating prerequisite |
Native param/return/property types everywhere; 166 static-returning methods |
Unchanged — still a load-time fatal for mismatched overrides |
WithInterface (with(), withRecursive(), resetWith(), hasWith()) on all four query types |
Unchanged — still the flagship justification |
Aura\SqlQuery\Exception deprecated in favour of ExceptionInterface; removal in 8.x |
Unchanged — still scheduled debt if written against |
| Duplicate named placeholders across clauses throw | Unchanged for names; the positional ? half is superseded by beta2's generated-name binding |
reset() returns static; union()/unionAll() take optional ?SelectInterface; join()/joinSubSelect() take array $bind; subSelect() moved to AbstractQuery with $source; rebuildCondAndBindValues() takes $clause; getStatement() abstract; getLastInsertIdName() narrowed |
Unchanged |
The pattern to note: beta2 reversed two behaviours beta1 introduced, one release apart. That is normal for a first-of-line beta, and it is also the clearest evidence available that the placeholder contract is not yet settled.
Strengths (retained):
QueryFactory and per-dialect quoters.$this contract enforceable rather than docblock-only.fromSubSelect()/joinSubSelect() accept any SelectInterface, so adapter-supplied builders nest without unwrapping.QueryInterface and traits (WhereTrait, WithTrait).Strengths (new or strengthened in beta2):
? with a value becomes a name, the generated statement is uniformly name-bound. This is a better contract for an adapter, for logging, and for PDO — it structurally eliminates the mixed-style statement that MySQL/PostgreSQL drivers reject and that SQLite ran with a hole in it.where(fn ($q) => $q->where('a = ?')->where('b = ?', [6]), [5])) now distributes them into the unfilled placeholders in reading order. This makes nested/grouped predicates composable in a way they were not in 3.x and were not in beta1.reset*() hygiene. Dropping generated values when their clause is cleared is what makes the generated-name scheme survivable in long-lived builders — without it, every resetWhere() would leave a binding PDO rejects.? inside comments, string literals, E'...' and dollar-quoted blocks — and the PostgreSQL quoter no longer mangling $$...$$ — removes a class of wrong-SQL bugs that previously only showed up in production data.Gaps vs. Laravel:
DB::raw().static returns makes "thin wrapper" considerably less thin; composition should be the default.strict_types, so behaviour depends on the calling file; cast at the edge.getBindValues() keys look like a stable API (__1__) but are allocated per render and released on reset. Treat them as opaque — the moment an adapter or a test asserts on __1__, it has coupled to an implementation detail.Laravel stack compatibility:
getBindValues() is string-keyed, it can be passed to DB::select($sql, $bindings) — Laravel's bind path preserves array keys and PDO accepts named parameters. Verify against your Laravel version rather than assuming; the previous numeric-key convention masked this question entirely.QueryFactory as a singleton remains correct.Key integration points (updated):
| Laravel Component | Aura.SqlQuery 7.x Equivalent | Integration Approach |
|---|---|---|
DB::select() |
Select |
Wrap; forward name-keyed binds. |
DB::table()->where() |
Select::from()->where() |
Compose, don't extend — extension inherits the 166-static conformance tax. |
DB::raw() |
Manual strings | fromRaw()/whereRaw(); now safer on PostgreSQL thanks to #274. |
| (no equivalent) | with(), withRecursive() |
Primary use case — CTEs Laravel cannot express. |
| Upserts | OnConflictUpdateInterface (Pgsql/SQLite) |
Dialect-correct ON CONFLICT. |
| Eloquent relationships | CTEs / subqueries | Custom accessors; no relationship layer. |
| Query scopes | Fluent methods | Rewrite; not reusable as-is. |
DB::transaction() |
None | Keep in Laravel; compose at the call site. |
Adapter-layer feasibility (improved but still bounded): the beta1 concern about positional binds across separate where() calls is gone — those now work. The remaining tax is name-keyed: an adapter must never index binds numerically, never assume name stability, and must surface a stable toSql()/getBindings() pair to consumers. getTextPattern() replaces the removed $text_pattern property for any dialect-specific text handling.
High:
static methods or redeclaring a typed property fatals at autoload. Loud, but total for the affected path.$text_pattern — a beta1-era subclass that overrode the property now has no effect and no warning. Text-pattern behaviour silently falls back to the parent. This is the one failure in the release that neither static analysis nor a load-time fatal will catch.getBindValues()[0], bindValue(0, ...), array_values()-then-reindex assumptions, and any test asserting on binding shape. Not findable by type checking alone; getBindValues() return type is array either way.cols()/where()/having() is unchanged.Aura\SqlQuery\Exception is deprecated and slated for removal in 8.x; write against ExceptionInterface now.reset*() drops generated values unless a rendered UNION branch still spells them. Builders reused across union construction and reset are the place to look for "parameter count does not match" errors at execute.Reduced or closed risk (new in beta2):
?/named statements — structurally impossible now; previously a hard PDO failure on MySQL/PostgreSQL and a silent unbound placeholders on SQLite.? collision across separate where() calls — closed (throws in beta1 → works in beta2).? count mismatch — now always throws, at build time."t"."c" — fixed (#274).LIMIT drop, docblock/implementation drift.Mitigation strategies:
getBindValues()[, bindValue(0, bindValue($i, array_values(...getBindValues()), and any test fixture asserting a bind-array shape. Named-placeholder reuse across clauses still needs the code-review sweep.__N__ keys.getTextPattern(), not the removed property; grep for text_pattern across the codebase and in any vendored fork.^7.0.ExceptionInterface, not the deprecated interface.limit/offset to int at the edge so strict and non-strict callers behave identically.getBindValues() by index, replaces a bind by position, or asserts on binding shape in tests? Was the beta1 remediation plan built around renaming ? placeholders, and does it now need to be rewritten around key access instead?text_pattern overrides — does any subclass, fork or vendored copy override it? If so, who converts it to getTextPattern() and how is the silent-failure case detected in review?static methods and typed properties, and is PHPStan/Rector at max level gating merges?ExceptionInterface so the 8.x removal is a non-event?QueryFactory as singleton or contextual binding; no change required by beta2.with(), withRecursive()), exposed through a dedicated reporting repository rather than bolted onto general Eloquent usage.toSql(): string + getBindings(): array where bindings are string-keyed and names are opaque. This is a cleaner contract than 3.x or beta1 offered, and it is the piece consumers must not over-fit to.Aura\SqlQuery\ExceptionInterface. Every concrete exception derives from \LogicException, so broad catch (\LogicException) blocks silently absorb package errors — audit existing handlers.| Phase | Actionable Steps | Tools/Dependencies |
|---|---|---|
| 0. Runtime | PHP 8.4+ everywhere; CI matrix and base images. | Docker, GitHub Actions, platform runtime config. |
| 1. Static audit | Find subclasses, adapters, property redeclarations; find text_pattern overrides; find numeric bind access; find named-placeholder reuse across clauses. |
PHPStan (max), Rector, ripgrep. |
| 2. Sandbox | Pin 7.0.0-beta2 explicitly alongside existing 3.x usage in a branch. |
Composer with an exact pre-release constraint. |
| 3. Signature remediation | Match native types and static returns; redeclare properties with identical types; implement getStatement() on custom query types; add $source/$clause to overridden subSelect()/rebuildCondAndBindValues(); replace $text_pattern overrides with getTextPattern(). |
PHPUnit/Pest, static analysis in CI. |
| 4. Bind remediation | Replace numeric-index bind reads and positional replacements with name-based access where names are the caller's own; where the name is generated, restructure to use bindValues() or a named placeholder instead. Rename colliding named placeholders across cols()/where()/having(). Drop beta1 plans that renamed ? placeholders in separate where() calls — those now work. |
Code review; per-query integration tests. |
| 5. Exception migration | Replace catch (Aura\SqlQuery\Exception) with ExceptionInterface; remove direct instantiation/subclassing of the old interface. |
Rector rule, static analysis. |
| 6. Adapter | Build the facade/macro using composition; expose opaque name-keyed bindings; cast limit/offset at the edge. |
Laravel facades, macros, strict_types=1. |
| 7. CTE adoption | Move one reporting query to with()/withRecursive() behind a flag; compare rows and latency against the DB::raw() version. |
Debugbar, Blackfire/Tideways. |
| 8. Rollout | Expand to the audited set; hold general query building on Laravel's native builder. | Env feature toggles, Horizon for queue-side visibility. |
| 9. GA gate | Re-evaluate at 7.0.0 stable; re-run phases 3–5 against GA deltas. | Release notes, docs/upgrading.md. |
There is no incremental path from 3.x — four majors are skipped. docs/upgrading.md is the only supported guide, and after beta2 it should be assumed incomplete with respect to the betas themselves; the beta release notes are the authority for beta1→beta2 deltas.
select(), from(), where(), join(), groupBy(), orderBy(), limit(), offset(), union()/unionAll(), with()/withRecursive().pluck(), chunk(), cursor(), Eloquent methods (note the with() naming collision between CTEs and eager loading), relationship methods.reset() returns static; limit()/offset() no longer coerce non-numeric strings; getBindValues() keys are names, not integers; ?/named mixing is impossible; reset*() may drop bindings you did not add by name.newQuery() overrides returning a composed builder remain the recommended seam.OnConflictUpdateInterface. PostgreSQL gains correct dollar-quoted literal handling. SQL Server vendor clauses (OUTPUT) still require extending SqlServerQuoter.DB::select() intact — verify on your version; this is newly relevant because bind arrays changed shape.strict_types=1 in adapter code and cast at boundaries.__1__ is a latent break. Publish an internal rule: bind keys are opaque.Aura\SqlQuery\Exception → ExceptionInterface before 8.x, or pay a second breaking change shortly after this one.InvalidArgumentException on a ?/value count mismatch, and a named-placeholder collision message, are far easier to triage than a wrong row count or a dropped LIMIT. Support cost per incident should fall even as incident count briefly rises.bindValues() for intentional overwrites.limit('5') throws" → cast at the edge; do not rely on the package's lack of strict_types.Select::$text_pattern; override getTextPattern() instead.| Mode | Trigger | Detection | Blast radius |
|---|---|---|---|
| Fatal on class load | Adapter/subclass signature mismatch after upgrade | Immediate, first autoload | Total for affected paths |
| Override silently ignored | Subclass overrode Select::$text_pattern (removed in beta2) |
None at build — wrong text pattern per dialect | Silent, potentially wide |
InvalidArgumentException on ?/value mismatch |
Placeholder count ≠ values after an edit | At build | Single request |
Named-placeholder collision (LogicException) |
Same name reused across cols()/where()/having() |
At build | Single request (500 if uncaught) |
| PDO parameter-count error at execute | Generated value survived reset*() because a rendered union branch still spells it |
At execute | Single request |
| Broken bind lookup | getBindValues()[0] or bindValue(0, ...) after beta2 |
Test, or first wrong-value write | Per call site |
TypeError on scalar |
limit('abc'), or limit('5') under strict_types=1 |
At build | Single request |
| Exception-handler over-capture | catch (\LogicException) swallowing package errors |
In testing, if at all | Potentially broad |
| Unbounded result set / SET clobbering | Previously silent; closed in beta1 and still closed | — | Closed |
Mixed ?/named statements, unbound ? on SQLite, corrupted $$...$$ on PostgreSQL |
Previously driver-rejected or silent; closed in beta2 | — | Closed |
The dangerous shape has shifted. Beta1's headline risk was silent-wrong-answer becomes loud exception. Beta2 largely fixes that and introduces the opposite: $text_pattern is the one change that degrades silently, and numeric bind access is the one that fails as a plausible-looking wrong value rather than an error. Those two deserve explicit review attention; the loud exceptions do not.
docs/upgrading.md is the primary artifact for 3.x→7.x; the beta1 and beta2 release notes are the authority for the beta deltas and must be read together. If any internal adapter documentation was written against beta1, mark it stale and re-review — beta2 invalidates the positional-bind guidance in it.DB::raw() output; and a test covering a closure condition with mixed filled/unfilled ? placeholders.How can I help you explore Laravel packages today?