Skip to content

fix(cte): accept the MATERIALIZED hint on a CTE - #314

Merged
tiagolauer merged 1 commit into
masterfrom
fix/283-cte-materialized
Aug 2, 2026
Merged

fix(cte): accept the MATERIALIZED hint on a CTE#314
tiagolauer merged 1 commit into
masterfrom
fix/283-cte-materialized

Conversation

@tiagolauer

Copy link
Copy Markdown
Owner

Fixes #283.

The bug

Query<DB, 'with t as materialized (select id from users) select id from t'>
// was: { [x: string]: unknown }[]
// strict: QueryTypeError<'unknown table: '>

AS MATERIALIZED (...) and AS NOT MATERIALIZED (...) are Postgres 12+ planner hints that sit between as and the body. ParseCteEntry wanted the opening paren directly after as, so either spelling failed the match and took the whole WITH clause with it.

The failure was quiet rather than loud, which is the second half of this. ParseWithClause had no [never] guard, so never extends { ctes: infer C extends CteEntry[]; rest: infer R extends string } passed with both infers falling back to their constraints. ResolveCteContext then got a rest of string, and the query degraded into an index signature row instead of reporting anything.

The fix

Skip the hint, and add the guard. Skipping is enough on its own because the hint changes how the CTE is executed, not what it returns. The guard is the same [never] pattern ExtraSourcesAfterKeyword carries from #229, and it means the next unparseable WITH clause fails cleanly instead of degrading.

Versioning

Minor: a query that failed to parse now parses and types correctly.

Verification

tests/cte-materialized.test-d.ts, seven cases red on master:

  • materialized and not materialized
  • both in strict mode, including a typo inside the CTE still being caught
  • a materialized CTE alongside a plain one
  • param inference through a materialized body
  • with recursive and the hint together

Two controls stay pinned: a plain CTE is unchanged, and a column actually named materialized is still read as a column rather than the hint.

tsc --noEmit clean, 192 unit tests pass, budget 192,881 (+395 over master).

Found during an audit pass; fix and tests written with Claude Code.

`AS MATERIALIZED (...)` and `AS NOT MATERIALIZED (...)` are Postgres 12+
planner hints that sit between `as` and the body. ParseCteEntry wanted the
opening paren directly after `as`, so either spelling failed the match and
took the whole WITH clause with it. Skipping the hint is enough: it changes
how the CTE runs, not what it returns.

The failure was quiet rather than loud, and that is the second half of this.
ParseWithClause had no [never] guard, so `never extends { ctes: infer C
extends CteEntry[]; rest: infer R extends string }` passed with both infers
falling back to their constraints. ResolveCteContext then got a `rest` of
`string` and the query came back as `{ [x: string]: unknown }[]`, with
strict mode reporting `unknown table: ''`. Same [never] pattern as #229.

Under VERSIONING.md this is a minor: a query that failed to parse now
parses and types correctly.

Fixes #283

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tiagolauer
tiagolauer force-pushed the fix/283-cte-materialized branch from be669f8 to d744a72 Compare August 2, 2026 13:05
@tiagolauer
tiagolauer merged commit 0386510 into master Aug 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WITH ... AS MATERIALIZED breaks the whole WITH clause

1 participant