refactor(postgres): represent SQL as typed text/data segments #9

Open
opened 2026-07-20 21:25:06 +02:00 by codinget · 0 comments
Owner

Problem

The PostgreSQL SQL template currently renders parameters through literal ? placeholders and later rewrites every ? to a numbered PostgreSQL parameter. That representation is fragile: PostgreSQL uses ?, ?|, and ?& as real JSONB operators, and SQL text containing those operators cannot be distinguished safely from parameter placeholders.

Rework the intermediate representation so SQL text and typed data remain separate until final expansion.

Proposed representation

Use a segment array along these lines:

type SqlSegment =
  | { _sql: string }
  | {
      _data: string | number | Uint8Array;
      _type: string;
    };

type SqlCode = SqlSegment[];

The underscore-prefixed discriminants are reserved for the SQL builder. Template values should continue to be accepted through typed helpers rather than allowing callers to construct arbitrary underscore-prefixed properties accidentally. Preserve the currently supported value semantics, including an explicit representation for nullable values if required.

Composition and expansion

  • The template tag produces alternating SQL and typed-data segments without inserting placeholder characters into SQL text.
  • catSql/concatenation only concatenates segment arrays. It must not renumber, copy, or render parameters.
  • Nested SqlCode values splice their segments directly into the surrounding array.
  • A final expansion step walks the segments once:
    • _sql segments are appended verbatim.
    • The first occurrence of a data-segment object appends its value to the parameter array and renders $n::type.
    • Reusing the same data-segment object by identity renders the same $n::type and does not append the value again.
    • Distinct objects with equal values remain distinct parameters.
  • Type names inserted into SQL must come from the builder's trusted typed API/known type set, never from untrusted runtime input.

For example, reusing one typed data object twice should produce SQL equivalent to $1::text || $1::text with a one-element parameter array.

Acceptance criteria

  • No parameter expansion relies on searching or replacing characters inside raw SQL strings.
  • Literal ?, JSONB ?, ?|, and ?& operators survive expansion unchanged.
  • Concatenation and nesting are associative and do not require placeholder renumbering.
  • Repeated references to the same data-segment object share one numbered parameter by object identity.
  • Equal values stored in different segment objects receive separate numbered parameters.
  • Existing PostgreSQL queries and migration recording are migrated to the new representation.
  • Unit tests cover plain SQL, each supported data type, casts, null handling, nesting, concatenation, literal question marks/JSONB operators, and identity deduplication.
  • PostgreSQL backend tests pass against a real PostgreSQL instance once that test infrastructure is available.
## Problem The PostgreSQL SQL template currently renders parameters through literal `?` placeholders and later rewrites every `?` to a numbered PostgreSQL parameter. That representation is fragile: PostgreSQL uses `?`, `?|`, and `?&` as real JSONB operators, and SQL text containing those operators cannot be distinguished safely from parameter placeholders. Rework the intermediate representation so SQL text and typed data remain separate until final expansion. ## Proposed representation Use a segment array along these lines: ```ts type SqlSegment = | { _sql: string } | { _data: string | number | Uint8Array; _type: string; }; type SqlCode = SqlSegment[]; ``` The underscore-prefixed discriminants are reserved for the SQL builder. Template values should continue to be accepted through typed helpers rather than allowing callers to construct arbitrary underscore-prefixed properties accidentally. Preserve the currently supported value semantics, including an explicit representation for nullable values if required. ## Composition and expansion - The template tag produces alternating SQL and typed-data segments without inserting placeholder characters into SQL text. - `catSql`/concatenation only concatenates segment arrays. It must not renumber, copy, or render parameters. - Nested `SqlCode` values splice their segments directly into the surrounding array. - A final expansion step walks the segments once: - `_sql` segments are appended verbatim. - The first occurrence of a data-segment object appends its value to the parameter array and renders `$n::type`. - Reusing the same data-segment object by identity renders the same `$n::type` and does not append the value again. - Distinct objects with equal values remain distinct parameters. - Type names inserted into SQL must come from the builder's trusted typed API/known type set, never from untrusted runtime input. For example, reusing one typed data object twice should produce SQL equivalent to `$1::text || $1::text` with a one-element parameter array. ## Acceptance criteria - No parameter expansion relies on searching or replacing characters inside raw SQL strings. - Literal `?`, JSONB `?`, `?|`, and `?&` operators survive expansion unchanged. - Concatenation and nesting are associative and do not require placeholder renumbering. - Repeated references to the same data-segment object share one numbered parameter by object identity. - Equal values stored in different segment objects receive separate numbered parameters. - Existing PostgreSQL queries and migration recording are migrated to the new representation. - Unit tests cover plain SQL, each supported data type, casts, null handling, nesting, concatenation, literal question marks/JSONB operators, and identity deduplication. - PostgreSQL backend tests pass against a real PostgreSQL instance once that test infrastructure is available.
codinget added the Kind/Enhancement
Priority
Medium
3
Agentic
Agent
gpt-5.6-sol
labels 2026-07-20 21:25:06 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: codinget/abode#9