@prisma-next/adapter-postgres 0.12.0-dev.7 → 0.12.0-dev.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-H8BiuXdq.mjs → adapter-BpvkORJG.mjs} +13 -13
- package/dist/adapter-BpvkORJG.mjs.map +1 -0
- package/dist/adapter.d.mts +3 -2
- package/dist/adapter.d.mts.map +1 -1
- package/dist/adapter.mjs +1 -1
- package/dist/column-types.d.mts.map +1 -1
- package/dist/control-adapter-BmVmHzER.mjs +1483 -0
- package/dist/control-adapter-BmVmHzER.mjs.map +1 -0
- package/dist/control.d.mts +60 -10
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +4 -660
- package/dist/control.mjs.map +1 -1
- package/dist/{descriptor-meta-C1wNCHkd.mjs → descriptor-meta-NBwpqHS7.mjs} +1 -1
- package/dist/{descriptor-meta-C1wNCHkd.mjs.map → descriptor-meta-NBwpqHS7.mjs.map} +1 -1
- package/dist/operation-types.d.mts +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +2 -2
- package/dist/{types-B1eiuBHQ.d.mts → types-Dv7M8jx8.d.mts} +1 -1
- package/dist/{types-B1eiuBHQ.d.mts.map → types-Dv7M8jx8.d.mts.map} +1 -1
- package/dist/types.d.mts +2 -2
- package/package.json +24 -24
- package/src/core/adapter.ts +28 -25
- package/src/core/control-adapter.ts +615 -193
- package/src/core/ddl-renderer.ts +155 -0
- package/src/core/enum-control-hooks.ts +2 -2
- package/src/core/marker-ledger.ts +124 -0
- package/src/core/sql-renderer.ts +66 -23
- package/dist/adapter-H8BiuXdq.mjs.map +0 -1
- package/dist/sql-renderer-DlZhVI9B.mjs +0 -457
- package/dist/sql-renderer-DlZhVI9B.mjs.map +0 -1
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
import { APP_SPACE_ID, extractCodecLookup } from "@prisma-next/framework-components/control";
|
|
2
|
+
import { LiteralExpr, RawExpr, collectOrderedParamRefs, isDdlNode } from "@prisma-next/sql-relational-core/ast";
|
|
3
|
+
import { postgresCodecRegistry } from "@prisma-next/target-postgres/codecs";
|
|
4
|
+
import { parseMarkerRowSafely, rethrowMarkerReadError, withMarkerReadErrorHandling } from "@prisma-next/errors/execution";
|
|
5
|
+
import { parseContractMarkerRow } from "@prisma-next/family-sql/verify";
|
|
6
|
+
import { UNBOUND_NAMESPACE_ID } from "@prisma-next/framework-components/ir";
|
|
7
|
+
import { PostgresTableSource, buildControlTableBootstrapQueries, buildSignMarkerBootstrapQueries, int4, int8, jsonb, pgTable, text, textArray, timestamptz } from "@prisma-next/target-postgres/contract-free";
|
|
8
|
+
import { parsePostgresDefault } from "@prisma-next/target-postgres/default-normalizer";
|
|
9
|
+
import { createResolveExistingEnumValues, enumStorageCompoundKey, readExistingEnumValues, readPostgresSchemaIrAnnotations } from "@prisma-next/target-postgres/enum-planning";
|
|
10
|
+
import { normalizeSchemaNativeType } from "@prisma-next/target-postgres/native-type-normalizer";
|
|
11
|
+
import { blindCast } from "@prisma-next/utils/casts";
|
|
12
|
+
import { ifDefined } from "@prisma-next/utils/defined";
|
|
13
|
+
import { REFERENTIAL_ACTION_SQL } from "@prisma-next/sql-contract/referential-action-sql";
|
|
14
|
+
import { escapeLiteral, quoteIdentifier } from "@prisma-next/target-postgres/sql-utils";
|
|
15
|
+
import { PG_ENUM_CODEC_ID, PG_TIMESTAMPTZ_CODEC_ID } from "@prisma-next/target-postgres/codec-ids";
|
|
16
|
+
import { createAstCodecRegistry, deriveParamMetadata, encodeParamsWithMetadata } from "@prisma-next/sql-runtime";
|
|
17
|
+
import { runtimeError } from "@prisma-next/framework-components/runtime";
|
|
18
|
+
//#region src/core/codec-lookup.ts
|
|
19
|
+
/**
|
|
20
|
+
* Build a {@link CodecLookup} populated with the Postgres-builtin codec definitions only.
|
|
21
|
+
*
|
|
22
|
+
* This is the default lookup used by `createPostgresAdapter()` and `new PostgresControlAdapter()` when called without a stack-derived lookup (e.g. from tests, or one-off scripts that don't compose a full stack).
|
|
23
|
+
*
|
|
24
|
+
* Extension codecs (e.g. `pg/vector@1` from `@prisma-next/extension-pgvector`) are intentionally NOT included here: a bare adapter cannot see extensions. Stack-composed paths (`SqlControlAdapterDescriptor.create(stack)` / `SqlRuntimeAdapterDescriptor.create(stack)`) supply the broader, extension-inclusive lookup at construction time.
|
|
25
|
+
*/
|
|
26
|
+
function createPostgresBuiltinCodecLookup() {
|
|
27
|
+
return extractCodecLookup([{
|
|
28
|
+
id: "postgres-builtin-codecs",
|
|
29
|
+
types: { codecTypes: { codecDescriptors: Array.from(postgresCodecRegistry.values()) } }
|
|
30
|
+
}]);
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region ../../../1-framework/3-tooling/migration/dist/exports/ledger-origin.mjs
|
|
34
|
+
function ledgerOriginFromStored(originCoreHash) {
|
|
35
|
+
if (originCoreHash === null || originCoreHash === "" || originCoreHash === "sha256:empty") return null;
|
|
36
|
+
return originCoreHash;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/core/ddl-renderer.ts
|
|
40
|
+
function quoteQualifiedIdentifier(name) {
|
|
41
|
+
return name.split(".").map(quoteIdentifier).join(".");
|
|
42
|
+
}
|
|
43
|
+
function renderPrimaryKeyConstraint(constraint) {
|
|
44
|
+
const cols = constraint.columns.map(quoteIdentifier).join(", ");
|
|
45
|
+
if (constraint.name !== void 0) return `CONSTRAINT ${quoteIdentifier(constraint.name)} PRIMARY KEY (${cols})`;
|
|
46
|
+
return `PRIMARY KEY (${cols})`;
|
|
47
|
+
}
|
|
48
|
+
function renderForeignKeyConstraint(constraint) {
|
|
49
|
+
const cols = constraint.columns.map(quoteIdentifier).join(", ");
|
|
50
|
+
const refCols = constraint.refColumns.map(quoteIdentifier).join(", ");
|
|
51
|
+
let sql = `FOREIGN KEY (${cols}) REFERENCES ${quoteQualifiedIdentifier(constraint.refTable)} (${refCols})`;
|
|
52
|
+
if (constraint.onDelete !== void 0) sql += ` ON DELETE ${REFERENTIAL_ACTION_SQL[constraint.onDelete]}`;
|
|
53
|
+
if (constraint.onUpdate !== void 0) sql += ` ON UPDATE ${REFERENTIAL_ACTION_SQL[constraint.onUpdate]}`;
|
|
54
|
+
if (constraint.name !== void 0) sql = `CONSTRAINT ${quoteIdentifier(constraint.name)} ${sql}`;
|
|
55
|
+
return sql;
|
|
56
|
+
}
|
|
57
|
+
function renderUniqueConstraint(constraint) {
|
|
58
|
+
const cols = constraint.columns.map(quoteIdentifier).join(", ");
|
|
59
|
+
if (constraint.name !== void 0) return `CONSTRAINT ${quoteIdentifier(constraint.name)} UNIQUE (${cols})`;
|
|
60
|
+
return `UNIQUE (${cols})`;
|
|
61
|
+
}
|
|
62
|
+
function renderTableConstraint(constraint) {
|
|
63
|
+
switch (constraint.kind) {
|
|
64
|
+
case "primary-key": return renderPrimaryKeyConstraint(constraint);
|
|
65
|
+
case "foreign-key": return renderForeignKeyConstraint(constraint);
|
|
66
|
+
case "unique": return renderUniqueConstraint(constraint);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
var PostgresDdlVisitorImpl = class {
|
|
70
|
+
createTable(node) {
|
|
71
|
+
const ifNotExists = node.ifNotExists ? "IF NOT EXISTS " : "";
|
|
72
|
+
const tableRef = node.schema ? `${quoteIdentifier(node.schema)}.${quoteIdentifier(node.table)}` : quoteIdentifier(node.table);
|
|
73
|
+
const columnDefs = node.columns.map((column) => renderColumn$1(column));
|
|
74
|
+
const constraintDefs = node.constraints !== void 0 ? node.constraints.map(renderTableConstraint) : [];
|
|
75
|
+
return `CREATE TABLE ${ifNotExists}${tableRef} (\n ${[...columnDefs, ...constraintDefs].join(",\n ")}\n)`;
|
|
76
|
+
}
|
|
77
|
+
createSchema(node) {
|
|
78
|
+
return `CREATE SCHEMA ${node.ifNotExists ? "IF NOT EXISTS " : ""}${quoteIdentifier(node.schema)}`;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
function isTextLikeNativeType(nativeType) {
|
|
82
|
+
return nativeType === "text" || nativeType === "varchar" || nativeType.startsWith("varchar(") || nativeType === "character varying" || nativeType.startsWith("character varying(") || nativeType === "char" || nativeType.startsWith("char(") || nativeType === "character" || nativeType.startsWith("character(");
|
|
83
|
+
}
|
|
84
|
+
const defaultVisitor = {
|
|
85
|
+
literal(node, ctx) {
|
|
86
|
+
const { value } = node;
|
|
87
|
+
if (typeof value === "number" || typeof value === "boolean") return `DEFAULT ${String(value)}`;
|
|
88
|
+
if (value === null) return "DEFAULT NULL";
|
|
89
|
+
const literal = `'${escapeLiteral(typeof value === "string" ? value : JSON.stringify(value))}'`;
|
|
90
|
+
return isTextLikeNativeType(ctx.nativeType) ? `DEFAULT ${literal}` : `DEFAULT ${literal}::${ctx.nativeType}`;
|
|
91
|
+
},
|
|
92
|
+
function(node, _ctx) {
|
|
93
|
+
if (node.expression === "autoincrement()") return "";
|
|
94
|
+
return `DEFAULT (${node.expression})`;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
function renderColumn$1(column) {
|
|
98
|
+
const parts = [quoteIdentifier(column.name), column.type];
|
|
99
|
+
if (column.notNull) parts.push("NOT NULL");
|
|
100
|
+
if (column.primaryKey) parts.push("PRIMARY KEY");
|
|
101
|
+
const defaultClause = column.default ? column.default.accept(defaultVisitor, { nativeType: column.type }) : "";
|
|
102
|
+
if (defaultClause.length > 0) parts.push(defaultClause);
|
|
103
|
+
return parts.join(" ");
|
|
104
|
+
}
|
|
105
|
+
function renderLoweredDdl(ast) {
|
|
106
|
+
const sql = ast.accept(new PostgresDdlVisitorImpl());
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
sql,
|
|
109
|
+
params: Object.freeze([])
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/core/enum-control-hooks.ts
|
|
114
|
+
const ENUM_INTROSPECT_QUERY = `
|
|
115
|
+
SELECT
|
|
116
|
+
n.nspname AS schema_name,
|
|
117
|
+
t.typname AS type_name,
|
|
118
|
+
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS values
|
|
119
|
+
FROM pg_type t
|
|
120
|
+
JOIN pg_namespace n ON t.typnamespace = n.oid
|
|
121
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
122
|
+
WHERE n.nspname = $1
|
|
123
|
+
GROUP BY n.nspname, t.typname
|
|
124
|
+
ORDER BY n.nspname, t.typname
|
|
125
|
+
`;
|
|
126
|
+
function isStringArray(value) {
|
|
127
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Parses a PostgreSQL array value into a JavaScript string array.
|
|
131
|
+
*
|
|
132
|
+
* The `pg` library returns `array_agg` results either as a JS array
|
|
133
|
+
* (when type parsers are configured) or as a string in PostgreSQL array
|
|
134
|
+
* literal format (`{value1,value2,...}`). Handles PG's quoting rules:
|
|
135
|
+
* - Elements containing commas, quotes, backslashes, or whitespace are
|
|
136
|
+
* double-quoted.
|
|
137
|
+
* - Inside quoted elements, `\"` represents `"` and `\\` represents `\`.
|
|
138
|
+
*
|
|
139
|
+
* Returns `null` when the input cannot be parsed as a PG array.
|
|
140
|
+
*/
|
|
141
|
+
function parsePostgresArray(value) {
|
|
142
|
+
if (isStringArray(value)) return value;
|
|
143
|
+
if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
|
|
144
|
+
const inner = value.slice(1, -1);
|
|
145
|
+
if (inner === "") return [];
|
|
146
|
+
return parseArrayElements(inner);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function parseArrayElements(input) {
|
|
151
|
+
const result = [];
|
|
152
|
+
let i = 0;
|
|
153
|
+
while (i < input.length) {
|
|
154
|
+
if (input[i] === ",") {
|
|
155
|
+
i++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (input[i] === "\"") {
|
|
159
|
+
i++;
|
|
160
|
+
let element = "";
|
|
161
|
+
while (i < input.length && input[i] !== "\"") {
|
|
162
|
+
if (input[i] === "\\" && i + 1 < input.length) {
|
|
163
|
+
i++;
|
|
164
|
+
element += input[i];
|
|
165
|
+
} else element += input[i];
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
i++;
|
|
169
|
+
result.push(element);
|
|
170
|
+
} else {
|
|
171
|
+
const nextComma = input.indexOf(",", i);
|
|
172
|
+
if (nextComma === -1) {
|
|
173
|
+
result.push(input.slice(i).trim());
|
|
174
|
+
i = input.length;
|
|
175
|
+
} else {
|
|
176
|
+
result.push(input.slice(i, nextComma).trim());
|
|
177
|
+
i = nextComma;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reads enum types from the live Postgres schema and returns them in
|
|
185
|
+
* the codec-typed annotation shape consumed by `control-adapter.ts`
|
|
186
|
+
* (which writes them under `schema.annotations.pg.storageTypes`).
|
|
187
|
+
*/
|
|
188
|
+
async function introspectPostgresEnumTypes(options) {
|
|
189
|
+
const namespace = options.schemaName ?? "public";
|
|
190
|
+
const result = await options.driver.query(ENUM_INTROSPECT_QUERY, [namespace]);
|
|
191
|
+
const types = {};
|
|
192
|
+
for (const row of result.rows) {
|
|
193
|
+
const values = parsePostgresArray(row.values);
|
|
194
|
+
if (!values) throw new Error(`Failed to parse enum values for type "${row.type_name}": unexpected format: ${JSON.stringify(row.values)}`);
|
|
195
|
+
types[row.type_name] = {
|
|
196
|
+
codecId: PG_ENUM_CODEC_ID,
|
|
197
|
+
nativeType: row.type_name,
|
|
198
|
+
typeParams: { values }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return types;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/core/marker-ledger.ts
|
|
205
|
+
const CONTROL_CODECS = createAstCodecRegistry(postgresCodecRegistry);
|
|
206
|
+
const marker = pgTable({
|
|
207
|
+
name: "marker",
|
|
208
|
+
schema: "prisma_contract"
|
|
209
|
+
}, {
|
|
210
|
+
space: text(),
|
|
211
|
+
core_hash: text(),
|
|
212
|
+
profile_hash: text(),
|
|
213
|
+
contract_json: jsonb({ nullable: true }),
|
|
214
|
+
canonical_version: int4({ nullable: true }),
|
|
215
|
+
updated_at: timestamptz(),
|
|
216
|
+
app_tag: text({ nullable: true }),
|
|
217
|
+
meta: jsonb({ nullable: true }),
|
|
218
|
+
invariants: textArray()
|
|
219
|
+
});
|
|
220
|
+
/**
|
|
221
|
+
* Writeable subset of the `prisma_contract.ledger` table. Omits the
|
|
222
|
+
* DB-generated `id` (bigserial) and `created_at` (default `now()`) so the
|
|
223
|
+
* insert path doesn't have to pass them.
|
|
224
|
+
*/
|
|
225
|
+
const ledger = pgTable({
|
|
226
|
+
name: "ledger",
|
|
227
|
+
schema: "prisma_contract"
|
|
228
|
+
}, {
|
|
229
|
+
space: text(),
|
|
230
|
+
migration_name: text(),
|
|
231
|
+
migration_hash: text(),
|
|
232
|
+
origin_core_hash: text({ nullable: true }),
|
|
233
|
+
destination_core_hash: text(),
|
|
234
|
+
operations: jsonb()
|
|
235
|
+
});
|
|
236
|
+
/**
|
|
237
|
+
* Read-side handle covering every column of `prisma_contract.ledger`,
|
|
238
|
+
* including the DB-generated `id` (for ORDER BY) and `created_at`.
|
|
239
|
+
*/
|
|
240
|
+
const ledgerReadShape = pgTable({
|
|
241
|
+
name: "ledger",
|
|
242
|
+
schema: "prisma_contract"
|
|
243
|
+
}, {
|
|
244
|
+
id: int8(),
|
|
245
|
+
space: text(),
|
|
246
|
+
migration_name: text(),
|
|
247
|
+
migration_hash: text(),
|
|
248
|
+
origin_core_hash: text({ nullable: true }),
|
|
249
|
+
destination_core_hash: text(),
|
|
250
|
+
operations: jsonb(),
|
|
251
|
+
created_at: timestamptz()
|
|
252
|
+
});
|
|
253
|
+
const infoSchemaTables = pgTable({
|
|
254
|
+
name: "tables",
|
|
255
|
+
schema: "information_schema"
|
|
256
|
+
}, {
|
|
257
|
+
table_schema: text(),
|
|
258
|
+
table_name: text()
|
|
259
|
+
});
|
|
260
|
+
const NOW = new RawExpr({
|
|
261
|
+
parts: ["now()"],
|
|
262
|
+
returns: {
|
|
263
|
+
codecId: PG_TIMESTAMPTZ_CODEC_ID,
|
|
264
|
+
nullable: false
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
function mergeInvariants(current, incoming) {
|
|
268
|
+
return [...new Set([...current, ...incoming])].sort();
|
|
269
|
+
}
|
|
270
|
+
async function execute(lower, driver, query) {
|
|
271
|
+
const lowered = lower(query);
|
|
272
|
+
const encoded = await encodeParamsWithMetadata(lowered.params.map((slot) => {
|
|
273
|
+
if (slot.kind === "literal") return slot.value;
|
|
274
|
+
throw new Error("Postgres control DML lowered to a bind parameter, which is unsupported");
|
|
275
|
+
}), deriveParamMetadata(query), {}, CONTROL_CODECS);
|
|
276
|
+
return (await driver.query(lowered.sql, encoded)).rows;
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/core/sql-renderer.ts
|
|
280
|
+
/**
|
|
281
|
+
* Postgres native types whose unknown-OID parameter inference is reliable in arbitrary expression positions. Parameters bound to a codec whose `meta.db.sql.postgres.nativeType` falls in this set are emitted as plain `$N`; everything else (including `json`, `jsonb`, extension types like `vector`, and unknown user types) is emitted as `$N::<nativeType>` so the planner picks an unambiguous overload.
|
|
282
|
+
*
|
|
283
|
+
* `json` / `jsonb` are intentionally excluded despite being Postgres builtins: their operator overloads make context inference unreliable in expression positions (e.g. `$1 -> 'key'` is ambiguous between the two).
|
|
284
|
+
*
|
|
285
|
+
* Spellings match the on-disk `meta.db.sql.postgres.nativeType` values in `@prisma-next/target-postgres`'s codec definitions, not the `udt_name` abbreviations that ADR 205 used as illustrative shorthand. The lookup-based cast policy compares against these strings directly.
|
|
286
|
+
*/
|
|
287
|
+
const POSTGRES_INFERRABLE_NATIVE_TYPES = new Set([
|
|
288
|
+
"integer",
|
|
289
|
+
"smallint",
|
|
290
|
+
"bigint",
|
|
291
|
+
"real",
|
|
292
|
+
"double precision",
|
|
293
|
+
"numeric",
|
|
294
|
+
"boolean",
|
|
295
|
+
"text",
|
|
296
|
+
"character",
|
|
297
|
+
"character varying",
|
|
298
|
+
"timestamp",
|
|
299
|
+
"timestamp without time zone",
|
|
300
|
+
"timestamp with time zone",
|
|
301
|
+
"time",
|
|
302
|
+
"timetz",
|
|
303
|
+
"interval",
|
|
304
|
+
"bit",
|
|
305
|
+
"bit varying"
|
|
306
|
+
]);
|
|
307
|
+
function renderTypedParam(index, codecId, codecLookup) {
|
|
308
|
+
if (codecId === void 0) return `$${index}`;
|
|
309
|
+
const meta = codecLookup.metaFor(codecId);
|
|
310
|
+
if (!(codecLookup.get(codecId) !== void 0 || meta !== void 0 || codecLookup.targetTypesFor(codecId) !== void 0)) throw new Error(`Postgres lowering: ParamRef carries codecId "${codecId}" but the assembled codec lookup has no entry for it. This usually indicates a missing extension pack in the runtime stack — register the pack that contributes this codec (e.g. \`extensionPacks: [pgvectorRuntime]\`), or use the codec directly from \`@prisma-next/target-postgres/codecs\` if it's a builtin.`);
|
|
311
|
+
const dbRecord = meta?.db;
|
|
312
|
+
const sqlBlock = isRecord(dbRecord) ? dbRecord["sql"] : void 0;
|
|
313
|
+
const dialectBlock = isRecord(sqlBlock) ? sqlBlock["postgres"] : void 0;
|
|
314
|
+
const nativeType = isRecord(dialectBlock) ? dialectBlock["nativeType"] : void 0;
|
|
315
|
+
if (typeof nativeType === "string" && !POSTGRES_INFERRABLE_NATIVE_TYPES.has(nativeType)) return `$${index}::${nativeType}`;
|
|
316
|
+
return `$${index}`;
|
|
317
|
+
}
|
|
318
|
+
function isRecord(value) {
|
|
319
|
+
return typeof value === "object" && value !== null;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Render a SQL query AST to a Postgres-flavored `{ sql, params }` payload.
|
|
323
|
+
*
|
|
324
|
+
* Shared between the runtime (`PostgresAdapterImpl.lower`) and control (`PostgresControlAdapter.lower`) entrypoints so emit-time and run-time paths produce byte-identical output for the same AST.
|
|
325
|
+
*/
|
|
326
|
+
function renderLoweredSql(ast, contract, codecLookup) {
|
|
327
|
+
const orderedRefs = collectOrderedParamRefs(ast);
|
|
328
|
+
const indexMap = /* @__PURE__ */ new Map();
|
|
329
|
+
const params = orderedRefs.map((ref, i) => {
|
|
330
|
+
indexMap.set(ref, i + 1);
|
|
331
|
+
return ref.kind === "prepared-param-ref" ? {
|
|
332
|
+
kind: "bind",
|
|
333
|
+
name: ref.name
|
|
334
|
+
} : {
|
|
335
|
+
kind: "literal",
|
|
336
|
+
value: ref.value
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
const pim = {
|
|
340
|
+
indexMap,
|
|
341
|
+
codecLookup
|
|
342
|
+
};
|
|
343
|
+
const node = ast;
|
|
344
|
+
let sql;
|
|
345
|
+
switch (node.kind) {
|
|
346
|
+
case "select":
|
|
347
|
+
sql = renderSelect(node, contract, pim);
|
|
348
|
+
break;
|
|
349
|
+
case "insert":
|
|
350
|
+
sql = renderInsert(node, contract, pim);
|
|
351
|
+
break;
|
|
352
|
+
case "update":
|
|
353
|
+
sql = renderUpdate(node, contract, pim);
|
|
354
|
+
break;
|
|
355
|
+
case "delete":
|
|
356
|
+
sql = renderDelete(node, contract, pim);
|
|
357
|
+
break;
|
|
358
|
+
case "raw-sql":
|
|
359
|
+
sql = renderRawSql(node, contract, pim);
|
|
360
|
+
break;
|
|
361
|
+
// v8 ignore next 4
|
|
362
|
+
default: throw new Error(`Unsupported AST node kind: ${node.kind}`);
|
|
363
|
+
}
|
|
364
|
+
return Object.freeze({
|
|
365
|
+
sql,
|
|
366
|
+
params: Object.freeze(params)
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
function renderLimitOffset(keyword, value, contract, pim) {
|
|
370
|
+
if (value === void 0) return "";
|
|
371
|
+
if (typeof value === "number") return `${keyword} ${value}`;
|
|
372
|
+
return `${keyword} ${renderExpr(value, contract, pim)}`;
|
|
373
|
+
}
|
|
374
|
+
function renderSelect(ast, contract, pim) {
|
|
375
|
+
return [
|
|
376
|
+
`SELECT ${renderDistinctPrefix(ast.distinct, ast.distinctOn, contract, pim)}${renderProjection(ast.projection, contract, pim)}`,
|
|
377
|
+
`FROM ${renderSource(ast.from, contract, pim)}`,
|
|
378
|
+
ast.joins?.length ? ast.joins.map((join) => renderJoin(join, contract, pim)).join(" ") : "",
|
|
379
|
+
ast.where ? `WHERE ${renderWhere(ast.where, contract, pim)}` : "",
|
|
380
|
+
ast.groupBy?.length ? `GROUP BY ${ast.groupBy.map((expr) => renderExpr(expr, contract, pim)).join(", ")}` : "",
|
|
381
|
+
ast.having ? `HAVING ${renderWhere(ast.having, contract, pim)}` : "",
|
|
382
|
+
ast.orderBy?.length ? `ORDER BY ${ast.orderBy.map((order) => {
|
|
383
|
+
return `${renderExpr(order.expr, contract, pim)} ${order.dir.toUpperCase()}`;
|
|
384
|
+
}).join(", ")}` : "",
|
|
385
|
+
renderLimitOffset("LIMIT", ast.limit, contract, pim),
|
|
386
|
+
renderLimitOffset("OFFSET", ast.offset, contract, pim)
|
|
387
|
+
].filter((part) => part.length > 0).join(" ").trim();
|
|
388
|
+
}
|
|
389
|
+
function renderProjection(projection, contract, pim) {
|
|
390
|
+
return projection.map((item) => {
|
|
391
|
+
const alias = quoteIdentifier(item.alias);
|
|
392
|
+
if (item.expr.kind === "literal") return `${renderLiteral(item.expr)} AS ${alias}`;
|
|
393
|
+
return `${renderExpr(item.expr, contract, pim)} AS ${alias}`;
|
|
394
|
+
}).join(", ");
|
|
395
|
+
}
|
|
396
|
+
function renderReturning(items, contract, pim) {
|
|
397
|
+
return items.map((item) => {
|
|
398
|
+
if (item.expr.kind === "column-ref") {
|
|
399
|
+
const rendered = renderColumn(item.expr);
|
|
400
|
+
return item.expr.column === item.alias ? rendered : `${rendered} AS ${quoteIdentifier(item.alias)}`;
|
|
401
|
+
}
|
|
402
|
+
if (item.expr.kind === "literal") return `${renderLiteral(item.expr)} AS ${quoteIdentifier(item.alias)}`;
|
|
403
|
+
return `${renderExpr(item.expr, contract, pim)} AS ${quoteIdentifier(item.alias)}`;
|
|
404
|
+
}).join(", ");
|
|
405
|
+
}
|
|
406
|
+
function renderDistinctPrefix(distinct, distinctOn, contract, pim) {
|
|
407
|
+
if (distinctOn && distinctOn.length > 0) return `DISTINCT ON (${distinctOn.map((expr) => renderExpr(expr, contract, pim)).join(", ")}) `;
|
|
408
|
+
if (distinct) return "DISTINCT ";
|
|
409
|
+
return "";
|
|
410
|
+
}
|
|
411
|
+
function qualifyTableFromNamespaceCoordinate(table, contract) {
|
|
412
|
+
if (table instanceof PostgresTableSource && table.schema !== void 0) return `${quoteIdentifier(table.schema)}.${quoteIdentifier(table.name)}`;
|
|
413
|
+
if (table.namespaceId === void 0) return quoteIdentifier(table.name);
|
|
414
|
+
const namespace = contract.storage.namespaces[table.namespaceId];
|
|
415
|
+
if (namespace === void 0) throw new Error(`Table "${table.name}" references namespace "${table.namespaceId}" which is not present as a Postgres schema on the contract`);
|
|
416
|
+
const qualifyTable = namespace.qualifyTable;
|
|
417
|
+
if (qualifyTable === void 0) throw new Error(`Table "${table.name}" references namespace "${table.namespaceId}" which is not materialised as a Postgres schema on the contract`);
|
|
418
|
+
return qualifyTable.call(namespace, table.name);
|
|
419
|
+
}
|
|
420
|
+
function renderTableSource(source, contract) {
|
|
421
|
+
const qualified = qualifyTableFromNamespaceCoordinate(source, contract);
|
|
422
|
+
if (!source.alias) return qualified;
|
|
423
|
+
return `${qualified} AS ${quoteIdentifier(source.alias)}`;
|
|
424
|
+
}
|
|
425
|
+
function renderSource(source, contract, pim) {
|
|
426
|
+
const node = source;
|
|
427
|
+
switch (node.kind) {
|
|
428
|
+
case "table-source": return renderTableSource(node, contract);
|
|
429
|
+
case "derived-table-source": return `(${renderSelect(node.query, contract, pim)}) AS ${quoteIdentifier(node.alias)}`;
|
|
430
|
+
// v8 ignore next 4
|
|
431
|
+
default: throw new Error(`Unsupported source node kind: ${node.kind}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function assertScalarSubquery(query) {
|
|
435
|
+
if (query.projection.length !== 1) throw new Error("Subquery expressions must project exactly one column");
|
|
436
|
+
}
|
|
437
|
+
function renderSubqueryExpr(expr, contract, pim) {
|
|
438
|
+
assertScalarSubquery(expr.query);
|
|
439
|
+
return `(${renderSelect(expr.query, contract, pim)})`;
|
|
440
|
+
}
|
|
441
|
+
function renderWhere(expr, contract, pim) {
|
|
442
|
+
return renderExpr(expr, contract, pim);
|
|
443
|
+
}
|
|
444
|
+
function renderNullCheck(expr, contract, pim) {
|
|
445
|
+
const rendered = renderExpr(expr.expr, contract, pim);
|
|
446
|
+
const renderedExpr = isAtomicExpressionKind(expr.expr.kind) ? rendered : `(${rendered})`;
|
|
447
|
+
return expr.isNull ? `${renderedExpr} IS NULL` : `${renderedExpr} IS NOT NULL`;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Atomic expression kinds whose rendered SQL is already self-delimited (a column reference, parameter, literal, function call, aggregate, etc.) and therefore does not need surrounding parentheses when used as the left operand of a postfix predicate like `IS NULL` or `IS NOT NULL`, or as either operand of a binary infix operator.
|
|
451
|
+
*
|
|
452
|
+
* Anything not in this set is treated as composite (binary, AND/OR/NOT, EXISTS, nested IS NULL, subqueries, operation templates) and gets wrapped to preserve grouping.
|
|
453
|
+
*/
|
|
454
|
+
function isAtomicExpressionKind(kind) {
|
|
455
|
+
switch (kind) {
|
|
456
|
+
case "column-ref":
|
|
457
|
+
case "identifier-ref":
|
|
458
|
+
case "param-ref":
|
|
459
|
+
case "prepared-param-ref":
|
|
460
|
+
case "literal":
|
|
461
|
+
case "aggregate":
|
|
462
|
+
case "window-func":
|
|
463
|
+
case "json-object":
|
|
464
|
+
case "json-array-agg":
|
|
465
|
+
case "list": return true;
|
|
466
|
+
case "subquery":
|
|
467
|
+
case "operation":
|
|
468
|
+
case "binary":
|
|
469
|
+
case "and":
|
|
470
|
+
case "or":
|
|
471
|
+
case "exists":
|
|
472
|
+
case "null-check":
|
|
473
|
+
case "not":
|
|
474
|
+
case "raw-expr": return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function renderBinary(expr, contract, pim) {
|
|
478
|
+
if (expr.right.kind === "list" && expr.right.values.length === 0) {
|
|
479
|
+
if (expr.op === "in") return "FALSE";
|
|
480
|
+
if (expr.op === "notIn") return "TRUE";
|
|
481
|
+
}
|
|
482
|
+
const leftExpr = expr.left;
|
|
483
|
+
const left = renderExpr(leftExpr, contract, pim);
|
|
484
|
+
const leftRendered = leftExpr.kind === "operation" || leftExpr.kind === "subquery" ? `(${left})` : left;
|
|
485
|
+
const rightNode = expr.right;
|
|
486
|
+
let right;
|
|
487
|
+
switch (rightNode.kind) {
|
|
488
|
+
case "list":
|
|
489
|
+
right = renderListLiteral(rightNode, contract, pim);
|
|
490
|
+
break;
|
|
491
|
+
case "literal":
|
|
492
|
+
right = renderLiteral(rightNode);
|
|
493
|
+
break;
|
|
494
|
+
case "column-ref":
|
|
495
|
+
right = renderColumn(rightNode);
|
|
496
|
+
break;
|
|
497
|
+
case "param-ref":
|
|
498
|
+
case "prepared-param-ref":
|
|
499
|
+
right = renderParamRef(rightNode, pim);
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
right = renderExpr(rightNode, contract, pim);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
return `${leftRendered} ${{
|
|
506
|
+
eq: "=",
|
|
507
|
+
neq: "!=",
|
|
508
|
+
gt: ">",
|
|
509
|
+
lt: "<",
|
|
510
|
+
gte: ">=",
|
|
511
|
+
lte: "<=",
|
|
512
|
+
like: "LIKE",
|
|
513
|
+
in: "IN",
|
|
514
|
+
notIn: "NOT IN"
|
|
515
|
+
}[expr.op]} ${right}`;
|
|
516
|
+
}
|
|
517
|
+
function renderListLiteral(expr, contract, pim) {
|
|
518
|
+
if (expr.values.length === 0) return "(NULL)";
|
|
519
|
+
return `(${expr.values.map((v) => {
|
|
520
|
+
if (v.kind === "param-ref" || v.kind === "prepared-param-ref") return renderParamRef(v, pim);
|
|
521
|
+
if (v.kind === "literal") return renderLiteral(v);
|
|
522
|
+
return renderExpr(v, contract, pim);
|
|
523
|
+
}).join(", ")})`;
|
|
524
|
+
}
|
|
525
|
+
function renderColumn(ref) {
|
|
526
|
+
if (ref.table === "excluded") return `excluded.${quoteIdentifier(ref.column)}`;
|
|
527
|
+
return `${quoteIdentifier(ref.table)}.${quoteIdentifier(ref.column)}`;
|
|
528
|
+
}
|
|
529
|
+
function renderAggregateExpr(expr, contract, pim) {
|
|
530
|
+
const fn = expr.fn.toUpperCase();
|
|
531
|
+
if (!expr.expr) return `${fn}(*)`;
|
|
532
|
+
return `${fn}(${renderExpr(expr.expr, contract, pim)})`;
|
|
533
|
+
}
|
|
534
|
+
function renderWindowFuncExpr(expr, contract, pim) {
|
|
535
|
+
return `${expr.fn.toUpperCase()}(${expr.args.map((arg) => renderExpr(arg, contract, pim)).join(", ")}) OVER (${[expr.partitionBy && expr.partitionBy.length > 0 ? `PARTITION BY ${expr.partitionBy.map((e) => renderExpr(e, contract, pim)).join(", ")}` : "", expr.orderBy && expr.orderBy.length > 0 ? `ORDER BY ${renderOrderByItems(expr.orderBy, contract, pim)}` : ""].filter((part) => part.length > 0).join(" ")})`;
|
|
536
|
+
}
|
|
537
|
+
function renderJsonObjectExpr(expr, contract, pim) {
|
|
538
|
+
return `json_build_object(${expr.entries.flatMap((entry) => {
|
|
539
|
+
const key = `'${escapeLiteral(entry.key)}'`;
|
|
540
|
+
if (entry.value.kind === "literal") return [key, renderLiteral(entry.value)];
|
|
541
|
+
return [key, renderExpr(entry.value, contract, pim)];
|
|
542
|
+
}).join(", ")})`;
|
|
543
|
+
}
|
|
544
|
+
function renderOrderByItems(items, contract, pim) {
|
|
545
|
+
return items.map((item) => `${renderExpr(item.expr, contract, pim)} ${item.dir.toUpperCase()}`).join(", ");
|
|
546
|
+
}
|
|
547
|
+
function renderJsonArrayAggExpr(expr, contract, pim) {
|
|
548
|
+
const aggregateOrderBy = expr.orderBy && expr.orderBy.length > 0 ? ` ORDER BY ${renderOrderByItems(expr.orderBy, contract, pim)}` : "";
|
|
549
|
+
const aggregated = `json_agg(${renderExpr(expr.expr, contract, pim)}${aggregateOrderBy})`;
|
|
550
|
+
if (expr.onEmpty === "emptyArray") return `coalesce(${aggregated}, json_build_array())`;
|
|
551
|
+
return aggregated;
|
|
552
|
+
}
|
|
553
|
+
function renderExpr(expr, contract, pim) {
|
|
554
|
+
const node = expr;
|
|
555
|
+
switch (node.kind) {
|
|
556
|
+
case "column-ref": return renderColumn(node);
|
|
557
|
+
case "identifier-ref": return quoteIdentifier(node.name);
|
|
558
|
+
case "operation": return renderOperation(node, contract, pim);
|
|
559
|
+
case "subquery": return renderSubqueryExpr(node, contract, pim);
|
|
560
|
+
case "aggregate": return renderAggregateExpr(node, contract, pim);
|
|
561
|
+
case "window-func": return renderWindowFuncExpr(node, contract, pim);
|
|
562
|
+
case "json-object": return renderJsonObjectExpr(node, contract, pim);
|
|
563
|
+
case "json-array-agg": return renderJsonArrayAggExpr(node, contract, pim);
|
|
564
|
+
case "binary": return renderBinary(node, contract, pim);
|
|
565
|
+
case "and":
|
|
566
|
+
if (node.exprs.length === 0) return "TRUE";
|
|
567
|
+
return `(${node.exprs.map((part) => renderExpr(part, contract, pim)).join(" AND ")})`;
|
|
568
|
+
case "or":
|
|
569
|
+
if (node.exprs.length === 0) return "FALSE";
|
|
570
|
+
return `(${node.exprs.map((part) => renderExpr(part, contract, pim)).join(" OR ")})`;
|
|
571
|
+
case "exists": return `${node.notExists ? "NOT " : ""}EXISTS (${renderSelect(node.subquery, contract, pim)})`;
|
|
572
|
+
case "null-check": return renderNullCheck(node, contract, pim);
|
|
573
|
+
case "not": return `NOT (${renderExpr(node.expr, contract, pim)})`;
|
|
574
|
+
case "param-ref":
|
|
575
|
+
case "prepared-param-ref": return renderParamRef(node, pim);
|
|
576
|
+
case "literal": return renderLiteral(node);
|
|
577
|
+
case "list": return renderListLiteral(node, contract, pim);
|
|
578
|
+
case "raw-expr": return renderRawExpr(node, contract, pim);
|
|
579
|
+
// v8 ignore next 4
|
|
580
|
+
default: throw new Error(`Unsupported expression node kind: ${node.kind}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function renderParamRef(ref, pim) {
|
|
584
|
+
const index = pim.indexMap.get(ref);
|
|
585
|
+
if (index === void 0) throw new Error("ParamRef not found in index map");
|
|
586
|
+
if (ref.kind === "prepared-param-ref") return renderTypedParam(index, ref.codec.codecId, pim.codecLookup);
|
|
587
|
+
if (ref.codec === void 0) throw runtimeError("RUNTIME.PARAM_REF_MISSING_CODEC", "Postgres renderer: ParamRef reached lowering without a bound CodecRef. Every column-bound ParamRef must carry a codec under the AST-bound codec contract. This usually indicates a builder path that constructed a ParamRef without threading the column codec.", {
|
|
588
|
+
paramIndex: index,
|
|
589
|
+
...ifDefined("name", ref.name)
|
|
590
|
+
});
|
|
591
|
+
return renderTypedParam(index, ref.codec.codecId, pim.codecLookup);
|
|
592
|
+
}
|
|
593
|
+
function renderLiteral(expr) {
|
|
594
|
+
if (typeof expr.value === "string") return `'${escapeLiteral(expr.value)}'`;
|
|
595
|
+
if (typeof expr.value === "number" || typeof expr.value === "boolean") return String(expr.value);
|
|
596
|
+
if (typeof expr.value === "bigint") return String(expr.value);
|
|
597
|
+
if (expr.value === null) return "NULL";
|
|
598
|
+
if (expr.value === void 0) return "NULL";
|
|
599
|
+
if (expr.value instanceof Date) return `'${escapeLiteral(expr.value.toISOString())}'`;
|
|
600
|
+
if (Array.isArray(expr.value)) return `ARRAY[${expr.value.map((v) => renderLiteral(new LiteralExpr(v))).join(", ")}]`;
|
|
601
|
+
const json = JSON.stringify(expr.value);
|
|
602
|
+
if (json === void 0) return "NULL";
|
|
603
|
+
return `'${escapeLiteral(json)}'`;
|
|
604
|
+
}
|
|
605
|
+
function renderOperation(expr, contract, pim) {
|
|
606
|
+
const self = renderExpr(expr.self, contract, pim);
|
|
607
|
+
const args = expr.args.map((arg) => {
|
|
608
|
+
return renderExpr(arg, contract, pim);
|
|
609
|
+
});
|
|
610
|
+
return expr.lowering.template.replace(/\{\{self\}\}|\{\{arg(\d+)\}\}/g, (token, argIndex) => {
|
|
611
|
+
if (token === "{{self}}") return self;
|
|
612
|
+
const arg = args[Number(argIndex)];
|
|
613
|
+
if (arg === void 0) throw new Error(`Operation lowering template for "${expr.method}" referenced missing argument {{arg${argIndex}}}; template has ${args.length} arg(s)`);
|
|
614
|
+
return arg;
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function renderJoin(join, contract, pim) {
|
|
618
|
+
return `${join.joinType.toUpperCase()} JOIN ${join.lateral ? "LATERAL " : ""}${renderSource(join.source, contract, pim)} ON ${renderJoinOn(join.on, contract, pim)}`;
|
|
619
|
+
}
|
|
620
|
+
function renderJoinOn(on, contract, pim) {
|
|
621
|
+
if (on.kind === "eq-col-join-on") return `${renderColumn(on.left)} = ${renderColumn(on.right)}`;
|
|
622
|
+
return renderWhere(on, contract, pim);
|
|
623
|
+
}
|
|
624
|
+
function getInsertColumnOrder(rows, contract, tableRef) {
|
|
625
|
+
const tableName = tableRef.name;
|
|
626
|
+
const orderedColumns = [];
|
|
627
|
+
const seenColumns = /* @__PURE__ */ new Set();
|
|
628
|
+
for (const row of rows) for (const column of Object.keys(row)) {
|
|
629
|
+
if (seenColumns.has(column)) continue;
|
|
630
|
+
seenColumns.add(column);
|
|
631
|
+
orderedColumns.push(column);
|
|
632
|
+
}
|
|
633
|
+
if (orderedColumns.length > 0) return orderedColumns;
|
|
634
|
+
let table;
|
|
635
|
+
if (tableRef.namespaceId !== void 0) table = contract.storage.namespaces[tableRef.namespaceId]?.entries.table[tableName];
|
|
636
|
+
if (table === void 0) for (const ns of Object.values(contract.storage.namespaces)) {
|
|
637
|
+
const found = ns.entries.table[tableName];
|
|
638
|
+
if (found !== void 0) {
|
|
639
|
+
table = found;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (!table) throw new Error(`INSERT target table not found in contract storage: ${tableName}`);
|
|
644
|
+
return Object.keys(table.columns);
|
|
645
|
+
}
|
|
646
|
+
function renderInsertValue(value, contract, pim) {
|
|
647
|
+
if (!value || value.kind === "default-value") return "DEFAULT";
|
|
648
|
+
switch (value.kind) {
|
|
649
|
+
case "param-ref":
|
|
650
|
+
case "prepared-param-ref": return renderParamRef(value, pim);
|
|
651
|
+
case "column-ref": return renderColumn(value);
|
|
652
|
+
case "raw-expr": return renderExpr(value, contract, pim);
|
|
653
|
+
// v8 ignore next 4
|
|
654
|
+
default: throw new Error(`Unsupported value node in INSERT: ${value.kind}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function renderInsert(ast, contract, pim) {
|
|
658
|
+
const table = qualifyTableFromNamespaceCoordinate(ast.table, contract);
|
|
659
|
+
const rows = ast.rows;
|
|
660
|
+
if (rows.length === 0) throw new Error("INSERT requires at least one row");
|
|
661
|
+
const hasExplicitValues = rows.some((row) => Object.keys(row).length > 0);
|
|
662
|
+
return `${(() => {
|
|
663
|
+
if (!hasExplicitValues) {
|
|
664
|
+
if (rows.length === 1) return `INSERT INTO ${table} DEFAULT VALUES`;
|
|
665
|
+
const defaultColumns = getInsertColumnOrder(rows, contract, ast.table);
|
|
666
|
+
if (defaultColumns.length === 0) return `INSERT INTO ${table} VALUES ${rows.map(() => "()").join(", ")}`;
|
|
667
|
+
const quotedColumns = defaultColumns.map((column) => quoteIdentifier(column));
|
|
668
|
+
const defaultRow = `(${defaultColumns.map(() => "DEFAULT").join(", ")})`;
|
|
669
|
+
return `INSERT INTO ${table} (${quotedColumns.join(", ")}) VALUES ${rows.map(() => defaultRow).join(", ")}`;
|
|
670
|
+
}
|
|
671
|
+
const columnOrder = getInsertColumnOrder(rows, contract, ast.table);
|
|
672
|
+
const columns = columnOrder.map((column) => quoteIdentifier(column));
|
|
673
|
+
const values = rows.map((row) => {
|
|
674
|
+
return `(${columnOrder.map((column) => renderInsertValue(row[column], contract, pim)).join(", ")})`;
|
|
675
|
+
}).join(", ");
|
|
676
|
+
return `INSERT INTO ${table} (${columns.join(", ")}) VALUES ${values}`;
|
|
677
|
+
})()}${ast.onConflict ? (() => {
|
|
678
|
+
const conflictColumns = ast.onConflict.columns.map((col) => quoteIdentifier(col.column));
|
|
679
|
+
if (conflictColumns.length === 0) throw new Error("INSERT onConflict requires at least one conflict column");
|
|
680
|
+
const action = ast.onConflict.action;
|
|
681
|
+
switch (action.kind) {
|
|
682
|
+
case "do-nothing": return ` ON CONFLICT (${conflictColumns.join(", ")}) DO NOTHING`;
|
|
683
|
+
case "do-update-set": {
|
|
684
|
+
const updateEntries = Object.entries(action.set);
|
|
685
|
+
if (updateEntries.length === 0) throw new Error("INSERT onConflict do-update-set requires at least one assignment");
|
|
686
|
+
const updates = updateEntries.map(([colName, value]) => {
|
|
687
|
+
return `${quoteIdentifier(colName)} = ${renderExpr(value, contract, pim)}`;
|
|
688
|
+
});
|
|
689
|
+
return ` ON CONFLICT (${conflictColumns.join(", ")}) DO UPDATE SET ${updates.join(", ")}`;
|
|
690
|
+
}
|
|
691
|
+
// v8 ignore next 4
|
|
692
|
+
default: throw new Error(`Unsupported onConflict action: ${action.kind}`);
|
|
693
|
+
}
|
|
694
|
+
})() : ""}${ast.returning?.length ? ` RETURNING ${renderReturning(ast.returning, contract, pim)}` : ""}`;
|
|
695
|
+
}
|
|
696
|
+
function renderUpdate(ast, contract, pim) {
|
|
697
|
+
const table = qualifyTableFromNamespaceCoordinate(ast.table, contract);
|
|
698
|
+
const setEntries = Object.entries(ast.set);
|
|
699
|
+
if (setEntries.length === 0) throw new Error("UPDATE requires at least one SET assignment");
|
|
700
|
+
const setClauses = setEntries.map(([col, val]) => {
|
|
701
|
+
return `${quoteIdentifier(col)} = ${renderExpr(val, contract, pim)}`;
|
|
702
|
+
});
|
|
703
|
+
const whereClause = ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : "";
|
|
704
|
+
const returningClause = ast.returning?.length ? ` RETURNING ${renderReturning(ast.returning, contract, pim)}` : "";
|
|
705
|
+
return `UPDATE ${table} SET ${setClauses.join(", ")}${whereClause}${returningClause}`;
|
|
706
|
+
}
|
|
707
|
+
function renderDelete(ast, contract, pim) {
|
|
708
|
+
return `DELETE FROM ${qualifyTableFromNamespaceCoordinate(ast.table, contract)}${ast.where ? ` WHERE ${renderWhere(ast.where, contract, pim)}` : ""}${ast.returning?.length ? ` RETURNING ${renderReturning(ast.returning, contract, pim)}` : ""}`;
|
|
709
|
+
}
|
|
710
|
+
function renderRawSql(ast, contract, pim) {
|
|
711
|
+
const out = [];
|
|
712
|
+
for (let i = 0; i < ast.fragments.length; i++) {
|
|
713
|
+
out.push(ast.fragments[i] ?? "");
|
|
714
|
+
if (i < ast.args.length) {
|
|
715
|
+
const arg = ast.args[i];
|
|
716
|
+
if (arg !== void 0) out.push(renderExpr(arg, contract, pim));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return out.join("");
|
|
720
|
+
}
|
|
721
|
+
function renderRawExpr(node, contract, pim) {
|
|
722
|
+
return node.parts.map((part) => typeof part === "string" ? part : renderExpr(part, contract, pim)).join("");
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/core/control-adapter.ts
|
|
726
|
+
const POSTGRES_MARKER_TABLE = "prisma_contract.marker";
|
|
727
|
+
const POSTGRES_LEDGER_TABLE = "prisma_contract.ledger";
|
|
728
|
+
/**
|
|
729
|
+
* Postgres control plane adapter for control-plane operations like introspection.
|
|
730
|
+
* Provides target-specific implementations for control-plane domain actions.
|
|
731
|
+
*/
|
|
732
|
+
var PostgresControlAdapter = class {
|
|
733
|
+
familyId = "sql";
|
|
734
|
+
targetId = "postgres";
|
|
735
|
+
codecLookup;
|
|
736
|
+
/**
|
|
737
|
+
* @param codecLookup - Codec lookup used by the SQL renderer to resolve
|
|
738
|
+
* per-codec metadata at lower-time. Defaults to a Postgres-builtins-only
|
|
739
|
+
* lookup when omitted. Stack-aware callers
|
|
740
|
+
* (`SqlControlAdapterDescriptor.create(stack)`) supply
|
|
741
|
+
* `stack.codecLookup` so extension codecs are visible to the renderer.
|
|
742
|
+
*/
|
|
743
|
+
constructor(codecLookup) {
|
|
744
|
+
this.codecLookup = codecLookup ?? createPostgresBuiltinCodecLookup();
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Target-specific normalizer for raw Postgres default expressions.
|
|
748
|
+
* Used by schema verification to normalize raw defaults before comparison.
|
|
749
|
+
*/
|
|
750
|
+
normalizeDefault = parsePostgresDefault;
|
|
751
|
+
/**
|
|
752
|
+
* Target-specific normalizer for Postgres schema native type names.
|
|
753
|
+
* Used by schema verification to normalize introspected type names
|
|
754
|
+
* before comparison with contract native types.
|
|
755
|
+
*/
|
|
756
|
+
normalizeNativeType = normalizeSchemaNativeType;
|
|
757
|
+
/**
|
|
758
|
+
* Bridges native `PostgresEnumStorageEntry` IR walks against the Postgres
|
|
759
|
+
* introspection shape (`schema.annotations.pg.storageTypes`). Lets
|
|
760
|
+
* the family-level schema verifier walk enum types without reaching
|
|
761
|
+
* into target-specific annotation layouts itself.
|
|
762
|
+
*/
|
|
763
|
+
resolveExistingEnumValues = (schema, enumType, namespaceId) => {
|
|
764
|
+
return readExistingEnumValues(schema, namespaceId === UNBOUND_NAMESPACE_ID ? readPostgresSchemaIrAnnotations(schema).schema ?? "public" : namespaceId, enumType.nativeType);
|
|
765
|
+
};
|
|
766
|
+
resolveExistingEnumValuesForContract = (contract) => createResolveExistingEnumValues(contract.storage);
|
|
767
|
+
bootstrapControlTableQueries() {
|
|
768
|
+
return buildControlTableBootstrapQueries();
|
|
769
|
+
}
|
|
770
|
+
bootstrapSignMarkerQueries() {
|
|
771
|
+
return buildSignMarkerBootstrapQueries();
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Lower a SQL query AST into a Postgres-flavored `{ sql, params }` payload.
|
|
775
|
+
*
|
|
776
|
+
* Delegates to the shared `renderLoweredSql` renderer so the control adapter
|
|
777
|
+
* emits byte-identical SQL to `PostgresAdapterImpl.lower()` for the same AST
|
|
778
|
+
* and contract. Used at migration plan/emit time (e.g. by `dataTransform`)
|
|
779
|
+
* without instantiating the runtime adapter.
|
|
780
|
+
*/
|
|
781
|
+
lower(ast, context) {
|
|
782
|
+
if (isDdlNode(ast)) return renderLoweredDdl(ast);
|
|
783
|
+
return renderLoweredSql(ast, context.contract, this.codecLookup);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Reads the contract marker from `prisma_contract.marker`. Probes
|
|
787
|
+
* `information_schema.tables` first so a fresh database (where the
|
|
788
|
+
* `prisma_contract` schema doesn't yet exist) returns `null` instead of a
|
|
789
|
+
* "relation does not exist" error — some Postgres wire-protocol clients
|
|
790
|
+
* (e.g. PGlite's TCP proxy) don't fully recover from extended-protocol
|
|
791
|
+
* parse errors, so we probe before reading.
|
|
792
|
+
*/
|
|
793
|
+
async readMarker(driver, space) {
|
|
794
|
+
const result = await this.readMarkerDiscriminated(driver, space);
|
|
795
|
+
return result.kind === "present" ? result.record : null;
|
|
796
|
+
}
|
|
797
|
+
async readMarkerDiscriminated(driver, space) {
|
|
798
|
+
return withMarkerReadErrorHandling(() => this.readMarkerResult(driver, space), {
|
|
799
|
+
space,
|
|
800
|
+
markerLocation: POSTGRES_MARKER_TABLE
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Reads every row from `prisma_contract.marker` and returns them keyed
|
|
805
|
+
* by `space`. Mirrors the existence probe in {@link readMarker}: a
|
|
806
|
+
* fresh database without the `prisma_contract` schema returns an empty
|
|
807
|
+
* map rather than raising "relation does not exist".
|
|
808
|
+
*/
|
|
809
|
+
async readAllMarkers(driver) {
|
|
810
|
+
return withMarkerReadErrorHandling(() => this.readAllMarkersResult(driver), {
|
|
811
|
+
space: APP_SPACE_ID,
|
|
812
|
+
markerLocation: POSTGRES_MARKER_TABLE
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
async readAllMarkersResult(driver) {
|
|
816
|
+
const lower = (query) => this.lower(query, { contract: void 0 });
|
|
817
|
+
if ((await execute(lower, driver, infoSchemaTables.select(infoSchemaTables.table_schema).where(infoSchemaTables.table_schema.eq("prisma_contract").and(infoSchemaTables.table_name.eq("marker"))).build())).length === 0) return /* @__PURE__ */ new Map();
|
|
818
|
+
await this.assertMarkerTableHasSpaceColumn(driver, APP_SPACE_ID);
|
|
819
|
+
const rows = blindCast(await execute(lower, driver, marker.select(marker.space, marker.core_hash, marker.profile_hash, marker.contract_json, marker.canonical_version, marker.updated_at, marker.app_tag, marker.meta, marker.invariants).build()));
|
|
820
|
+
const out = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const row of rows) out.set(row.space, parseMarkerRowSafely(row, parseContractMarkerRow, {
|
|
822
|
+
space: row.space,
|
|
823
|
+
markerLocation: POSTGRES_MARKER_TABLE
|
|
824
|
+
}));
|
|
825
|
+
return out;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Reads per-migration ledger rows from `prisma_contract.ledger` in apply
|
|
829
|
+
* order. Probes `information_schema.tables` first so a fresh database
|
|
830
|
+
* without the ledger table returns `[]` instead of raising "relation does
|
|
831
|
+
* not exist".
|
|
832
|
+
*/
|
|
833
|
+
async readLedger(driver, space) {
|
|
834
|
+
return withMarkerReadErrorHandling(() => this.readLedgerResult(driver, space), {
|
|
835
|
+
space: space ?? "*",
|
|
836
|
+
markerLocation: POSTGRES_LEDGER_TABLE
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
async readLedgerResult(driver, space) {
|
|
840
|
+
const lower = (query) => this.lower(query, { contract: void 0 });
|
|
841
|
+
if ((await execute(lower, driver, infoSchemaTables.select(infoSchemaTables.table_schema).where(infoSchemaTables.table_schema.eq("prisma_contract").and(infoSchemaTables.table_name.eq("ledger"))).build())).length === 0) return [];
|
|
842
|
+
const base = ledgerReadShape.select(ledgerReadShape.space, ledgerReadShape.migration_name, ledgerReadShape.migration_hash, ledgerReadShape.origin_core_hash, ledgerReadShape.destination_core_hash, ledgerReadShape.operations, ledgerReadShape.created_at);
|
|
843
|
+
return blindCast(await execute(lower, driver, (space !== void 0 ? base.where(ledgerReadShape.space.eq(space)) : base).orderBy(ledgerReadShape.id).build())).map((row) => {
|
|
844
|
+
const appliedAt = row.created_at instanceof Date ? row.created_at : new Date(row.created_at);
|
|
845
|
+
return {
|
|
846
|
+
space: row.space,
|
|
847
|
+
migrationName: row.migration_name,
|
|
848
|
+
migrationHash: row.migration_hash,
|
|
849
|
+
from: ledgerOriginFromStored(row.origin_core_hash),
|
|
850
|
+
to: row.destination_core_hash,
|
|
851
|
+
appliedAt,
|
|
852
|
+
operationCount: Array.isArray(row.operations) ? row.operations.length : 0
|
|
853
|
+
};
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Stamps the initial marker row for `space` via the shared contract-free DML
|
|
858
|
+
* builder, lowered through {@link lower} and executed on the driver. See the
|
|
859
|
+
* `SqlControlAdapter.initMarker` contract.
|
|
860
|
+
*/
|
|
861
|
+
async insertMarker(driver, space, destination) {
|
|
862
|
+
await execute((query) => this.lower(query, { contract: void 0 }), driver, marker.insert({
|
|
863
|
+
space,
|
|
864
|
+
core_hash: destination.storageHash,
|
|
865
|
+
profile_hash: destination.profileHash,
|
|
866
|
+
contract_json: null,
|
|
867
|
+
canonical_version: null,
|
|
868
|
+
updated_at: NOW,
|
|
869
|
+
app_tag: null,
|
|
870
|
+
meta: {},
|
|
871
|
+
invariants: destination.invariants ?? []
|
|
872
|
+
}).build());
|
|
873
|
+
}
|
|
874
|
+
async initMarker(driver, space, destination) {
|
|
875
|
+
await execute((query) => this.lower(query, { contract: void 0 }), driver, marker.upsert({
|
|
876
|
+
space,
|
|
877
|
+
core_hash: destination.storageHash,
|
|
878
|
+
profile_hash: destination.profileHash,
|
|
879
|
+
contract_json: null,
|
|
880
|
+
canonical_version: null,
|
|
881
|
+
updated_at: NOW,
|
|
882
|
+
app_tag: null,
|
|
883
|
+
meta: {},
|
|
884
|
+
invariants: destination.invariants ?? []
|
|
885
|
+
}).onConflict(marker.space).doUpdate((excluded) => ({
|
|
886
|
+
core_hash: excluded.core_hash,
|
|
887
|
+
profile_hash: excluded.profile_hash,
|
|
888
|
+
contract_json: excluded.contract_json,
|
|
889
|
+
canonical_version: excluded.canonical_version,
|
|
890
|
+
updated_at: NOW,
|
|
891
|
+
app_tag: excluded.app_tag,
|
|
892
|
+
meta: excluded.meta,
|
|
893
|
+
invariants: excluded.invariants
|
|
894
|
+
})).build());
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Compare-and-swap advance of the marker row for `space`. See the
|
|
898
|
+
* `SqlControlAdapter.updateMarker` contract.
|
|
899
|
+
*/
|
|
900
|
+
async updateMarker(driver, space, expectedFrom, destination) {
|
|
901
|
+
const currentInvariants = destination.invariants === void 0 ? [] : (await this.readMarker(driver, space))?.invariants ?? [];
|
|
902
|
+
const mergedInvariants = destination.invariants === void 0 ? void 0 : mergeInvariants(currentInvariants, destination.invariants);
|
|
903
|
+
return (await execute((q) => this.lower(q, { contract: void 0 }), driver, marker.update().set({
|
|
904
|
+
core_hash: destination.storageHash,
|
|
905
|
+
profile_hash: destination.profileHash,
|
|
906
|
+
updated_at: NOW,
|
|
907
|
+
...mergedInvariants !== void 0 ? { invariants: mergedInvariants } : {}
|
|
908
|
+
}).where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom))).returning(marker.space).build())).length > 0;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Appends a ledger entry for `space`. See the
|
|
912
|
+
* `SqlControlAdapter.writeLedgerEntry` contract.
|
|
913
|
+
*/
|
|
914
|
+
async writeLedgerEntry(driver, space, entry) {
|
|
915
|
+
await execute((query) => this.lower(query, { contract: void 0 }), driver, ledger.insert({
|
|
916
|
+
space,
|
|
917
|
+
migration_name: entry.migrationName,
|
|
918
|
+
migration_hash: entry.migrationHash,
|
|
919
|
+
origin_core_hash: entry.from,
|
|
920
|
+
destination_core_hash: entry.to,
|
|
921
|
+
operations: entry.operations
|
|
922
|
+
}).build());
|
|
923
|
+
}
|
|
924
|
+
async assertMarkerTableHasSpaceColumn(driver, space) {
|
|
925
|
+
const rows = (await driver.query(`select column_name
|
|
926
|
+
from information_schema.columns
|
|
927
|
+
where table_schema = 'prisma_contract'
|
|
928
|
+
and table_name = 'marker'`)).rows;
|
|
929
|
+
if (rows.length === 0) return;
|
|
930
|
+
if (!rows.every((row) => typeof row.column_name === "string")) return;
|
|
931
|
+
if (rows.some((row) => row.column_name === "space")) return;
|
|
932
|
+
rethrowMarkerReadError(/* @__PURE__ */ new Error("column \"space\" does not exist"), {
|
|
933
|
+
space,
|
|
934
|
+
markerLocation: POSTGRES_MARKER_TABLE
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async readMarkerResult(driver, space) {
|
|
938
|
+
const lower = (query) => this.lower(query, { contract: void 0 });
|
|
939
|
+
if ((await execute(lower, driver, infoSchemaTables.select(infoSchemaTables.table_schema).where(infoSchemaTables.table_schema.eq("prisma_contract").and(infoSchemaTables.table_name.eq("marker"))).build())).length === 0) return { kind: "no-table" };
|
|
940
|
+
await this.assertMarkerTableHasSpaceColumn(driver, space);
|
|
941
|
+
const row = (await execute(lower, driver, marker.select(marker.core_hash, marker.profile_hash, marker.contract_json, marker.canonical_version, marker.updated_at, marker.app_tag, marker.meta, marker.invariants).where(marker.space.eq(space)).build()))[0];
|
|
942
|
+
if (!row) return { kind: "absent" };
|
|
943
|
+
return {
|
|
944
|
+
kind: "present",
|
|
945
|
+
record: parseContractMarkerRow(row)
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Introspects a Postgres database schema and returns a raw SqlSchemaIR.
|
|
950
|
+
*
|
|
951
|
+
* This is a pure schema discovery operation that queries the Postgres catalog
|
|
952
|
+
* and returns the schema structure without type mapping or contract enrichment.
|
|
953
|
+
* Type mapping and enrichment are handled separately by enrichment helpers.
|
|
954
|
+
*
|
|
955
|
+
* When `contract` is provided and its storage declares more than one
|
|
956
|
+
* namespace (or any explicit bound namespace), the adapter walks every
|
|
957
|
+
* declared namespace and merges the per-schema introspection results
|
|
958
|
+
* into a single `SqlSchemaIR`. `UNBOUND_NAMESPACE_ID` resolves to the
|
|
959
|
+
* connection's `current_schema()` so late-bound tables follow the
|
|
960
|
+
* runtime `search_path`. When no contract is passed, the adapter falls
|
|
961
|
+
* back to introspecting the single `schema` argument (defaulting to
|
|
962
|
+
* `'public'`).
|
|
963
|
+
*
|
|
964
|
+
* Uses batched queries to minimize database round trips (6 queries per
|
|
965
|
+
* schema walked).
|
|
966
|
+
*
|
|
967
|
+
* @param driver - SqlControlDriverInstance<'postgres'> instance for executing queries
|
|
968
|
+
* @param contract - Optional contract for contract-guided introspection (multi-namespace walk, filtering)
|
|
969
|
+
* @param schema - Schema name to introspect when no contract is provided (defaults to 'public')
|
|
970
|
+
* @returns Promise resolving to SqlSchemaIR representing the live database schema
|
|
971
|
+
*/
|
|
972
|
+
async introspect(driver, contract, schema = "public") {
|
|
973
|
+
const declaredNamespaces = extractContractNamespaceIds(contract);
|
|
974
|
+
const ir = declaredNamespaces.length > 0 ? await this.introspectNamespaces(driver, declaredNamespaces) : await this.introspectSchema(driver, schema);
|
|
975
|
+
const existingSchemas = await this.listExistingSchemas(driver);
|
|
976
|
+
const annotations = ir.annotations ?? {};
|
|
977
|
+
const pg = annotations.pg ?? {};
|
|
978
|
+
return {
|
|
979
|
+
...ir,
|
|
980
|
+
annotations: {
|
|
981
|
+
...annotations,
|
|
982
|
+
pg: {
|
|
983
|
+
...pg,
|
|
984
|
+
existingSchemas
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Lists every non-system schema present in the connected database.
|
|
991
|
+
* The introspection consumer (`verifyPostgresNamespacePresence`)
|
|
992
|
+
* treats the result as the authoritative ground truth — declared
|
|
993
|
+
* namespaces whose `ddlSchemaName` is missing from this list become
|
|
994
|
+
* `missing_schema` issues, and the planner emits the matching
|
|
995
|
+
* `CREATE SCHEMA` before table DDL.
|
|
996
|
+
*/
|
|
997
|
+
async listExistingSchemas(driver) {
|
|
998
|
+
return (await driver.query(`SELECT nspname
|
|
999
|
+
FROM pg_catalog.pg_namespace
|
|
1000
|
+
WHERE nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
1001
|
+
AND nspname NOT LIKE 'pg_temp_%'
|
|
1002
|
+
AND nspname NOT LIKE 'pg_toast_temp_%'
|
|
1003
|
+
ORDER BY nspname`)).rows.map((row) => row.nspname);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Walks every declared namespace, resolving `UNBOUND_NAMESPACE_ID` to
|
|
1007
|
+
* the connection's `current_schema()`, and merges the per-schema results
|
|
1008
|
+
* into a single `SqlSchemaIR`. The merged `tables` map is flat (keyed by
|
|
1009
|
+
* table name) so callers that look up by `tableName` see every contract
|
|
1010
|
+
* table regardless of which namespace it lives in.
|
|
1011
|
+
*/
|
|
1012
|
+
async introspectNamespaces(driver, namespaceIds) {
|
|
1013
|
+
const resolvedSchemas = await Promise.all(namespaceIds.map(async (id) => {
|
|
1014
|
+
if (id === UNBOUND_NAMESPACE_ID) {
|
|
1015
|
+
const { rows } = await driver.query("SELECT current_schema() AS current_schema");
|
|
1016
|
+
return rows[0]?.current_schema ?? "public";
|
|
1017
|
+
}
|
|
1018
|
+
return id;
|
|
1019
|
+
}));
|
|
1020
|
+
const uniqueSchemas = Array.from(new Set(resolvedSchemas));
|
|
1021
|
+
const perSchema = await Promise.all(uniqueSchemas.map((s) => this.introspectSchema(driver, s)));
|
|
1022
|
+
const mergedTables = {};
|
|
1023
|
+
for (const ir of perSchema) for (const [tableName, table] of Object.entries(ir.tables)) mergedTables[tableName] = table;
|
|
1024
|
+
const mergedStorageTypes = {};
|
|
1025
|
+
for (let i = 0; i < perSchema.length; i++) {
|
|
1026
|
+
const ir = perSchema[i];
|
|
1027
|
+
const pg = blindCast(ir?.annotations?.["pg"])?.storageTypes;
|
|
1028
|
+
if (!pg) continue;
|
|
1029
|
+
for (const [key, value] of Object.entries(pg)) mergedStorageTypes[key] = value;
|
|
1030
|
+
}
|
|
1031
|
+
const firstAnnotations = perSchema[0]?.annotations;
|
|
1032
|
+
const firstPg = blindCast(firstAnnotations?.["pg"]) ?? {};
|
|
1033
|
+
return {
|
|
1034
|
+
tables: mergedTables,
|
|
1035
|
+
...ifDefined("annotations", {
|
|
1036
|
+
...firstAnnotations,
|
|
1037
|
+
pg: {
|
|
1038
|
+
...firstPg,
|
|
1039
|
+
...ifDefined("storageTypes", Object.keys(mergedStorageTypes).length > 0 ? mergedStorageTypes : void 0)
|
|
1040
|
+
}
|
|
1041
|
+
})
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Introspects a single Postgres schema and returns a raw SqlSchemaIR
|
|
1046
|
+
* containing only the tables in that schema. Used by `introspect` as
|
|
1047
|
+
* the per-namespace walk.
|
|
1048
|
+
*/
|
|
1049
|
+
async introspectSchema(driver, schema) {
|
|
1050
|
+
const [tablesResult, columnsResult, pkResult, fkResult, uniqueResult, indexResult, checkResult] = await Promise.all([
|
|
1051
|
+
driver.query(`SELECT table_name
|
|
1052
|
+
FROM information_schema.tables
|
|
1053
|
+
WHERE table_schema = $1
|
|
1054
|
+
AND table_type = 'BASE TABLE'
|
|
1055
|
+
ORDER BY table_name`, [schema]),
|
|
1056
|
+
driver.query(`SELECT
|
|
1057
|
+
c.table_name,
|
|
1058
|
+
column_name,
|
|
1059
|
+
data_type,
|
|
1060
|
+
udt_name,
|
|
1061
|
+
is_nullable,
|
|
1062
|
+
character_maximum_length,
|
|
1063
|
+
numeric_precision,
|
|
1064
|
+
numeric_scale,
|
|
1065
|
+
column_default,
|
|
1066
|
+
format_type(a.atttypid, a.atttypmod) AS formatted_type
|
|
1067
|
+
FROM information_schema.columns c
|
|
1068
|
+
JOIN pg_catalog.pg_class cl
|
|
1069
|
+
ON cl.relname = c.table_name
|
|
1070
|
+
JOIN pg_catalog.pg_namespace ns
|
|
1071
|
+
ON ns.nspname = c.table_schema
|
|
1072
|
+
AND ns.oid = cl.relnamespace
|
|
1073
|
+
JOIN pg_catalog.pg_attribute a
|
|
1074
|
+
ON a.attrelid = cl.oid
|
|
1075
|
+
AND a.attname = c.column_name
|
|
1076
|
+
AND a.attnum > 0
|
|
1077
|
+
AND NOT a.attisdropped
|
|
1078
|
+
WHERE c.table_schema = $1
|
|
1079
|
+
ORDER BY c.table_name, c.ordinal_position`, [schema]),
|
|
1080
|
+
driver.query(`SELECT
|
|
1081
|
+
tc.table_name,
|
|
1082
|
+
tc.constraint_name,
|
|
1083
|
+
kcu.column_name,
|
|
1084
|
+
kcu.ordinal_position
|
|
1085
|
+
FROM information_schema.table_constraints tc
|
|
1086
|
+
JOIN information_schema.key_column_usage kcu
|
|
1087
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1088
|
+
AND tc.table_schema = kcu.table_schema
|
|
1089
|
+
AND tc.table_name = kcu.table_name
|
|
1090
|
+
WHERE tc.table_schema = $1
|
|
1091
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
1092
|
+
ORDER BY tc.table_name, kcu.ordinal_position`, [schema]),
|
|
1093
|
+
driver.query(`SELECT
|
|
1094
|
+
tc.table_name,
|
|
1095
|
+
tc.constraint_name,
|
|
1096
|
+
kcu.column_name,
|
|
1097
|
+
kcu.ordinal_position,
|
|
1098
|
+
ref_ns.nspname AS referenced_table_schema,
|
|
1099
|
+
ref_cl.relname AS referenced_table_name,
|
|
1100
|
+
ref_att.attname AS referenced_column_name,
|
|
1101
|
+
rc.delete_rule,
|
|
1102
|
+
rc.update_rule
|
|
1103
|
+
FROM information_schema.table_constraints tc
|
|
1104
|
+
JOIN information_schema.key_column_usage kcu
|
|
1105
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1106
|
+
AND tc.table_schema = kcu.table_schema
|
|
1107
|
+
AND tc.table_name = kcu.table_name
|
|
1108
|
+
JOIN pg_catalog.pg_constraint pgc
|
|
1109
|
+
ON pgc.conname = tc.constraint_name
|
|
1110
|
+
AND pgc.connamespace = (
|
|
1111
|
+
SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = tc.table_schema
|
|
1112
|
+
)
|
|
1113
|
+
JOIN pg_catalog.pg_class ref_cl
|
|
1114
|
+
ON ref_cl.oid = pgc.confrelid
|
|
1115
|
+
JOIN pg_catalog.pg_namespace ref_ns
|
|
1116
|
+
ON ref_ns.oid = ref_cl.relnamespace
|
|
1117
|
+
JOIN pg_catalog.pg_attribute ref_att
|
|
1118
|
+
ON ref_att.attrelid = pgc.confrelid
|
|
1119
|
+
AND ref_att.attnum = pgc.confkey[kcu.ordinal_position]
|
|
1120
|
+
JOIN information_schema.referential_constraints rc
|
|
1121
|
+
ON rc.constraint_name = tc.constraint_name
|
|
1122
|
+
AND rc.constraint_schema = tc.table_schema
|
|
1123
|
+
WHERE tc.table_schema = $1
|
|
1124
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
1125
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`, [schema]),
|
|
1126
|
+
driver.query(`SELECT
|
|
1127
|
+
tc.table_name,
|
|
1128
|
+
tc.constraint_name,
|
|
1129
|
+
kcu.column_name,
|
|
1130
|
+
kcu.ordinal_position
|
|
1131
|
+
FROM information_schema.table_constraints tc
|
|
1132
|
+
JOIN information_schema.key_column_usage kcu
|
|
1133
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
1134
|
+
AND tc.table_schema = kcu.table_schema
|
|
1135
|
+
AND tc.table_name = kcu.table_name
|
|
1136
|
+
WHERE tc.table_schema = $1
|
|
1137
|
+
AND tc.constraint_type = 'UNIQUE'
|
|
1138
|
+
ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position`, [schema]),
|
|
1139
|
+
driver.query(`SELECT
|
|
1140
|
+
i.tablename,
|
|
1141
|
+
i.indexname,
|
|
1142
|
+
ix.indisunique,
|
|
1143
|
+
a.attname,
|
|
1144
|
+
k.ord AS index_position,
|
|
1145
|
+
am.amname,
|
|
1146
|
+
ic.reloptions
|
|
1147
|
+
FROM pg_indexes i
|
|
1148
|
+
JOIN pg_class ic ON ic.relname = i.indexname
|
|
1149
|
+
JOIN pg_namespace ins ON ins.oid = ic.relnamespace AND ins.nspname = $1
|
|
1150
|
+
JOIN pg_index ix ON ix.indexrelid = ic.oid
|
|
1151
|
+
JOIN pg_am am ON am.oid = ic.relam
|
|
1152
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
1153
|
+
JOIN pg_namespace tn ON tn.oid = t.relnamespace AND tn.nspname = $1
|
|
1154
|
+
JOIN LATERAL unnest(ix.indkey::int[]) WITH ORDINALITY AS k(attnum, ord) ON true
|
|
1155
|
+
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND a.attnum > 0
|
|
1156
|
+
WHERE i.schemaname = $1
|
|
1157
|
+
AND NOT EXISTS (
|
|
1158
|
+
SELECT 1
|
|
1159
|
+
FROM information_schema.table_constraints tc
|
|
1160
|
+
WHERE tc.table_schema = $1
|
|
1161
|
+
AND tc.table_name = i.tablename
|
|
1162
|
+
AND tc.constraint_name = i.indexname
|
|
1163
|
+
)
|
|
1164
|
+
ORDER BY i.tablename, i.indexname, k.ord`, [schema]),
|
|
1165
|
+
driver.query(`SELECT
|
|
1166
|
+
cl.relname AS table_name,
|
|
1167
|
+
c.conname AS constraint_name,
|
|
1168
|
+
pg_get_constraintdef(c.oid) AS constraintdef
|
|
1169
|
+
FROM pg_catalog.pg_constraint c
|
|
1170
|
+
JOIN pg_catalog.pg_class cl ON cl.oid = c.conrelid
|
|
1171
|
+
JOIN pg_catalog.pg_namespace ns ON ns.oid = cl.relnamespace
|
|
1172
|
+
WHERE ns.nspname = $1
|
|
1173
|
+
AND c.contype = 'c'
|
|
1174
|
+
ORDER BY cl.relname, c.conname`, [schema])
|
|
1175
|
+
]);
|
|
1176
|
+
const columnsByTable = groupBy(columnsResult.rows, "table_name");
|
|
1177
|
+
const pksByTable = groupBy(pkResult.rows, "table_name");
|
|
1178
|
+
const fksByTable = groupBy(fkResult.rows, "table_name");
|
|
1179
|
+
const uniquesByTable = groupBy(uniqueResult.rows, "table_name");
|
|
1180
|
+
const indexesByTable = groupBy(indexResult.rows, "tablename");
|
|
1181
|
+
const checksByTable = groupBy(checkResult.rows, "table_name");
|
|
1182
|
+
const pkConstraintsByTable = /* @__PURE__ */ new Map();
|
|
1183
|
+
for (const row of pkResult.rows) {
|
|
1184
|
+
let constraints = pkConstraintsByTable.get(row.table_name);
|
|
1185
|
+
if (!constraints) {
|
|
1186
|
+
constraints = /* @__PURE__ */ new Set();
|
|
1187
|
+
pkConstraintsByTable.set(row.table_name, constraints);
|
|
1188
|
+
}
|
|
1189
|
+
constraints.add(row.constraint_name);
|
|
1190
|
+
}
|
|
1191
|
+
const tables = {};
|
|
1192
|
+
for (const tableRow of tablesResult.rows) {
|
|
1193
|
+
const tableName = tableRow.table_name;
|
|
1194
|
+
const columns = {};
|
|
1195
|
+
for (const colRow of columnsByTable.get(tableName) ?? []) {
|
|
1196
|
+
let nativeType = colRow.udt_name;
|
|
1197
|
+
const formattedType = colRow.formatted_type ? normalizeFormattedType(colRow.formatted_type, colRow.data_type, colRow.udt_name) : null;
|
|
1198
|
+
if (formattedType) nativeType = formattedType;
|
|
1199
|
+
else if (colRow.data_type === "character varying" || colRow.data_type === "character") if (colRow.character_maximum_length) nativeType = `${colRow.data_type}(${colRow.character_maximum_length})`;
|
|
1200
|
+
else nativeType = colRow.data_type;
|
|
1201
|
+
else if (colRow.data_type === "numeric" || colRow.data_type === "decimal") if (colRow.numeric_precision && colRow.numeric_scale !== null) nativeType = `${colRow.data_type}(${colRow.numeric_precision},${colRow.numeric_scale})`;
|
|
1202
|
+
else if (colRow.numeric_precision) nativeType = `${colRow.data_type}(${colRow.numeric_precision})`;
|
|
1203
|
+
else nativeType = colRow.data_type;
|
|
1204
|
+
else nativeType = colRow.udt_name || colRow.data_type;
|
|
1205
|
+
columns[colRow.column_name] = {
|
|
1206
|
+
name: colRow.column_name,
|
|
1207
|
+
nativeType,
|
|
1208
|
+
nullable: colRow.is_nullable === "YES",
|
|
1209
|
+
...ifDefined("default", colRow.column_default ?? void 0)
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
const pkRows = [...pksByTable.get(tableName) ?? []];
|
|
1213
|
+
const primaryKeyColumns = pkRows.sort((a, b) => a.ordinal_position - b.ordinal_position).map((row) => row.column_name);
|
|
1214
|
+
const primaryKey = primaryKeyColumns.length > 0 ? {
|
|
1215
|
+
columns: primaryKeyColumns,
|
|
1216
|
+
...pkRows[0]?.constraint_name ? { name: pkRows[0].constraint_name } : {}
|
|
1217
|
+
} : void 0;
|
|
1218
|
+
const foreignKeysMap = /* @__PURE__ */ new Map();
|
|
1219
|
+
for (const fkRow of fksByTable.get(tableName) ?? []) {
|
|
1220
|
+
const existing = foreignKeysMap.get(fkRow.constraint_name);
|
|
1221
|
+
if (existing) {
|
|
1222
|
+
existing.columns.push(fkRow.column_name);
|
|
1223
|
+
existing.referencedColumns.push(fkRow.referenced_column_name);
|
|
1224
|
+
} else foreignKeysMap.set(fkRow.constraint_name, {
|
|
1225
|
+
columns: [fkRow.column_name],
|
|
1226
|
+
referencedTable: fkRow.referenced_table_name,
|
|
1227
|
+
referencedSchema: fkRow.referenced_table_schema,
|
|
1228
|
+
referencedColumns: [fkRow.referenced_column_name],
|
|
1229
|
+
name: fkRow.constraint_name,
|
|
1230
|
+
deleteRule: fkRow.delete_rule,
|
|
1231
|
+
updateRule: fkRow.update_rule
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
const foreignKeys = Array.from(foreignKeysMap.values()).map((fk) => ({
|
|
1235
|
+
columns: Object.freeze([...fk.columns]),
|
|
1236
|
+
referencedTable: fk.referencedTable,
|
|
1237
|
+
referencedSchema: fk.referencedSchema,
|
|
1238
|
+
referencedColumns: Object.freeze([...fk.referencedColumns]),
|
|
1239
|
+
name: fk.name,
|
|
1240
|
+
...ifDefined("onDelete", mapReferentialAction(fk.deleteRule)),
|
|
1241
|
+
...ifDefined("onUpdate", mapReferentialAction(fk.updateRule))
|
|
1242
|
+
}));
|
|
1243
|
+
const pkConstraints = pkConstraintsByTable.get(tableName) ?? /* @__PURE__ */ new Set();
|
|
1244
|
+
const uniquesMap = /* @__PURE__ */ new Map();
|
|
1245
|
+
for (const uniqueRow of uniquesByTable.get(tableName) ?? []) {
|
|
1246
|
+
if (pkConstraints.has(uniqueRow.constraint_name)) continue;
|
|
1247
|
+
const existing = uniquesMap.get(uniqueRow.constraint_name);
|
|
1248
|
+
if (existing) existing.columns.push(uniqueRow.column_name);
|
|
1249
|
+
else uniquesMap.set(uniqueRow.constraint_name, {
|
|
1250
|
+
columns: [uniqueRow.column_name],
|
|
1251
|
+
name: uniqueRow.constraint_name
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
const uniques = Array.from(uniquesMap.values()).map((uq) => ({
|
|
1255
|
+
columns: Object.freeze([...uq.columns]),
|
|
1256
|
+
name: uq.name
|
|
1257
|
+
}));
|
|
1258
|
+
const indexesMap = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const idxRow of indexesByTable.get(tableName) ?? []) {
|
|
1260
|
+
if (!idxRow.attname) continue;
|
|
1261
|
+
const existing = indexesMap.get(idxRow.indexname);
|
|
1262
|
+
if (existing) existing.columns.push(idxRow.attname);
|
|
1263
|
+
else {
|
|
1264
|
+
const indexType = idxRow.amname && idxRow.amname !== "btree" ? idxRow.amname : void 0;
|
|
1265
|
+
const indexOptions = parsePgReloptions(idxRow.reloptions, idxRow.indexname);
|
|
1266
|
+
indexesMap.set(idxRow.indexname, {
|
|
1267
|
+
columns: [idxRow.attname],
|
|
1268
|
+
name: idxRow.indexname,
|
|
1269
|
+
unique: idxRow.indisunique,
|
|
1270
|
+
type: indexType,
|
|
1271
|
+
options: indexOptions
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const indexes = Array.from(indexesMap.values()).map((idx) => ({
|
|
1276
|
+
columns: Object.freeze([...idx.columns]),
|
|
1277
|
+
name: idx.name,
|
|
1278
|
+
unique: idx.unique,
|
|
1279
|
+
...idx.type !== void 0 && { type: idx.type },
|
|
1280
|
+
...idx.options !== void 0 && { options: idx.options }
|
|
1281
|
+
}));
|
|
1282
|
+
const checksForTable = [];
|
|
1283
|
+
for (const checkRow of checksByTable.get(tableName) ?? []) {
|
|
1284
|
+
const parsed = parseCheckConstraintDef(checkRow.constraintdef);
|
|
1285
|
+
if (parsed) checksForTable.push({
|
|
1286
|
+
name: checkRow.constraint_name,
|
|
1287
|
+
column: parsed.column,
|
|
1288
|
+
permittedValues: parsed.permittedValues
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
tables[tableName] = {
|
|
1292
|
+
name: tableName,
|
|
1293
|
+
columns,
|
|
1294
|
+
...ifDefined("primaryKey", primaryKey),
|
|
1295
|
+
foreignKeys,
|
|
1296
|
+
uniques,
|
|
1297
|
+
indexes,
|
|
1298
|
+
...ifDefined("checks", checksForTable.length > 0 ? checksForTable : void 0)
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const rawStorageTypes = await introspectPostgresEnumTypes({
|
|
1302
|
+
driver,
|
|
1303
|
+
schemaName: schema
|
|
1304
|
+
});
|
|
1305
|
+
const storageTypes = {};
|
|
1306
|
+
for (const [typeName, annotation] of Object.entries(rawStorageTypes)) storageTypes[enumStorageCompoundKey(schema, typeName)] = annotation;
|
|
1307
|
+
return {
|
|
1308
|
+
tables,
|
|
1309
|
+
annotations: { pg: {
|
|
1310
|
+
schema,
|
|
1311
|
+
version: await this.getPostgresVersion(driver),
|
|
1312
|
+
...ifDefined("storageTypes", Object.keys(storageTypes).length > 0 ? storageTypes : void 0)
|
|
1313
|
+
} }
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Gets the Postgres version from the database.
|
|
1318
|
+
*/
|
|
1319
|
+
async getPostgresVersion(driver) {
|
|
1320
|
+
return ((await driver.query("SELECT version() AS version", [])).rows[0]?.version ?? "").match(/PostgreSQL (\d+\.\d+)/)?.[1] ?? "unknown";
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
/**
|
|
1324
|
+
* Extracts the namespace coordinate ids declared on a contract's storage,
|
|
1325
|
+
* or returns an empty array when no contract (or no storage / namespaces)
|
|
1326
|
+
* is present. Used by `PostgresControlAdapter.introspect` to decide
|
|
1327
|
+
* between the multi-namespace walk and the single-schema fallback.
|
|
1328
|
+
*/
|
|
1329
|
+
function extractContractNamespaceIds(contract) {
|
|
1330
|
+
if (contract === null || typeof contract !== "object") return [];
|
|
1331
|
+
const storage = contract.storage;
|
|
1332
|
+
if (storage === null || typeof storage !== "object") return [];
|
|
1333
|
+
const namespaces = storage.namespaces;
|
|
1334
|
+
if (namespaces === null || typeof namespaces !== "object") return [];
|
|
1335
|
+
return Object.keys(namespaces);
|
|
1336
|
+
}
|
|
1337
|
+
function normalizeFormattedType(formattedType, dataType, udtName) {
|
|
1338
|
+
if (formattedType === "integer") return "int4";
|
|
1339
|
+
if (formattedType === "smallint") return "int2";
|
|
1340
|
+
if (formattedType === "bigint") return "int8";
|
|
1341
|
+
if (formattedType === "real") return "float4";
|
|
1342
|
+
if (formattedType === "double precision") return "float8";
|
|
1343
|
+
if (formattedType === "boolean") return "bool";
|
|
1344
|
+
if (formattedType.startsWith("varchar")) return formattedType.replace("varchar", "character varying");
|
|
1345
|
+
if (formattedType.startsWith("bpchar")) return formattedType.replace("bpchar", "character");
|
|
1346
|
+
if (formattedType.startsWith("varbit")) return formattedType.replace("varbit", "bit varying");
|
|
1347
|
+
if (dataType === "timestamp with time zone" || udtName === "timestamptz") return formattedType.replace("timestamp", "timestamptz").replace(" with time zone", "").trim();
|
|
1348
|
+
if (dataType === "timestamp without time zone" || udtName === "timestamp") return formattedType.replace(" without time zone", "").trim();
|
|
1349
|
+
if (dataType === "time with time zone" || udtName === "timetz") return formattedType.replace("time", "timetz").replace(" with time zone", "").trim();
|
|
1350
|
+
if (dataType === "time without time zone" || udtName === "time") return formattedType.replace(" without time zone", "").trim();
|
|
1351
|
+
if (formattedType.startsWith("\"") && formattedType.endsWith("\"")) return formattedType.slice(1, -1);
|
|
1352
|
+
return formattedType;
|
|
1353
|
+
}
|
|
1354
|
+
const PG_REFERENTIAL_ACTION_MAP = {
|
|
1355
|
+
"NO ACTION": "noAction",
|
|
1356
|
+
RESTRICT: "restrict",
|
|
1357
|
+
CASCADE: "cascade",
|
|
1358
|
+
"SET NULL": "setNull",
|
|
1359
|
+
"SET DEFAULT": "setDefault"
|
|
1360
|
+
};
|
|
1361
|
+
/**
|
|
1362
|
+
* Maps a Postgres referential action rule to the canonical SqlReferentialAction.
|
|
1363
|
+
* Returns undefined for 'NO ACTION' (the database default) to keep the IR sparse.
|
|
1364
|
+
* Throws for unrecognized rules to prevent silent data loss.
|
|
1365
|
+
*/
|
|
1366
|
+
function mapReferentialAction(rule) {
|
|
1367
|
+
const mapped = PG_REFERENTIAL_ACTION_MAP[rule];
|
|
1368
|
+
if (mapped === void 0) throw new Error(`Unknown PostgreSQL referential action rule: "${rule}". Expected one of: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT.`);
|
|
1369
|
+
if (mapped === "noAction") return void 0;
|
|
1370
|
+
return mapped;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Groups an array of objects by a specified key.
|
|
1374
|
+
* Returns a Map for O(1) lookup by group key.
|
|
1375
|
+
*/
|
|
1376
|
+
/**
|
|
1377
|
+
* Parses a `pg_class.reloptions` array into a `Record<string, string>`.
|
|
1378
|
+
*
|
|
1379
|
+
* Postgres returns reloptions as a `text[]` whose entries are `key=value`
|
|
1380
|
+
* strings; the value side is always a string regardless of the underlying
|
|
1381
|
+
* scalar type. The verifier compares contract options to introspected
|
|
1382
|
+
* options after coercing both sides to strings, so keeping the raw text
|
|
1383
|
+
* here is correct.
|
|
1384
|
+
*
|
|
1385
|
+
* Returns `undefined` when the input is null/empty (no WITH clause).
|
|
1386
|
+
*/
|
|
1387
|
+
function parsePgReloptions(reloptions, indexName) {
|
|
1388
|
+
if (!reloptions || reloptions.length === 0) return;
|
|
1389
|
+
const result = {};
|
|
1390
|
+
for (const entry of reloptions) {
|
|
1391
|
+
const eq = entry.indexOf("=");
|
|
1392
|
+
if (eq === -1) throw new Error(`Postgres introspection: malformed reloption entry "${entry}" on index "${indexName}" (expected "key=value")`);
|
|
1393
|
+
const key = entry.slice(0, eq);
|
|
1394
|
+
result[key] = entry.slice(eq + 1);
|
|
1395
|
+
}
|
|
1396
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function groupBy(items, key) {
|
|
1399
|
+
const map = /* @__PURE__ */ new Map();
|
|
1400
|
+
for (const item of items) {
|
|
1401
|
+
const groupKey = item[key];
|
|
1402
|
+
let group = map.get(groupKey);
|
|
1403
|
+
if (!group) {
|
|
1404
|
+
group = [];
|
|
1405
|
+
map.set(groupKey, group);
|
|
1406
|
+
}
|
|
1407
|
+
group.push(item);
|
|
1408
|
+
}
|
|
1409
|
+
return map;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Parses a Postgres check-constraint definition string (as returned by
|
|
1413
|
+
* `pg_get_constraintdef`) into a column name and permitted values array.
|
|
1414
|
+
*
|
|
1415
|
+
* Handles two shapes that Postgres emits for enum-style checks:
|
|
1416
|
+
*
|
|
1417
|
+
* 1. `= ANY (ARRAY[...])` — Postgres rewrites `col IN ('a','b')` to this form:
|
|
1418
|
+
* `CHECK ((col = ANY (ARRAY['a'::text, 'b'::text])))`
|
|
1419
|
+
*
|
|
1420
|
+
* 2. `IN (...)` — stays as-is when written directly:
|
|
1421
|
+
* `CHECK ((col IN ('a', 'b')))`
|
|
1422
|
+
*
|
|
1423
|
+
* Column names may be plain identifiers (`status`) or double-quoted identifiers
|
|
1424
|
+
* (`"my-col"`). Double-quoted identifiers with embedded `""` are un-escaped to a
|
|
1425
|
+
* single `"`.
|
|
1426
|
+
*
|
|
1427
|
+
* String literal values may contain Postgres-style doubled single-quotes (`''`),
|
|
1428
|
+
* which are un-escaped to a single `'` (e.g. `O''Brien` → `O'Brien`).
|
|
1429
|
+
*
|
|
1430
|
+
* Returns `{ column, permittedValues }` when the predicate matches one of
|
|
1431
|
+
* the two recognised shapes. Returns `undefined` for anything else (e.g.
|
|
1432
|
+
* a free-form SQL predicate that wasn't emitted by this slice).
|
|
1433
|
+
*/
|
|
1434
|
+
function parseCheckConstraintDef(constraintdef) {
|
|
1435
|
+
const afterCheck = constraintdef.replace(/^CHECK\s*\(/i, "").replace(/\)$/, "").trim();
|
|
1436
|
+
const inner = afterCheck.startsWith("(") && afterCheck.endsWith(")") ? afterCheck.slice(1, -1).trim() : afterCheck;
|
|
1437
|
+
const anyArrayMatch = inner.match(/^(?:"((?:[^"]|"")*)"|(\w+))\s*=\s*ANY\s*\(\s*ARRAY\s*\[(.+)\]\s*\)\s*$/i);
|
|
1438
|
+
if (anyArrayMatch) {
|
|
1439
|
+
const column = anyArrayMatch[1] !== void 0 ? anyArrayMatch[1].replace(/""/g, "\"") : anyArrayMatch[2];
|
|
1440
|
+
const arrayBody = anyArrayMatch[3];
|
|
1441
|
+
if (!column || !arrayBody) return void 0;
|
|
1442
|
+
const permittedValues = extractArrayLiterals(arrayBody);
|
|
1443
|
+
return permittedValues ? {
|
|
1444
|
+
column,
|
|
1445
|
+
permittedValues
|
|
1446
|
+
} : void 0;
|
|
1447
|
+
}
|
|
1448
|
+
const inMatch = inner.match(/^(?:"((?:[^"]|"")*)"|(\w+))\s+IN\s*\((.+)\)\s*$/i);
|
|
1449
|
+
if (inMatch) {
|
|
1450
|
+
const column = inMatch[1] !== void 0 ? inMatch[1].replace(/""/g, "\"") : inMatch[2];
|
|
1451
|
+
const listBody = inMatch[3];
|
|
1452
|
+
if (!column || !listBody) return void 0;
|
|
1453
|
+
const permittedValues = extractQuotedLiterals(listBody);
|
|
1454
|
+
return permittedValues ? {
|
|
1455
|
+
column,
|
|
1456
|
+
permittedValues
|
|
1457
|
+
} : void 0;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Extracts string literals from an `ARRAY[...]` body.
|
|
1462
|
+
* Handles `'value'::type` casts by stripping the cast part.
|
|
1463
|
+
* Postgres stores single quotes inside values as doubled single-quotes (`''`);
|
|
1464
|
+
* each extracted value is un-escaped so `O''Brien` becomes `O'Brien`.
|
|
1465
|
+
*/
|
|
1466
|
+
function extractArrayLiterals(arrayBody) {
|
|
1467
|
+
const values = [...arrayBody.matchAll(/'((?:[^'\\]|\\.|'')*)'\s*(?:::[^\s,\]]+)?/g)].map((m) => (m[1] ?? "").replace(/''/g, "'"));
|
|
1468
|
+
return values.length > 0 ? values : void 0;
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Extracts string literals from an `IN (...)` list.
|
|
1472
|
+
* Handles single-quoted literals with possible escaped quotes.
|
|
1473
|
+
* Postgres stores single quotes inside values as doubled single-quotes (`''`);
|
|
1474
|
+
* each extracted value is un-escaped so `O''Brien` becomes `O'Brien`.
|
|
1475
|
+
*/
|
|
1476
|
+
function extractQuotedLiterals(listBody) {
|
|
1477
|
+
const values = [...listBody.matchAll(/'((?:[^'\\]|\\.|'')*)'/g)].map((m) => (m[1] ?? "").replace(/''/g, "'"));
|
|
1478
|
+
return values.length > 0 ? values : void 0;
|
|
1479
|
+
}
|
|
1480
|
+
//#endregion
|
|
1481
|
+
export { createPostgresBuiltinCodecLookup as i, renderLoweredSql as n, renderLoweredDdl as r, PostgresControlAdapter as t };
|
|
1482
|
+
|
|
1483
|
+
//# sourceMappingURL=control-adapter-BmVmHzER.mjs.map
|