API index and search · Build metadata
Source snapshot
packages/query-compiler/src/postgres.ts
1// The Postgres dialect leaf (§6.1/§6.2) — the product target. The walker is shared with the2// SQLite oracle; this file supplies only the Postgres-specific leaves:3//4// - JSON assembly via `jsonb_build_object` / `jsonb_agg` / `'[]'::jsonb` (the design's §6.15// choice — the direct analogue of `rindle-d2s`'s `json_object`, NOT z2s's `row_to_json`);6// - explicit `ASC NULLS FIRST` / `DESC NULLS LAST` (§8 — Postgres's default is the opposite7// of the engine's null-low order);8// - the outbound projection reconciliation (temporal → epoch-ms);9// - the inbound filter/paging cast strategy `$N::text::<type>`, ported verbatim from z2s's10// `sql.ts` — the `::text` intermediate pins the driver's parameter description to text, and11// the `::<type>` half is value-model↔native reconciliation (§6.2). Values are parameterized.12//13// NOTE: `$N` numbering is per-occurrence (no de-duplication of repeated values). z2s reuses a14// placeholder for a repeated (value,type); that is a plan-cache optimization, not correctness —15// deferred (203 §7 posture).1617import type { Dir } from "./ast.ts";18import type { ColumnType } from "./catalog.ts";19import type { Dialect, LitScalar } from "./dialect.ts";20import {21 formatTypeForLookup,22 isPgNativeStringType,23 isPgNumberType,24 isPgStringType,25 isPgTextRepresentedType,26 isTemporalType,27 needsTimeOfDayNormalization,28} from "./pg-types.ts";2930function quoteIdent(name: string): string {31 return `"${name.replace(/"/g, '""')}"`;32}3334/** The Postgres cast for a literal whose type is inferred from its JS value (no column). */35function pgTypeForLiteralType(litType: "boolean" | "number" | "string"): string {36 // `double precision` is IEEE-754 like a JS number, so it round-trips any zql number exactly.37 return litType === "boolean" ? "boolean" : litType === "number" ? "double precision" : "text";38}3940/**41 * The inbound value↔native cast for a filter/paging value against a known column type — the42 * verbatim port of z2s's `formatCommonToSingularAndPlural` (`sql.ts:194`). `vp` is the value43 * placeholder (`$N`, or `value` inside an array unnest). `isComparison` widens strings to bare44 * `text` and numbers to `double precision` so neither is forced to the column's width/precision.45 */46function pgCast(vp: string, type: string, isEnum: boolean, isComparison: boolean): string {47 const t = formatTypeForLookup(type);48 // Temporal: epoch-ms ↔ native. The zone-naive spellings get `AT TIME ZONE 'UTC'`.49 if (t === "timestamptz" || t === "timestamp with time zone") {50 return `to_timestamp(${vp}::text::numeric / 1000.0)`;51 }52 if (t === "date" || t === "timestamp" || t === "timestamp without time zone") {53 return `to_timestamp(${vp}::text::numeric / 1000.0) AT TIME ZONE 'UTC'`;54 }55 if (t === "timetz" || t === "time with time zone") {56 return `(${vp}::text::int * interval'1ms')::time`;57 }58 if (t === "time" || t === "time without time zone") {59 return `(${vp}::text::int * interval'1ms')::time AT TIME ZONE 'UTC'`;60 }61 if (t === "uuid") return `${vp}::text::uuid`;62 if (isEnum) return `${vp}::text::"${type}"`;63 if (isPgNativeStringType(type)) return isComparison ? `${vp}::text` : `${vp}::text::${type}`;64 if (isPgTextRepresentedType(type)) return `${vp}::text::${type}`;65 if (isPgNumberType(type)) {66 return isComparison ? `${vp}::text::double precision` : `${vp}::text::${type}`;67 }68 return `${vp}::text::${type}`;69}7071/** The text-pinned serialization of a bound value — z2s's `stringify` (`sql.ts:120`). Strings72 * and enum/string columns bind their raw text; everything else binds its JSON text form so the73 * `$N::text::…` cast re-parses it authoritatively. */74function stringifyForColumn(value: LitScalar, col: ColumnType): string | null {75 if (value === null) return null;76 if (col.isArray) return JSON.stringify(value);77 if (col.isEnum || isPgStringType(col.type)) return String(value);78 return JSON.stringify(value);79}8081function bindValue(82 params: unknown[],83 value: LitScalar,84 col: ColumnType | null,85 isComparison: boolean,86): string {87 const idx = params.length + 1;8889 // Free literal (no column context): infer the cast from the JS type.90 if (col === null) {91 if (value === null) {92 params.push(null);93 return `$${idx}`;94 }95 const litType = typeof value as "boolean" | "number" | "string";96 params.push(litType === "string" ? value : JSON.stringify(value));97 return `$${idx}::text::${pgTypeForLiteralType(litType)}`;98 }99100 params.push(stringifyForColumn(value, col));101 if (value === null && col.isArray) return `$${idx}`;102 if (col.isArray) {103 // Unnest a JSON array param, casting each element (z2s's `formatPlural`, `sql.ts:260`).104 return `ARRAY(SELECT ${pgCast("value", col.type, col.isEnum, isComparison)} FROM jsonb_array_elements_text($${idx}::text::jsonb))`;105 }106 return pgCast(`$${idx}`, col.type, col.isEnum, isComparison);107}108109/**110 * Project a stored column into the value model's representation (§6.2 outbound half). Temporal111 * columns become **integer epoch-ms** — the value model always snaps to whole milliseconds, so112 * `::bigint` rounds off the native µs resolution (unlike z2s, which projects the non-normalized113 * path as an unsnapped float). Time-of-day-with-offset types additionally wrap `mod 1 day`.114 * Enums (their label), uuid, numeric, json, and the scalar types project raw. See115 * `needsTimeOfDayNormalization` for the deliberate divergence from z2s on `timestamptz`.116 *117 * The rounding mode (`::bigint` = round-half-to-even) must match pg-source's projector for exact118 * parity — it is the one shared value-model contract (§10), not an independent choice here.119 */120function projectColumn(colRef: string, col: ColumnType | null): string {121 if (!col || col.isEnum || !isTemporalType(col.type)) return colRef;122 const normalize = needsTimeOfDayNormalization(col.type);123 const toMs = (epoch: string): string =>124 normalize ? `((${epoch})::bigint + 86400000) % 86400000` : `(${epoch})::bigint`;125 if (col.isArray) {126 return `CASE WHEN ${colRef} IS NULL THEN NULL ELSE ARRAY(SELECT ${toMs(`EXTRACT(EPOCH FROM unnest(${colRef})) * 1000`)}) END`;127 }128 return toMs(`EXTRACT(EPOCH FROM ${colRef}) * 1000`);129}130131function orderDir(dir: Dir): string {132 // Engine order is null-low; Postgres's default is the opposite, so make it explicit (§8).133 return dir === "asc" ? "ASC NULLS FIRST" : "DESC NULLS LAST";134}135136/**137 * The Postgres dialect — the BYO-Postgres server-mutator read target. Emits `(sql, params)`138 * with `$N::text::<type>` parameters and never imports a driver (Invariant 7).139 */140export const postgresDialect: Dialect = {141 name: "postgres",142 objectBuild: (parts) => `jsonb_build_object(${parts.join(", ")})`,143 groupArray: (elem, orderSql) => `jsonb_agg(${elem}${orderSql})`,144 emptyArray: "'[]'::jsonb",145 reassertJson: (x) => x, // jsonb is a real type; no re-assert needed146 trueLit: "TRUE",147 falseLit: "FALSE",148 orderDir,149 projectColumn,150 bindValue,151 quoteIdent,152};153