Skip to content

feat: type cast placeholders in every clause and subquery - #71

Merged
omer-cengel merged 2 commits into
masterfrom
feature/cast-placeholders-in-every-clause
Oct 10, 2026
Merged

omer-cengel merged 2 commits into
masterfrom
feature/cast-placeholders-in-every-clause

Conversation

@omer-cengel

Copy link
Copy Markdown
Member

Summary

A placeholder written as the direct operand of ::type or CAST(... AS type) is now typed by that cast wherever it appears in an accepted SELECT, INSERT, UPDATE, or DELETE, and every placeholder binds in the order the query writes it. This makes dynamic ORDER BY CASE sorts, cast LIMIT/OFFSET pages, and placeholders inside EXISTS, IN (SELECT ...), and other subqueries compile instead of failing the placeholder accounting check.

Changes

  • The SQL parser reports each placeholder occurrence once, in textual order at every clause and nesting depth, together with the innermost cast whose direct operand it is.
  • Query analysis binds parameters in that textual order instead of in clause-analysis order, so a projected placeholder binds before a WHERE one and OFFSET ... LIMIT ... binds the offset first.
  • Cast placeholders are accepted in projections, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, FETCH, WITH bodies, and subqueries at any depth, in reads and writes.
  • Naming: a named placeholder keeps its name; an indexed cast placeholder that is the whole LIMIT row count or OFFSET value is limit or offset; every other newly accepted indexed cast placeholder is param<N>. Placeholders accepted before keep their names, types, and binding positions.
  • Repeated casts of one placeholder that agree share one parameter; conflicting ones are rejected with the existing conflict message.
  • Query documentation covers the new locations, names, binding order, and the forms that stay rejected.
  • Tests cover each clause in both placeholder forms, subqueries and WITH bodies, sharing and conflicts, rejections, generated-source compilation, and PostgreSQL execution of a dynamic sort, a cast page in both clause orders, and an EXISTS membership check.

Scope and non-goals

  • An uncast placeholder outside the clauses that type it, such as ORDER BY $1, LIMIT $1 + 1, FETCH FIRST $1 ROWS ONLY, an uncast subquery placeholder, or lower(name) = :name, is still rejected with the existing message, as is a placeholder that is not the cast's direct operand, such as (:x)::int.
  • Join ON conditions, conflict-clause predicates, and RETURNING items are unchanged.
  • Subqueries and WITH bodies are not otherwise analyzed: their placeholders are typed only by their casts, and a projected scalar subquery remains an unsupported projection.
  • No runtime, generator, CLI, or configuration changes.

@omer-cengel
omer-cengel merged commit 991efbe into master Oct 10, 2026
7 checks passed
@omer-cengel
omer-cengel deleted the feature/cast-placeholders-in-every-clause branch October 10, 2026 23:42
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.

1 participant