v3.2.0
What's Changed
Top-level
WITH name AS (...)Common Table Expressions — a singleWITH
keyword declares any number of comma-separated, forward-chaining CTEs that
the mainSELECTand itsJOIN/UNIONbranches can reference by name.
Each CTE is materialised once into an in-memory stream and shared across
references. Fluent API gets a matchingwith()registry. No breaking
changes.
Added
-
WITH name AS (SELECT ...) [, name2 AS (SELECT ...) ...]clause. Parser
accepts aWITHblock before any top-levelSELECT(including
EXPLAIN [ANALYZE] WITH ...). Later CTEs may reference earlier ones
(forward-only chaining). Duplicate names andWITH RECURSIVEproduce
targetedParseExceptions. References to undefined CTE names inFROM/
JOINraise a parser error that lists the declared names — typos surface
before the builder ever opens a file. -
FQL\Sql\Ast\Node\CommonTableExpressionNodeand
FQL\Sql\Ast\Expression\CteReferenceNodeAST shapes;SelectStatementNode
gained a$commonTablesarray (default[], fully back-compatible). -
FQL\Sql\Parser\WithClauseParser. Mirrors the circular-DI pattern used
byFromClauseParser/UnionParser. Manages the in-WITH known-CTE-names
scope onFromClauseParserincrementally so forward chaining works during
body parsing. -
FQL\Sql\Builder\CteRegistry+CteReferenceCounter. Per-build registry
with a memory-aware resolution strategy:- FROM position → always materialise the CTE body (the outer SELECT
merges clauses into the same Query instance, so a fresh, clause-mergeable
Query is mandatory). - JOIN / UNION position with multi-reference (refCount ≥ 2) →
materialise on the first reference, share the cached stream with the rest. - JOIN / UNION position with a single reference → inline (build the
body fresh, return it directly, no caching). JOIN/UNION consume the Query
opaquely, so neither field-collision nor the in-memory buffer is needed. - JOIN after FROM of the same CTE → JOIN re-uses the FROM's already-
materialised cache for free.
A cycle guard rejects mutually recursive references at build time.
- FROM position → always materialise the CTE body (the outer SELECT
-
FQL\Traits\Withabletrait +Interface\Query::with()/hasCte()/
getCte()/getCtes(). Fluent counterpart of the parser-level WITH:
register named sub-queries on a Query for downstream JOIN/UNION composition.
Note: FROM-position CTE reference is parser-only because the source stream
of an already-constructed Query is immutable. -
SqlFormatter::renderWithClause()— round-trips WITH statements back
to readable SQL.CteReferenceNoderenders as the bare CTE name. -
KEYWORD_WITHandKEYWORD_RECURSIVEtokens inTokenType/Tokenizer
(RECURSIVEexists solely so the parser can raise an early, clear error
for unsupported recursive CTEs). -
New example
examples/with.phpandcomposer example:withscript. -
New tests:
tests/SQL/SqlWithTest.php,tests/Query/WithTest.php,
tests/Traits/WithableTest.php.
Changed
QueryBuildingVisitor::buildSource()and the formerresolveJoinSource()
were unified into a singleresolveSource(ExpressionNode): Interface\Query
helper. BothFROMandJOINsource materialisation now flow through one
method, which made the CTE branch a one-line addition rather than a
cross-cutting change. Pure internal refactor; behaviour is identical for
non-WITH statements (full test suite passes unchanged).FromClauseParsergained asetKnownCteNames(array)/getKnownCteNames()
pair used byWithClauseParser/StatementParserto scope CTE name
visibility. When the scope is empty, the parser behaves exactly as before.Interface\Querygained aWITHconstant alongside the other clause
keywords.
Notes
- Evaluation strategy is position-aware. FROM-position references always
materialise (the outer statement merges further clauses into the returned
Query — an inline body would collide on field names). JOIN/UNION-position
references inline by default (the Query is consumed opaquely, no clauses
ever land on it) and only escalate to materialisation when the CTE is
referenced more than once. JOINs reuse any cache already populated by a
prior FROM reference for free. Net effect: a CTE used exactly once in a
JOIN never pays the in-memory materialisation buffer. - Out of MVP scope:
WITH RECURSIVE, nestedWITHinside subqueries,
CTE references insideIN (...)/EXISTS (...)(the parser does not yet
support subqueries in condition operands).DESCRIBE+WITHis rejected
by the parser (DESCRIBE expects a source, not a SELECT).
Full Changelog: v3.1.0...v3.2.0