@schemic/postgres 0.1.0-alpha.0

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/lib/index.js ADDED
@@ -0,0 +1,1161 @@
1
+ // src/index.ts
2
+ import {
3
+ connectionEntry,
4
+ nullable as nullable2,
5
+ registerDriver
6
+ } from "@schemic/core/driver";
7
+
8
+ // src/emit.ts
9
+ import { nullable } from "@schemic/core/driver";
10
+ var escId = (name) => `"${name.replace(/"/g, '""')}"`;
11
+ var SCALAR_TO_PG = {
12
+ string: "text",
13
+ int: "integer",
14
+ float: "double precision",
15
+ decimal: "numeric",
16
+ number: "double precision",
17
+ bool: "boolean",
18
+ datetime: "timestamp with time zone",
19
+ uuid: "uuid",
20
+ bytes: "bytea",
21
+ duration: "interval"
22
+ };
23
+ function pgColumn(type) {
24
+ if (type.t === "option" || type.t === "nullable") {
25
+ return { ...pgColumn(type.inner), nullable: true };
26
+ }
27
+ if (type.t === "scalar") {
28
+ const sql = SCALAR_TO_PG[type.name];
29
+ if (!sql) throw new Error(`postgres: unsupported scalar "${type.name}"`);
30
+ return { sql, nullable: false };
31
+ }
32
+ if (type.t === "literal") {
33
+ const base = typeof type.value === "number" ? "double precision" : typeof type.value === "boolean" ? "boolean" : "text";
34
+ return { sql: base, nullable: false };
35
+ }
36
+ if (type.t === "union") {
37
+ if (type.members.every(
38
+ (m) => m.t === "literal" && typeof m.value === "string"
39
+ )) {
40
+ return { sql: "text", nullable: false };
41
+ }
42
+ throw new Error("postgres: non-enum unions are unsupported");
43
+ }
44
+ if (type.t === "array" || type.t === "set") {
45
+ const elem = pgColumn(type.elem);
46
+ return { sql: `${elem.sql}[]`, nullable: false };
47
+ }
48
+ if (type.t === "object") {
49
+ return { sql: "jsonb", nullable: false };
50
+ }
51
+ if (type.t === "record") {
52
+ if (type.tables.length !== 1) {
53
+ return { sql: "text", nullable: false };
54
+ }
55
+ return { sql: "text", nullable: false, references: type.tables[0] };
56
+ }
57
+ if (type.t === "geometry") {
58
+ return { sql: "jsonb", nullable: false };
59
+ }
60
+ if (type.t === "native") {
61
+ if (type.db !== "postgres") {
62
+ throw new Error(
63
+ `postgres: native type "${type.name}" belongs to driver "${type.db}"`
64
+ );
65
+ }
66
+ const params = type.params;
67
+ const sql = params && params.length > 0 ? `${type.name}(${params.join(", ")})` : type.name;
68
+ return { sql, nullable: false };
69
+ }
70
+ throw new Error(`postgres: cannot emit type ${JSON.stringify(type)}`);
71
+ }
72
+ function pgCanonType(type) {
73
+ if (type.t === "option") return nullable(pgCanonType(type.inner));
74
+ if (type.t === "nullable") return nullable(pgCanonType(type.inner));
75
+ if (type.t === "array")
76
+ return {
77
+ t: "array",
78
+ elem: pgCanonType(type.elem),
79
+ ...type.size !== void 0 ? { size: type.size } : {}
80
+ };
81
+ if (type.t === "set")
82
+ return {
83
+ t: "set",
84
+ elem: pgCanonType(type.elem),
85
+ ...type.size !== void 0 ? { size: type.size } : {}
86
+ };
87
+ if (type.t === "object") return { t: "object", fields: {} };
88
+ return type;
89
+ }
90
+ function normAction(a) {
91
+ const u = a?.toUpperCase();
92
+ return u && u !== "NO ACTION" ? u : void 0;
93
+ }
94
+ function canonField(f, table) {
95
+ const out = { name: f.name, table, type: pgCanonType(f.type) };
96
+ if (f.identity !== void 0) out.identity = f.identity;
97
+ if (f.reference) {
98
+ const ref = {};
99
+ const od = normAction(f.reference.on_delete);
100
+ const ou = normAction(f.reference.on_update);
101
+ if (od) ref.on_delete = od;
102
+ if (ou) ref.on_update = ou;
103
+ if (ref.on_delete !== void 0 || ref.on_update !== void 0)
104
+ out.reference = ref;
105
+ }
106
+ return out;
107
+ }
108
+ function pgEmitFields(t) {
109
+ return t.fields.filter((f) => !f.name.includes(".")).map((f) => ({ ...f, table: t.name, type: pgCanonType(f.type) })).sort((a, b) => a.name.localeCompare(b.name));
110
+ }
111
+ function fkActions(ref) {
112
+ let s2 = "";
113
+ if (ref?.on_delete) s2 += ` ON DELETE ${ref.on_delete}`;
114
+ if (ref?.on_update) s2 += ` ON UPDATE ${ref.on_update}`;
115
+ return s2;
116
+ }
117
+ function fieldColumnDdl(f) {
118
+ const col = pgColumn(f.type);
119
+ let s2 = `${escId(f.name)} ${col.sql}`;
120
+ if (!col.nullable) s2 += " NOT NULL";
121
+ if (f.identity)
122
+ s2 += ` GENERATED ${f.identity === "always" ? "ALWAYS" : "BY DEFAULT"} AS IDENTITY`;
123
+ if (f.default !== void 0) s2 += ` DEFAULT ${f.default}`;
124
+ if (f.computed !== void 0)
125
+ s2 += ` GENERATED ALWAYS AS (${f.computed}) STORED`;
126
+ if (f.check !== void 0) s2 += ` CHECK (${f.check})`;
127
+ return s2;
128
+ }
129
+ function createTableDdl(t) {
130
+ const fields = pgEmitFields(t);
131
+ const custom = !!(t.primaryKey && t.primaryKey.length > 0);
132
+ const cols = [];
133
+ if (!custom) cols.push(`${escId("id")} text PRIMARY KEY`);
134
+ for (const f of fields) cols.push(fieldColumnDdl(f));
135
+ if (custom) cols.push(`PRIMARY KEY (${t.primaryKey?.map(escId).join(", ")})`);
136
+ for (const c of t.checks ?? []) cols.push(`CHECK (${c})`);
137
+ return `CREATE TABLE ${escId(t.name)} (
138
+ ${cols.join(",\n ")}
139
+ );`;
140
+ }
141
+ var fkName = (table, field) => `${table}_${field}_fkey`;
142
+ var dropTableSql = (table) => `DROP TABLE IF EXISTS ${escId(table)} CASCADE;`;
143
+ var addFkSql = (table, field, ref, actions = "") => `ALTER TABLE ${escId(table)} ADD CONSTRAINT ${escId(fkName(table, field))} FOREIGN KEY (${escId(field)}) REFERENCES ${escId(ref)} (${escId("id")})${actions};`;
144
+ var dropFkSql = (table, field) => `ALTER TABLE ${escId(table)} DROP CONSTRAINT IF EXISTS ${escId(fkName(table, field))};`;
145
+
146
+ // src/kinds.ts
147
+ import {
148
+ KindRegistry
149
+ } from "@schemic/core";
150
+ var tableRef = (name) => ({ kind: "table", name });
151
+ function commentLines(t) {
152
+ return t.fields.filter((f) => f.comment !== void 0).map(
153
+ (f) => `COMMENT ON COLUMN ${escId(t.name)}.${escId(f.name)} IS '${(f.comment ?? "").replace(/'/g, "''")}';`
154
+ );
155
+ }
156
+ var hasHardClause = (f) => f.identity !== void 0 || f.computed !== void 0 || f.check !== void 0;
157
+ var sameArr = (a, b) => JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
158
+ var sameField = (a, b) => JSON.stringify(a) === JSON.stringify(b);
159
+ var fieldKey = (table, name) => `field:${table}:${name}`;
160
+ var createInput = (t) => ({
161
+ name: t.name,
162
+ fields: t.fields,
163
+ ...t.primaryKey ? { primaryKey: t.primaryKey } : {},
164
+ ...t.checks ? { checks: t.checks } : {}
165
+ });
166
+ function fieldDisplayItems(prev, next) {
167
+ const table = (next ?? prev)?.name ?? "";
168
+ const before = new Map((prev?.fields ?? []).map((f) => [f.name, f]));
169
+ const after = new Map((next?.fields ?? []).map((f) => [f.name, f]));
170
+ const items = [];
171
+ for (const f of next?.fields ?? [])
172
+ if (!before.has(f.name))
173
+ items.push({
174
+ op: "add",
175
+ key: fieldKey(table, f.name),
176
+ kind: "field",
177
+ table,
178
+ ddl: fieldColumnDdl(f)
179
+ });
180
+ for (const f of next?.fields ?? []) {
181
+ const b = before.get(f.name);
182
+ if (!b) continue;
183
+ if (fieldColumnDdl(b) !== fieldColumnDdl(f) || b.comment !== f.comment)
184
+ items.push({
185
+ op: "change",
186
+ key: fieldKey(table, f.name),
187
+ kind: "field",
188
+ table,
189
+ before: fieldColumnDdl(b),
190
+ after: fieldColumnDdl(f)
191
+ });
192
+ }
193
+ for (const f of prev?.fields ?? [])
194
+ if (!after.has(f.name))
195
+ items.push({
196
+ op: "remove",
197
+ key: fieldKey(table, f.name),
198
+ kind: "field",
199
+ table,
200
+ ddl: `ALTER TABLE ${escId(table)} DROP COLUMN IF EXISTS ${escId(f.name)};`,
201
+ old: fieldColumnDdl(f)
202
+ });
203
+ if (items.length === 0 && prev && next)
204
+ items.push({
205
+ op: "change",
206
+ key: `table:${table}:${table}`,
207
+ kind: "table",
208
+ table,
209
+ before: createTableDdl(createInput(prev)),
210
+ after: createTableDdl(createInput(next))
211
+ });
212
+ return items;
213
+ }
214
+ var tableEngine = {
215
+ // Objects arrive already in this kind's portable shape (from `explode`/`introspectAll` via splitTable
216
+ // / lowerSchema). `lower` is the identity — the split already produced the normalized portable object.
217
+ lower: (t) => t,
218
+ // CREATE TABLE (columns + PK + table CHECKs) followed by any column COMMENTs.
219
+ emit: (t) => [
220
+ createTableDdl({
221
+ name: t.name,
222
+ fields: t.fields,
223
+ ...t.primaryKey ? { primaryKey: t.primaryKey } : {},
224
+ ...t.checks ? { checks: t.checks } : {}
225
+ }),
226
+ ...commentLines(t)
227
+ ],
228
+ // Change-detection key (NOT the emitted DDL): the equality-relevant shape only — canonField keeps
229
+ // type/nullability/identity/FK-actions and DROPS the rewrite-prone/non-introspected clauses
230
+ // (DEFAULT/CHECK/GENERATED/COMMENT), and table-level CHECKs are omitted too. So those clauses stay
231
+ // faithful in `emit` (fresh apply) but never count as a change -> no phantom-diff of a freshly
232
+ // applied schema vs introspect (PG rewrites exprs on read; comments aren't introspected). This is
233
+ // the fixed-slot driver's emit/equal asymmetry, restored at the kind seam.
234
+ canonical: (t) => createTableDdl({
235
+ name: t.name,
236
+ fields: t.fields.map((f) => canonField(f, t.name)),
237
+ ...t.primaryKey ? { primaryKey: t.primaryKey } : {}
238
+ }),
239
+ // Per-FIELD display items (Manuel's decision: field-level changes grouped under their table). Each
240
+ // carries `table` so the CLI groups them hierarchically. DISPLAY ONLY — never affects up/down DDL.
241
+ // (prev,next): diff the columns; (undefined,next): list all columns as adds (the --full projection).
242
+ displayItems: (prev, next) => fieldDisplayItems(prev, next),
243
+ remove: (t) => [dropTableSql(t.name)],
244
+ // In-place column ALTERs (add/drop/type/nullability/default/comment). A structural change pg can't
245
+ // ALTER (PK, table CHECK, or a column's identity/generated/field-CHECK) falls back to drop+recreate.
246
+ overwrite: (prev, next) => {
247
+ const before = new Map(prev.fields.map((f) => [f.name, f]));
248
+ const after = new Map(next.fields.map((f) => [f.name, f]));
249
+ const changedHard = next.fields.some((f) => {
250
+ const b = before.get(f.name);
251
+ return b && (hasHardClause(f) || hasHardClause(b)) && !sameField(b, f);
252
+ });
253
+ const addedHard = next.fields.some(
254
+ (f) => !before.has(f.name) && hasHardClause(f)
255
+ );
256
+ const removedHard = prev.fields.some(
257
+ (f) => !after.has(f.name) && hasHardClause(f)
258
+ );
259
+ if (!sameArr(prev.primaryKey, next.primaryKey) || !sameArr(prev.checks, next.checks) || changedHard || addedHard || removedHard) {
260
+ return [...tableEngine.remove(prev), ...tableEngine.emit(next)];
261
+ }
262
+ const out = [];
263
+ const t = next.name;
264
+ for (const f of next.fields)
265
+ if (!before.has(f.name))
266
+ out.push(`ALTER TABLE ${escId(t)} ADD COLUMN ${fieldColumnDdl(f)};`);
267
+ for (const f of next.fields) {
268
+ const b = before.get(f.name);
269
+ if (!b) continue;
270
+ const ca = pgColumn(f.type);
271
+ const cb = pgColumn(b.type);
272
+ if (ca.sql !== cb.sql)
273
+ out.push(
274
+ `ALTER TABLE ${escId(t)} ALTER COLUMN ${escId(f.name)} TYPE ${ca.sql};`
275
+ );
276
+ if (ca.nullable !== cb.nullable)
277
+ out.push(
278
+ `ALTER TABLE ${escId(t)} ALTER COLUMN ${escId(f.name)} ${ca.nullable ? "DROP NOT NULL" : "SET NOT NULL"};`
279
+ );
280
+ if (f.default !== b.default)
281
+ out.push(
282
+ f.default === void 0 ? `ALTER TABLE ${escId(t)} ALTER COLUMN ${escId(f.name)} DROP DEFAULT;` : `ALTER TABLE ${escId(t)} ALTER COLUMN ${escId(f.name)} SET DEFAULT ${f.default};`
283
+ );
284
+ if (f.comment !== b.comment)
285
+ out.push(
286
+ f.comment === void 0 ? `COMMENT ON COLUMN ${escId(t)}.${escId(f.name)} IS NULL;` : `COMMENT ON COLUMN ${escId(t)}.${escId(f.name)} IS '${f.comment.replace(/'/g, "''")}';`
287
+ );
288
+ }
289
+ for (const f of prev.fields)
290
+ if (!after.has(f.name))
291
+ out.push(
292
+ `ALTER TABLE ${escId(t)} DROP COLUMN IF EXISTS ${escId(f.name)};`
293
+ );
294
+ return out;
295
+ }
296
+ };
297
+ var indexEngine = {
298
+ lower: (i) => i,
299
+ emit: (i) => [
300
+ `CREATE ${i.unique ? "UNIQUE " : ""}INDEX ${escId(i.name)} ON ${escId(i.table)} (${i.cols.map(escId).join(", ")});`
301
+ ],
302
+ remove: (i) => [`DROP INDEX IF EXISTS ${escId(i.name)};`],
303
+ // An index emits AFTER its table (deps), but NO `owner` -> no clustering: the spine then falls back
304
+ // to ordinal+name, so all indexes emit as a rank group after all tables (pg's emit convention),
305
+ // rather than clustered next to each table. owner is opt-in readability we deliberately decline.
306
+ deps: (i) => [tableRef(i.table)]
307
+ // no overwrite: an index change is a drop+recreate (the spine's default).
308
+ };
309
+ var constraintEngine = {
310
+ lower: (c) => c,
311
+ emit: (c) => [
312
+ addFkSql(
313
+ c.table,
314
+ c.column,
315
+ c.refTable,
316
+ fkActions({ on_delete: c.onDelete, on_update: c.onUpdate })
317
+ )
318
+ ],
319
+ remove: (c) => [dropFkSql(c.table, c.column)],
320
+ // A FK emits AFTER both its own table and the referenced table — this is what breaks mutual-FK
321
+ // cycles (tables have no deps, so they create first, then the constraints between them). NO
322
+ // `owner`: like the index kind, constraints emit as a rank group after all tables (pg convention).
323
+ deps: (c) => c.refTable === c.table ? [tableRef(c.table)] : [tableRef(c.table), tableRef(c.refTable)]
324
+ // no overwrite: a FK change is drop+recreate (the spine's default).
325
+ };
326
+ var registry = new KindRegistry();
327
+ registry.define({
328
+ name: "table",
329
+ build: (t) => t,
330
+ ...tableEngine
331
+ });
332
+ registry.define({
333
+ name: "index",
334
+ build: (i) => i,
335
+ ...indexEngine
336
+ });
337
+ registry.define({
338
+ name: "constraint",
339
+ build: (c) => c,
340
+ ...constraintEngine
341
+ });
342
+ function splitTable(t) {
343
+ const out = [];
344
+ const fields = pgEmitFields(t);
345
+ const table = {
346
+ kind: "table",
347
+ name: t.name,
348
+ fields,
349
+ ...t.primaryKey && t.primaryKey.length > 0 ? { primaryKey: t.primaryKey } : {},
350
+ ...t.checks && t.checks.length > 0 ? { checks: t.checks } : {}
351
+ };
352
+ out.push(table);
353
+ for (const ix of t.indexes) {
354
+ const index = {
355
+ kind: "index",
356
+ name: ix.name,
357
+ table: t.name,
358
+ cols: ix.cols,
359
+ unique: ix.unique
360
+ };
361
+ out.push(index);
362
+ }
363
+ for (const f of fields) {
364
+ const ref = pgColumn(f.type).references;
365
+ if (!ref) continue;
366
+ const onDelete = normAction(f.reference?.on_delete);
367
+ const onUpdate = normAction(f.reference?.on_update);
368
+ const fk = {
369
+ kind: "constraint",
370
+ name: fkName(t.name, f.name),
371
+ table: t.name,
372
+ ctype: "fk",
373
+ column: f.name,
374
+ refTable: ref,
375
+ ...onDelete ? { onDelete } : {},
376
+ ...onUpdate ? { onUpdate } : {}
377
+ };
378
+ out.push(fk);
379
+ }
380
+ return out;
381
+ }
382
+ var splitTables = (tables) => tables.flatMap(splitTable);
383
+
384
+ // src/lower.ts
385
+ var defOf = (schema) => schema._zod.def;
386
+ var CANON = {
387
+ text: "string",
388
+ integer: "int",
389
+ "double precision": "float",
390
+ numeric: "decimal",
391
+ boolean: "bool",
392
+ timestamptz: "datetime",
393
+ uuid: "uuid",
394
+ bytea: "bytes",
395
+ interval: "duration"
396
+ };
397
+ function tokenToPortable(type, params) {
398
+ if (type === "jsonb") return { t: "object", fields: {} };
399
+ if (params && params.length > 0)
400
+ return { t: "native", db: "postgres", name: type, params };
401
+ const scalar = CANON[type];
402
+ return scalar ? { t: "scalar", name: scalar } : { t: "native", db: "postgres", name: type };
403
+ }
404
+ function structure(schema) {
405
+ const wrappers = [];
406
+ let cur = schema;
407
+ for (; ; ) {
408
+ const def = defOf(cur);
409
+ if (def.type === "optional" && def.innerType) {
410
+ wrappers.push("option");
411
+ cur = def.innerType;
412
+ } else if (def.type === "nullable" && def.innerType) {
413
+ wrappers.push("nullable");
414
+ cur = def.innerType;
415
+ } else if (def.type === "array" && def.element) {
416
+ wrappers.push("array");
417
+ cur = def.element;
418
+ } else if ((def.type === "default" || def.type === "prefault" || def.type === "catch" || def.type === "readonly") && def.innerType) {
419
+ cur = def.innerType;
420
+ } else {
421
+ break;
422
+ }
423
+ }
424
+ return { wrappers, leaf: cur };
425
+ }
426
+ function leafPortable(meta) {
427
+ if (meta.references) return { t: "record", tables: [meta.references.table] };
428
+ if (!meta.pg) return { t: "scalar", name: "string" };
429
+ return tokenToPortable(meta.pg.type, meta.pg.params);
430
+ }
431
+ function portableType(meta, wrappers) {
432
+ let type = leafPortable(meta);
433
+ for (let i = wrappers.length - 1; i >= 0; i--) {
434
+ const w = wrappers[i];
435
+ type = w === "array" ? { t: "array", elem: type } : w === "option" ? { t: "option", inner: type } : { t: "nullable", inner: type };
436
+ }
437
+ return type;
438
+ }
439
+ function lowerField(name, table, field) {
440
+ const meta = field.native;
441
+ const { wrappers } = structure(field.schema);
442
+ const pf = {
443
+ name,
444
+ table,
445
+ type: portableType(meta, wrappers)
446
+ };
447
+ if (meta.default !== void 0) pf.default = meta.default;
448
+ if (meta.check !== void 0) pf.check = meta.check;
449
+ if (meta.generated !== void 0) pf.computed = meta.generated;
450
+ if (meta.identity !== void 0) pf.identity = meta.identity;
451
+ if (meta.comment !== void 0) pf.comment = meta.comment;
452
+ if (meta.references) {
453
+ const ref = {};
454
+ if (meta.references.onDelete) ref.on_delete = meta.references.onDelete;
455
+ if (meta.references.onUpdate) ref.on_update = meta.references.onUpdate;
456
+ if (ref.on_delete !== void 0 || ref.on_update !== void 0)
457
+ pf.reference = ref;
458
+ }
459
+ return pf;
460
+ }
461
+ function lowerTable(def) {
462
+ const fields = [];
463
+ const indexes = [];
464
+ const pkCols = [...def.config.primaryKey ?? []];
465
+ for (const [name, field] of Object.entries(def.fields)) {
466
+ fields.push(lowerField(name, def.name, field));
467
+ if (field.native.unique)
468
+ indexes.push({
469
+ name: `${def.name}_${name}_key`,
470
+ cols: [name],
471
+ unique: true
472
+ });
473
+ if (field.native.primaryKey && !pkCols.includes(name)) pkCols.push(name);
474
+ }
475
+ for (const ix of def.config.indexes ?? []) {
476
+ indexes.push({
477
+ name: ix.name ?? `${def.name}_${ix.cols.join("_")}_idx`,
478
+ cols: ix.cols,
479
+ unique: !!ix.unique
480
+ });
481
+ }
482
+ const table = { name: def.name, fields, indexes };
483
+ if (pkCols.length > 0) table.primaryKey = pkCols;
484
+ if (def.config.checks && def.config.checks.length > 0)
485
+ table.checks = def.config.checks;
486
+ return table;
487
+ }
488
+ function pgLower(tables) {
489
+ return tables.map(lowerTable);
490
+ }
491
+
492
+ // src/authoring.ts
493
+ import {
494
+ SFieldBase,
495
+ toZod
496
+ } from "@schemic/core/authoring";
497
+ import * as z from "zod";
498
+ var blankMeta = () => ({});
499
+ var sqlExpr = (sql) => ({ __sql: sql });
500
+ var isSqlExpr = (v) => typeof v === "object" && v !== null && "__sql" in v;
501
+ function pgLiteral(v) {
502
+ if (v === null) return "NULL";
503
+ if (typeof v === "number") return String(v);
504
+ if (typeof v === "boolean") return v ? "true" : "false";
505
+ return `'${v.replace(/'/g, "''")}'`;
506
+ }
507
+ var toExpr = (v) => isSqlExpr(v) ? v.__sql : pgLiteral(v);
508
+ var PgField = class _PgField extends SFieldBase {
509
+ rebuild(schema, native) {
510
+ return new _PgField(schema, native);
511
+ }
512
+ blank() {
513
+ return blankMeta();
514
+ }
515
+ with(meta) {
516
+ return new _PgField(this.schema, { ...this.native, ...meta });
517
+ }
518
+ // --- pg-native `$`-methods (DDL authoring) ---
519
+ /** `DEFAULT <value>` — a JS literal, or `sqlExpr("now()")` for a raw SQL default. */
520
+ $default(value) {
521
+ return this.with({ default: toExpr(value) });
522
+ }
523
+ /** Field-level `CHECK (<expr>)`. */
524
+ $check(expr) {
525
+ return this.with({ check: isSqlExpr(expr) ? expr.__sql : expr });
526
+ }
527
+ /** `GENERATED ALWAYS AS (<expr>) STORED` — a computed column. */
528
+ $generated(expr) {
529
+ return this.with({ generated: isSqlExpr(expr) ? expr.__sql : expr });
530
+ }
531
+ /** `GENERATED {ALWAYS|BY DEFAULT} AS IDENTITY` (auto-increment). */
532
+ $identity(mode = "by-default") {
533
+ return this.with({ identity: mode });
534
+ }
535
+ /** Column-level `UNIQUE`. */
536
+ $unique() {
537
+ return this.with({ unique: true });
538
+ }
539
+ /** Mark this column (part of) the PRIMARY KEY. */
540
+ $primaryKey() {
541
+ return this.with({ primaryKey: true });
542
+ }
543
+ /** Foreign key to `table(id)` with optional `ON DELETE`/`ON UPDATE` actions. */
544
+ $references(table, opts) {
545
+ return this.with({ references: { table, ...opts ?? {} } });
546
+ }
547
+ /** `COMMENT ON COLUMN`. */
548
+ $comment(text) {
549
+ return this.with({ comment: text });
550
+ }
551
+ /**
552
+ * ESCAPE HATCH (chainable form) — teach the driver how to STORE this field's value in Postgres:
553
+ * give the **wire type** as an `s.*`/Zod field (its pg column type is taken from it) plus a codec
554
+ * (`encode`: app -> wire, `decode`: wire -> app). This turns an otherwise-unmappable App value
555
+ * (e.g. `s.instanceof(Money)`) into a real pg column. Omit the codec for an identity mapping (the
556
+ * app value is stored as-is). Mirrors SurrealDB's `.$surreal(wire, codec)`; the standalone
557
+ * {@link s.$postgres} factory is the from-scratch equivalent. `$`-prefixed to avoid clashing with Zod.
558
+ */
559
+ $postgres(wire, codec2) {
560
+ const wireSchema = toZod(wire);
561
+ const c = z.codec(wireSchema, this.schema, {
562
+ decode: (w) => codec2 ? codec2.decode(w) : w,
563
+ encode: (a) => codec2 ? codec2.encode(a) : a
564
+ });
565
+ const wirePg = wire instanceof _PgField ? wire.native.pg : void 0;
566
+ return new _PgField(c, {
567
+ ...this.native,
568
+ ...wirePg ? { pg: wirePg } : {}
569
+ });
570
+ }
571
+ };
572
+ var mk = (type, schema, params) => new PgField(schema, { pg: params ? { type, params } : { type } });
573
+ var s = {
574
+ // Zod drop-ins (the canonical superset; each maps to a sensible pg default). Native aliases below
575
+ // (text/varchar/int/numeric/…) give precise control.
576
+ string: () => mk("text", z.string()),
577
+ number: () => mk("double precision", z.number()),
578
+ // text
579
+ text: () => mk("text", z.string()),
580
+ varchar: (n) => n === void 0 ? mk("varchar", z.string()) : mk("varchar", z.string().max(n), [n]),
581
+ char: (n) => n === void 0 ? mk("char", z.string()) : mk("char", z.string(), [n]),
582
+ citext: () => mk("citext", z.string()),
583
+ // numeric
584
+ smallint: () => mk("smallint", z.int().gte(-32768).lte(32767)),
585
+ integer: () => mk("integer", z.int()),
586
+ int: () => mk("integer", z.int()),
587
+ bigint: () => mk("bigint", z.int()),
588
+ serial: () => mk("integer", z.int()).$identity("by-default"),
589
+ bigserial: () => mk("bigint", z.int()).$identity("by-default"),
590
+ numeric: (precision, scale) => precision === void 0 ? mk("numeric", z.number()) : (
591
+ // Postgres stores `numeric(p)` as `numeric(p,0)`; keep scale explicit so it round-trips.
592
+ mk("numeric", z.number(), [precision, scale ?? 0])
593
+ ),
594
+ decimal: (precision, scale) => s.numeric(precision, scale),
595
+ real: () => mk("real", z.number()),
596
+ doublePrecision: () => mk("double precision", z.number()),
597
+ float: () => mk("double precision", z.number()),
598
+ money: () => mk("money", z.string()),
599
+ // boolean
600
+ boolean: () => mk("boolean", z.boolean()),
601
+ bool: () => mk("boolean", z.boolean()),
602
+ // temporal
603
+ timestamptz: () => mk("timestamptz", z.date()),
604
+ timestamp: () => mk("timestamp", z.date()),
605
+ date: () => mk("date", z.date()),
606
+ time: () => mk("time", z.string()),
607
+ timetz: () => mk("timetz", z.string()),
608
+ interval: () => mk("interval", z.string()),
609
+ // identity / network / uuid / bytes
610
+ uuid: () => mk("uuid", z.uuid()),
611
+ bytea: () => mk("bytea", z.instanceof(Uint8Array)),
612
+ inet: () => mk("inet", z.string()),
613
+ cidr: () => mk("cidr", z.string()),
614
+ macaddr: () => mk("macaddr", z.string()),
615
+ // json
616
+ jsonb: (shape) => mk("jsonb", shape ?? z.unknown()),
617
+ json: (shape) => mk("json", shape ?? z.unknown()),
618
+ // enum (string-literal union -> text) and single literal
619
+ enum: (values) => mk("text", z.enum(values)),
620
+ literal: (value) => mk(
621
+ typeof value === "number" ? "double precision" : typeof value === "boolean" ? "boolean" : "text",
622
+ z.literal(value)
623
+ ),
624
+ // object -> jsonb (opaque on disk). Accepts field OR raw-Zod values (a Zod drop-in superset).
625
+ object: (shape) => mk(
626
+ "jsonb",
627
+ z.object(
628
+ Object.fromEntries(
629
+ Object.entries(shape).map(([k, v]) => [k, toZod(v)])
630
+ )
631
+ )
632
+ ),
633
+ // array(elem) -> `<elem>[]`; carries the element's pg metadata so it lowers to an array of that type.
634
+ array: (elem) => new PgField(
635
+ z.array(toZod(elem)),
636
+ elem instanceof PgField ? elem.native : {}
637
+ ),
638
+ // foreign key: `text` column + FK to `table(id)`
639
+ references: (table, opts) => new PgField(z.string(), {
640
+ pg: { type: "text" },
641
+ references: { table, ...opts ?? {} }
642
+ }),
643
+ /**
644
+ * ESCAPE HATCH — a pg type with no portable meaning, stored via a Zod codec (encode/decode). The
645
+ * column is emitted as `pgType`; App-side reads/writes go through `codec`. Mirrors surreal `$surreal`.
646
+ */
647
+ $postgres: (pgType, codec2) => new PgField(codec2, { pg: { type: pgType } })
648
+ };
649
+ var PgTableDef = class _PgTableDef {
650
+ constructor(name, fields, config = {}) {
651
+ this.name = name;
652
+ this.fields = fields;
653
+ this.config = config;
654
+ }
655
+ /** Composite / custom PRIMARY KEY (overrides the implicit `id`). */
656
+ primaryKey(...cols) {
657
+ return new _PgTableDef(this.name, this.fields, {
658
+ ...this.config,
659
+ primaryKey: cols
660
+ });
661
+ }
662
+ /** A table-level `CHECK (<expr>)`. */
663
+ check(expr) {
664
+ return new _PgTableDef(this.name, this.fields, {
665
+ ...this.config,
666
+ checks: [...this.config.checks ?? [], expr]
667
+ });
668
+ }
669
+ /** A secondary index over `cols` (optionally `UNIQUE`). */
670
+ index(cols, opts) {
671
+ return new _PgTableDef(this.name, this.fields, {
672
+ ...this.config,
673
+ indexes: [...this.config.indexes ?? [], { cols, ...opts ?? {} }]
674
+ });
675
+ }
676
+ /**
677
+ * A foreign-key field referencing THIS table (for use in another table's shape):
678
+ * `author: user.record({ onDelete: "cascade" })`. Also satisfies the CLI's structural table check.
679
+ */
680
+ record(opts) {
681
+ return s.references(this.name, opts);
682
+ }
683
+ };
684
+ function defineTable(name, fields, config) {
685
+ return new PgTableDef(name, fields, config ?? {});
686
+ }
687
+
688
+ // src/index.ts
689
+ var nativeT = (name, params) => params && params.length > 0 ? { t: "native", db: "postgres", name, params } : { t: "native", db: "postgres", name };
690
+ var scalarT = (name) => ({ t: "scalar", name });
691
+ var DATATYPE_TO_SCALAR = {
692
+ text: "string",
693
+ integer: "int",
694
+ "double precision": "float",
695
+ boolean: "bool",
696
+ uuid: "uuid",
697
+ bytea: "bytes",
698
+ interval: "duration"
699
+ };
700
+ var DATATYPE_TO_NATIVE = {
701
+ "time without time zone": "time",
702
+ "time with time zone": "timetz"
703
+ };
704
+ function introspectType(c) {
705
+ const dt = c.data_type;
706
+ if (dt === "ARRAY") {
707
+ return { t: "array", elem: pgScalarFromUdt(c.udt_name.replace(/^_/, "")) };
708
+ }
709
+ if (dt === "jsonb") return { t: "object", fields: {} };
710
+ if (dt === "json") return nativeT("json");
711
+ if (dt === "character varying")
712
+ return c.character_maximum_length != null ? nativeT("varchar", [c.character_maximum_length]) : nativeT("varchar");
713
+ if (dt === "character")
714
+ return c.character_maximum_length != null ? nativeT("char", [c.character_maximum_length]) : nativeT("char");
715
+ if (dt === "numeric")
716
+ return c.numeric_precision != null ? nativeT("numeric", [c.numeric_precision, c.numeric_scale ?? 0]) : scalarT("decimal");
717
+ if (dt === "timestamp without time zone") return nativeT("timestamp");
718
+ if (dt === "timestamp with time zone") return scalarT("datetime");
719
+ const sc = DATATYPE_TO_SCALAR[dt];
720
+ if (sc) return scalarT(sc);
721
+ return nativeT(DATATYPE_TO_NATIVE[dt] ?? dt);
722
+ }
723
+ var UDT_TO_SCALAR = {
724
+ text: "string",
725
+ int4: "int",
726
+ float8: "float",
727
+ numeric: "decimal",
728
+ bool: "bool",
729
+ timestamptz: "datetime",
730
+ uuid: "uuid",
731
+ bytea: "bytes",
732
+ interval: "duration"
733
+ };
734
+ function pgScalarFromUdt(udt) {
735
+ const name = UDT_TO_SCALAR[udt];
736
+ return name ? { t: "scalar", name } : { t: "native", db: "postgres", name: udt };
737
+ }
738
+ async function pgIntrospect(conn, exclude = /* @__PURE__ */ new Set()) {
739
+ const skip = new Set(exclude);
740
+ for (const t of exclude) skip.add(`${t}_lock`);
741
+ const { rows: cols } = await conn.query(
742
+ `SELECT table_name, column_name, data_type, udt_name, is_nullable,
743
+ is_identity, identity_generation,
744
+ character_maximum_length, numeric_precision, numeric_scale
745
+ FROM information_schema.columns
746
+ WHERE table_schema = 'public'
747
+ ORDER BY table_name, ordinal_position`
748
+ );
749
+ const { rows: fks } = await conn.query(
750
+ `SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table_name,
751
+ rc.delete_rule, rc.update_rule
752
+ FROM information_schema.table_constraints tc
753
+ JOIN information_schema.key_column_usage kcu
754
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
755
+ JOIN information_schema.constraint_column_usage ccu
756
+ ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
757
+ JOIN information_schema.referential_constraints rc
758
+ ON rc.constraint_name = tc.constraint_name AND rc.constraint_schema = tc.table_schema
759
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`
760
+ );
761
+ const { rows: pks } = await conn.query(
762
+ `SELECT tc.table_name, kcu.column_name
763
+ FROM information_schema.table_constraints tc
764
+ JOIN information_schema.key_column_usage kcu
765
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
766
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'
767
+ ORDER BY kcu.ordinal_position`
768
+ );
769
+ const { rows: idxs } = await conn.query(
770
+ `SELECT t.relname AS table_name, i.relname AS index_name, a.attname AS column_name,
771
+ ix.indisunique AS is_unique
772
+ FROM pg_class t
773
+ JOIN pg_namespace n ON n.oid = t.relnamespace AND n.nspname = 'public'
774
+ JOIN pg_index ix ON ix.indrelid = t.oid AND NOT ix.indisprimary
775
+ AND ix.indpred IS NULL AND ix.indexprs IS NULL
776
+ JOIN pg_class i ON i.oid = ix.indexrelid
777
+ JOIN pg_am am ON am.oid = i.relam AND am.amname = 'btree'
778
+ JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[])
779
+ WITH ORDINALITY AS k(attnum, ord) ON true
780
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
781
+ WHERE t.relkind = 'r'
782
+ ORDER BY t.relname, i.relname, k.ord`
783
+ );
784
+ const fkBy = /* @__PURE__ */ new Map();
785
+ for (const f of fks) fkBy.set(`${f.table_name}.${f.column_name}`, f);
786
+ const pkBy = /* @__PURE__ */ new Map();
787
+ for (const p of pks) {
788
+ const list = pkBy.get(p.table_name) ?? [];
789
+ list.push(p.column_name);
790
+ pkBy.set(p.table_name, list);
791
+ }
792
+ const idColBy = /* @__PURE__ */ new Map();
793
+ for (const c of cols)
794
+ if (c.column_name === "id") idColBy.set(c.table_name, c);
795
+ const isImplicit = (table) => {
796
+ const pk = pkBy.get(table);
797
+ if (!(pk?.length === 1 && pk[0] === "id")) return false;
798
+ const id = idColBy.get(table);
799
+ return !!id && id.data_type === "text" && id.is_identity !== "YES";
800
+ };
801
+ const seen = /* @__PURE__ */ new Set();
802
+ const byTable = /* @__PURE__ */ new Map();
803
+ for (const c of cols) {
804
+ if (skip.has(c.table_name)) continue;
805
+ seen.add(c.table_name);
806
+ if (c.column_name === "id" && isImplicit(c.table_name)) continue;
807
+ const fk = fkBy.get(`${c.table_name}.${c.column_name}`);
808
+ let type = fk ? { t: "record", tables: [fk.foreign_table_name] } : introspectType(c);
809
+ if (c.is_nullable === "YES") type = nullable2(type);
810
+ const pf = {
811
+ name: c.column_name,
812
+ table: c.table_name,
813
+ type
814
+ };
815
+ if (c.is_identity === "YES")
816
+ pf.identity = c.identity_generation === "ALWAYS" ? "always" : "by-default";
817
+ if (fk) {
818
+ const ref = {};
819
+ if (fk.delete_rule && fk.delete_rule !== "NO ACTION")
820
+ ref.on_delete = fk.delete_rule;
821
+ if (fk.update_rule && fk.update_rule !== "NO ACTION")
822
+ ref.on_update = fk.update_rule;
823
+ if (ref.on_delete !== void 0 || ref.on_update !== void 0)
824
+ pf.reference = ref;
825
+ }
826
+ const list = byTable.get(c.table_name) ?? [];
827
+ list.push(pf);
828
+ byTable.set(c.table_name, list);
829
+ }
830
+ const idxBy = /* @__PURE__ */ new Map();
831
+ for (const r of idxs) {
832
+ if (skip.has(r.table_name)) continue;
833
+ const byName = idxBy.get(r.table_name) ?? /* @__PURE__ */ new Map();
834
+ const ix = byName.get(r.index_name) ?? {
835
+ name: r.index_name,
836
+ cols: [],
837
+ unique: r.is_unique
838
+ };
839
+ ix.cols.push(r.column_name);
840
+ byName.set(r.index_name, ix);
841
+ idxBy.set(r.table_name, byName);
842
+ }
843
+ return [...seen].map((name) => {
844
+ const t = {
845
+ name,
846
+ fields: byTable.get(name) ?? [],
847
+ indexes: [...idxBy.get(name)?.values() ?? []]
848
+ };
849
+ if (!isImplicit(name)) {
850
+ const pk = pkBy.get(name);
851
+ if (pk && pk.length > 0) t.primaryKey = pk;
852
+ }
853
+ return t;
854
+ });
855
+ }
856
+ async function newPglite(dataDir) {
857
+ const pkg = "@electric-sql/pglite";
858
+ let PGlite;
859
+ try {
860
+ const mod = await import(pkg);
861
+ PGlite = mod.PGlite;
862
+ } catch {
863
+ PGlite = void 0;
864
+ }
865
+ if (!PGlite) {
866
+ throw new Error(
867
+ "postgres driver needs `@electric-sql/pglite` (embedded) \u2014 install it, or wire a node-postgres client."
868
+ );
869
+ }
870
+ return new PGlite(dataDir);
871
+ }
872
+ var shadow = {
873
+ // A throwaway in-memory PGlite IS the shadow: apply the DDL, read it back as kind objects, done (no
874
+ // drop needed — the instance is discarded). This is the "embedded engine" canonicalization path.
875
+ async roundTrip(_conn, _config, ddl) {
876
+ const scratch = await newPglite();
877
+ try {
878
+ if (ddl.trim()) await scratch.exec(ddl);
879
+ return splitTables(await pgIntrospect(scratch));
880
+ } finally {
881
+ await scratch.close();
882
+ }
883
+ },
884
+ async ephemeral() {
885
+ const conn = await newPglite();
886
+ return { conn, stop: () => conn.close() };
887
+ }
888
+ };
889
+ var isFragment = (v) => typeof v === "object" && v !== null && "__pgRaw" in v;
890
+ var isBound = (v) => typeof v === "object" && v !== null && typeof v.query === "string" && Array.isArray(v.params);
891
+ function raw(sql) {
892
+ return { __pgRaw: sql };
893
+ }
894
+ function identifier(name) {
895
+ return { __pgRaw: escId(name) };
896
+ }
897
+ function pgSql(strings, ...values) {
898
+ let query = "";
899
+ const params = [];
900
+ strings.forEach((str, i) => {
901
+ query += str;
902
+ if (i >= values.length) return;
903
+ const v = values[i];
904
+ if (isFragment(v)) {
905
+ query += v.__pgRaw;
906
+ } else if (isBound(v)) {
907
+ query += v.query.replace(
908
+ /\$(\d+)/g,
909
+ (_m, n) => `$${params.length + Number(n)}`
910
+ );
911
+ params.push(...v.params);
912
+ } else {
913
+ params.push(v);
914
+ query += `$${params.length}`;
915
+ }
916
+ });
917
+ return { query, params };
918
+ }
919
+ function postgresConnection(input) {
920
+ return connectionEntry("postgres", input);
921
+ }
922
+ var MIG_UP = "-- schemic:up";
923
+ var MIG_DOWN = "-- schemic:down";
924
+ function renderMigration(_tag, diff) {
925
+ return `${MIG_UP}
926
+ ${diff.up.join("\n")}
927
+
928
+ ${MIG_DOWN}
929
+ ${diff.down.join("\n")}
930
+ `;
931
+ }
932
+ function migSection(content, direction) {
933
+ const up = content.indexOf(MIG_UP);
934
+ const down = content.indexOf(MIG_DOWN);
935
+ if (up === -1 || down === -1) return direction === "up" ? content : "";
936
+ return direction === "up" ? content.slice(up + MIG_UP.length, down) : content.slice(down + MIG_DOWN.length);
937
+ }
938
+ var sqlStr = (v) => `'${v.replace(/'/g, "''")}'`;
939
+ var lockTableOf = (table) => `${table}_lock`;
940
+ async function ensureMigTable(conn, table) {
941
+ await conn.exec(
942
+ `CREATE TABLE IF NOT EXISTS ${escId(table)} (
943
+ ${escId("tag")} text PRIMARY KEY,
944
+ ${escId("file")} text NOT NULL,
945
+ ${escId("checksum")} text NOT NULL,
946
+ ${escId("applied_at")} timestamptz NOT NULL DEFAULT now()
947
+ );`
948
+ );
949
+ }
950
+ var recordInsert = (table, r) => `INSERT INTO ${escId(table)} (${escId("tag")}, ${escId("file")}, ${escId("checksum")}) VALUES (${sqlStr(r.tag)}, ${sqlStr(r.file)}, ${sqlStr(r.checksum)});`;
951
+ var migrations = {
952
+ extension: ".sql",
953
+ render: renderMigration,
954
+ ensure: ensureMigTable,
955
+ async applied(conn, table) {
956
+ const { rows } = await conn.query(
957
+ `SELECT ${escId("tag")}, ${escId("checksum")} FROM ${escId(table)};`
958
+ );
959
+ return new Map(rows.map((r) => [r.tag, r.checksum]));
960
+ },
961
+ // Postgres runs DDL inside a transaction, so the migration's section + its bookkeeping write commit
962
+ // atomically — the record lands iff the DDL applied.
963
+ async apply(conn, table, { content, direction, record }) {
964
+ const ddl = migSection(content, direction).trim();
965
+ const book = direction === "up" ? recordInsert(table, record) : `DELETE FROM ${escId(table)} WHERE ${escId("tag")} = ${sqlStr(record.tag)};`;
966
+ await conn.exec(`BEGIN;
967
+ ${ddl ? `${ddl}
968
+ ` : ""}${book}
969
+ COMMIT;`);
970
+ },
971
+ async record(conn, table, record) {
972
+ await ensureMigTable(conn, table);
973
+ await conn.exec(recordInsert(table, record));
974
+ },
975
+ async clear(conn, table) {
976
+ await conn.exec(`DELETE FROM ${escId(table)};`);
977
+ },
978
+ // A persisted lock ROW (survives across separate CLI runs on a file-based PGlite, unlike a session
979
+ // advisory lock). The PK collision on a held lock is the "already locked" signal.
980
+ async lock(conn, table) {
981
+ const lt = lockTableOf(table);
982
+ await conn.exec(
983
+ `CREATE TABLE IF NOT EXISTS ${escId(lt)} (${escId("id")} int PRIMARY KEY);`
984
+ );
985
+ try {
986
+ await conn.exec(`INSERT INTO ${escId(lt)} (${escId("id")}) VALUES (1);`);
987
+ } catch {
988
+ throw new Error(
989
+ "Migrations are locked \u2014 another run is in progress. If it's stale, run `schemic unlock`."
990
+ );
991
+ }
992
+ },
993
+ async unlock(conn, table) {
994
+ const lt = lockTableOf(table);
995
+ await conn.exec(
996
+ `CREATE TABLE IF NOT EXISTS ${escId(lt)} (${escId("id")} int PRIMARY KEY);
997
+ DELETE FROM ${escId(lt)} WHERE ${escId("id")} = 1;`
998
+ );
999
+ }
1000
+ };
1001
+ var postgresDriver = {
1002
+ name: "postgres",
1003
+ // The kind registry (table/index/constraint) — core runs lower/diff/emit/order generically over it.
1004
+ registry,
1005
+ // Authoring (pg-native `defineTable` -> PgTableDef) -> kinded Definables: lower each table to the
1006
+ // driver's `PgTable` IR (./lower.ts), then split it into [table, ...index, ...constraint] objects
1007
+ // (./kinds.ts splitTable). Core then runs lowerSchema(registry, explode(...)). pg has no standalone
1008
+ // defs, so `defs` is unused.
1009
+ explode: (tables) => splitTables(pgLower(tables)),
1010
+ // One information_schema/pg_catalog read -> ALL kind objects, canonicalized identically to lowering
1011
+ // (a clean apply round-trips to a zero diff) and complete (table + index + FK), so no phantom diffs.
1012
+ introspectAll: async (conn, exclude) => splitTables(await pgIntrospect(conn, exclude)),
1013
+ /**
1014
+ * Raw READ query for connection RESOLVERS + seed (returns rows opaquely). Postgres binds
1015
+ * POSITIONALLY, so the uniform `vars` record is mapped onto `$1..$n`: a string with NAMED `$name`
1016
+ * placeholders + `vars` is rewritten to positional `$1..$n` with `vars` bound by name (never
1017
+ * string-interpolated); native numeric `$1` is left untouched; no `vars` -> run as-is. To build a
1018
+ * query safely from interpolated values, use {@link pgSql} (positional) and run it via the raw
1019
+ * connection: `conn.query(q.query, q.params)` — that also avoids rewriting `$` inside string literals.
1020
+ */
1021
+ async query(conn, sql, vars) {
1022
+ if (!vars || Object.keys(vars).length === 0) {
1023
+ return (await conn.query(sql)).rows;
1024
+ }
1025
+ const params = [];
1026
+ const text = sql.replace(
1027
+ /\$([A-Za-z_][A-Za-z0-9_]*)/g,
1028
+ (_m, name) => {
1029
+ if (!(name in vars)) {
1030
+ throw new Error(`postgres query: no binding for $${name}`);
1031
+ }
1032
+ params.push(vars[name]);
1033
+ return `$${params.length}`;
1034
+ }
1035
+ );
1036
+ return (await conn.query(text, params)).rows;
1037
+ },
1038
+ connect(config, _over) {
1039
+ const url = typeof config.params.url === "string" ? config.params.url : "";
1040
+ const dir = url.startsWith("file:") ? url.slice("file:".length) : void 0;
1041
+ return newPglite(dir);
1042
+ },
1043
+ async apply(conn, statements, opts) {
1044
+ if (!statements.length) return;
1045
+ const body = statements.join("\n");
1046
+ if (opts?.transactional === false) {
1047
+ await conn.exec(body);
1048
+ return;
1049
+ }
1050
+ await conn.exec(`BEGIN;
1051
+ ${body}
1052
+ COMMIT;`);
1053
+ },
1054
+ close(conn) {
1055
+ return conn.close();
1056
+ },
1057
+ // Apply-time migration bookkeeping (the `_migrations` table SQL behind migrate/rollback/status).
1058
+ migrations,
1059
+ // `schemic init --driver postgres` scaffolds a real connections-only pg project from these files
1060
+ // (the CLI adds the neutral migration snapshot).
1061
+ initScaffold: () => ({
1062
+ "schemic.config.ts": INIT_CONFIG_TS,
1063
+ "database/schema/tables.ts": INIT_SCHEMA_TS,
1064
+ "database/seed.ts": INIT_SEED_TS,
1065
+ ".env.example": INIT_ENV
1066
+ }),
1067
+ // `schemic new <kind> <name>` -> the starter authoring module for a new entity. pg's only
1068
+ // standalone definable is the `table`; indexes/FKs are authored INSIDE a table, so those kinds
1069
+ // throw with guidance. The CLI writes the returned text under registry.display(kind).folder.
1070
+ scaffoldEntity: (kind, name) => scaffoldPgEntity(kind, name),
1071
+ shadow
1072
+ };
1073
+ var INIT_CONFIG_TS = `import { defineConfig } from "@schemic/core/config";
1074
+ import { postgresConnection } from "@schemic/postgres";
1075
+
1076
+ // Connections-only config: a map of named connections, each from a driver factory. Values are
1077
+ // explicit \u2014 read env yourself (no magic env vars).
1078
+ export default defineConfig({
1079
+ connections: {
1080
+ default: postgresConnection({
1081
+ schema: "./database/schema",
1082
+ // PGlite (embedded): \`file:<dir>\` is a persistent data dir; "" is in-memory. Point
1083
+ // DATABASE_URL at a real server (\`postgres://\u2026\`) once the node-postgres client lands.
1084
+ url: process.env.DATABASE_URL ?? "file:./.pgdata",
1085
+ }),
1086
+ },
1087
+ });
1088
+ `;
1089
+ var INIT_SCHEMA_TS = `import { defineTable, s, sqlExpr } from "@schemic/postgres";
1090
+
1091
+ export const user = defineTable("user", {
1092
+ email: s.varchar(255).$unique(),
1093
+ name: s.text(),
1094
+ age: s.smallint().optional(),
1095
+ createdAt: s.timestamptz().$default(sqlExpr("now()")),
1096
+ });
1097
+ `;
1098
+ var INIT_SEED_TS = `// Seed script \u2014 run with \`schemic seed\`. Receives the live connection(s).
1099
+ export default async function seed() {
1100
+ // await conn.query("INSERT INTO ...");
1101
+ }
1102
+ `;
1103
+ var INIT_ENV = `# A real Postgres server (uncomment to use instead of embedded PGlite):
1104
+ # DATABASE_URL=postgres://user:pass@localhost:5432/app
1105
+ `;
1106
+ function toIdentifier(name) {
1107
+ const camel = name.replace(
1108
+ /[^a-zA-Z0-9_$]+([a-zA-Z0-9])?/g,
1109
+ (_m, c) => c ? c.toUpperCase() : ""
1110
+ );
1111
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel || "entity";
1112
+ }
1113
+ function scaffoldTable(name) {
1114
+ const ident = toIdentifier(name);
1115
+ return `import { defineTable, s, sqlExpr } from "@schemic/postgres";
1116
+
1117
+ // \`sc new table ${name}\` scaffolded this. Author your columns, then \`sc gen\`.
1118
+ export const ${ident} = defineTable(${JSON.stringify(name)}, {
1119
+ // An implicit \`"id" text PRIMARY KEY\` is added unless you declare a PK below.
1120
+ name: s.text(),
1121
+ // email: s.varchar(255).$unique(), // -> UNIQUE INDEX
1122
+ // age: s.smallint().optional(), // -> nullable column
1123
+ // status: s.text().$check("status in ('active', 'archived')"), // -> CHECK constraint
1124
+ // owner: s.references("other_table", { onDelete: "cascade" }), // -> FOREIGN KEY
1125
+ createdAt: s.timestamptz().$default(sqlExpr("now()")),
1126
+ });
1127
+ // Table-level options (chain onto defineTable(...) above):
1128
+ // .primaryKey("a", "b") composite PK (drops the implicit id)
1129
+ // .check("age >= 0") table-level CHECK
1130
+ // .index(["name"]) secondary index (add { unique: true } for UNIQUE)
1131
+ `;
1132
+ }
1133
+ function scaffoldPgEntity(kind, name) {
1134
+ switch (kind) {
1135
+ case "table":
1136
+ return scaffoldTable(name);
1137
+ case "index":
1138
+ case "constraint":
1139
+ throw new Error(
1140
+ `postgres: "${kind}" isn't a standalone entity \u2014 indexes and foreign keys are authored inside a table (defineTable(...).index([...]) / s.references(...)). Run \`sc new table <name>\`.`
1141
+ );
1142
+ default:
1143
+ throw new Error(
1144
+ `postgres: unknown entity kind "${kind}" \u2014 pg scaffolds: table.`
1145
+ );
1146
+ }
1147
+ }
1148
+ registerDriver(postgresDriver);
1149
+ export {
1150
+ PgField,
1151
+ PgTableDef,
1152
+ defineTable,
1153
+ identifier,
1154
+ pgSql,
1155
+ postgresConnection,
1156
+ postgresDriver,
1157
+ raw,
1158
+ s,
1159
+ sqlExpr
1160
+ };
1161
+ //# sourceMappingURL=index.js.map