@supawatch/target-seed 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Omar Dulaimi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @supawatch/target-seed
2
+
3
+ The seed target for
4
+ [supawatch](https://github.com/omar-dulaimi/supawatch). Emits `seed.sql`:
5
+ deterministic, FK-aware seed rows. Parents insert before children,
6
+ identity columns get explicit ids via `OVERRIDING SYSTEM VALUE`,
7
+ sequences are resynced with `setval`, enum labels are real, and the byte
8
+ output is stable for an unchanged schema, so the file diffs like code.
9
+
10
+ ```ts
11
+ targets: [{ kind: "seed", rows: 3 }]
12
+ ```
13
+
14
+ Drop the output at `supabase/seed.sql` and `supabase db reset` applies
15
+ it. The repo's suite proves the hard parts against a real empty
16
+ database: an identity-always bigint parent, a uuid parent, FK chains
17
+ with a nullable edge, and a post-seed insert that does not collide
18
+ because sequences were resynced. Seeded rows are ground-truth checked
19
+ against the generated Zod schemas.
20
+
21
+ Honest limits, emitted as comments rather than guesses: tables whose
22
+ required foreign keys form a cycle, multi-column foreign keys, and
23
+ required columns with no default whose types have no honest literal
24
+ (composites, unknown types) are skipped by name.
25
+
26
+ MIT.
@@ -0,0 +1,13 @@
1
+ import type { Snapshot, SnapshotFile, Target, TargetCapabilities, TargetOptions } from "@supawatch/core";
2
+ export interface SeedTargetOptions extends TargetOptions {
3
+ rows?: number;
4
+ }
5
+ export declare class SeedTarget implements Target<SeedTargetOptions> {
6
+ readonly name = "seed";
7
+ readonly fileExtension = ".sql";
8
+ readonly barrel = false;
9
+ readonly capabilities: TargetCapabilities;
10
+ renderTable(): never;
11
+ renderSnapshot(snapshot: Snapshot, opts: SeedTargetOptions): SnapshotFile[];
12
+ }
13
+ export default SeedTarget;
package/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ // Deterministic PRNG (mulberry32), seeded per column so output is
2
+ // stable regardless of table iteration order changes.
3
+ function mulberry32(seed) {
4
+ let a = seed >>> 0;
5
+ return () => {
6
+ a |= 0;
7
+ a = (a + 0x6d2b79f5) | 0;
8
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
9
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
10
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
11
+ };
12
+ }
13
+ function hashString(s) {
14
+ let h = 2166136261;
15
+ for (let i = 0; i < s.length; i++) {
16
+ h ^= s.charCodeAt(i);
17
+ h = Math.imul(h, 16777619);
18
+ }
19
+ return h >>> 0;
20
+ }
21
+ function sqlString(s) {
22
+ return "'" + s.replace(/'/g, "''") + "'";
23
+ }
24
+ function deterministicUuid(rand) {
25
+ const hex = () => Math.floor(rand() * 16).toString(16);
26
+ const s = (n) => Array.from({ length: n }, hex).join("");
27
+ // version 4 and variant bits set, so validators accept it
28
+ return `${s(8)}-${s(4)}-4${s(3)}-${"89ab"[Math.floor(rand() * 4)]}${s(3)}-${s(12)}`;
29
+ }
30
+ function literalFor(runtime, col, table, rowIndex, rand) {
31
+ switch (runtime.kind) {
32
+ case "number":
33
+ return runtime.integer
34
+ ? String(1 + Math.floor(rand() * 1000))
35
+ : (rand() * 100).toFixed(2);
36
+ case "string":
37
+ switch (runtime.format) {
38
+ case "uuid":
39
+ return sqlString(deterministicUuid(rand));
40
+ case "numeric":
41
+ return sqlString((rand() * 1000).toFixed(2));
42
+ case "bigint":
43
+ return sqlString(String(1 + Math.floor(rand() * 100000)));
44
+ case "composite":
45
+ case "array-literal":
46
+ return null; // cannot construct honestly from here
47
+ default:
48
+ return sqlString(`${table.name} ${col.name} ${rowIndex + 1}`);
49
+ }
50
+ case "boolean":
51
+ return rand() < 0.5 ? "true" : "false";
52
+ case "date": {
53
+ const day = 1 + Math.floor(rand() * 27);
54
+ const month = 1 + Math.floor(rand() * 12);
55
+ return sqlString(`2026-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T09:00:00Z`);
56
+ }
57
+ case "bytes":
58
+ return "'\\x00'";
59
+ case "json":
60
+ return sqlString("{}") + "::jsonb";
61
+ case "enum": {
62
+ const label = runtime.labels[rowIndex % runtime.labels.length];
63
+ return sqlString(label) + `::"${col.pgTypeName.replace(/^_/, "")}"`;
64
+ }
65
+ case "array": {
66
+ const el = literalFor(runtime.element, col, table, rowIndex, rand);
67
+ if (el === null)
68
+ return null;
69
+ return `array[${el}]`;
70
+ }
71
+ case "unknown":
72
+ return null;
73
+ }
74
+ }
75
+ // Kahn's algorithm over single-column FK edges; nullable-FK edges are
76
+ // soft (broken first on cycles, seeded as null).
77
+ function topoSort(tables) {
78
+ const byName = new Map(tables.map((t) => [`${t.schema}.${t.name}`, t]));
79
+ const deps = new Map();
80
+ for (const t of tables) {
81
+ const key = `${t.schema}.${t.name}`;
82
+ const set = new Set();
83
+ for (const fk of t.foreignKeys) {
84
+ const target = `${fk.referencedSchema}.${fk.referencedTable}`;
85
+ const col = t.columns.find((c) => c.name === fk.columns[0]);
86
+ if (target !== key && byName.has(target) && col && !col.nullable) {
87
+ set.add(target);
88
+ }
89
+ }
90
+ deps.set(key, set);
91
+ }
92
+ const ordered = [];
93
+ const done = new Set();
94
+ let progress = true;
95
+ while (progress) {
96
+ progress = false;
97
+ for (const t of tables) {
98
+ const key = `${t.schema}.${t.name}`;
99
+ if (done.has(key))
100
+ continue;
101
+ const remaining = [...(deps.get(key) ?? [])].filter((d) => !done.has(d));
102
+ if (remaining.length === 0) {
103
+ ordered.push(t);
104
+ done.add(key);
105
+ progress = true;
106
+ }
107
+ }
108
+ }
109
+ const cyclic = tables.filter((t) => !done.has(`${t.schema}.${t.name}`));
110
+ return { ordered, cyclic };
111
+ }
112
+ export class SeedTarget {
113
+ name = "seed";
114
+ fileExtension = ".sql";
115
+ barrel = false;
116
+ capabilities = {
117
+ strictObjects: false,
118
+ brandedTypes: false,
119
+ dateInstances: false,
120
+ };
121
+ renderTable() {
122
+ throw new Error("seed is a snapshot-level target");
123
+ }
124
+ renderSnapshot(snapshot, opts) {
125
+ const rows = opts.rows ?? 3;
126
+ const lines = [
127
+ "-- Generated by supawatch. Do not edit.",
128
+ "-- Deterministic seed data; identical schema produces identical bytes.",
129
+ "begin;",
130
+ ];
131
+ const tables = snapshot.tables.filter((t) => t.kind === "table");
132
+ const byName = new Map(tables.map((t) => [`${t.schema}.${t.name}`, t]));
133
+ // The literal a table's Nth row uses for its single-column primary
134
+ // key. Children reuse this for their FK cells, so uuid and numeric
135
+ // parents both reference correctly. Deterministic by construction.
136
+ const pkLiteral = (t, i) => {
137
+ const pkName = t.primaryKey.length === 1 ? t.primaryKey[0] : null;
138
+ if (!pkName)
139
+ return null;
140
+ const col = t.columns.find((c) => c.name === pkName);
141
+ if (!col)
142
+ return null;
143
+ if (col.runtime.kind === "number")
144
+ return String(i + 1);
145
+ // bigint primary keys arrive as strings from the driver but seed
146
+ // sequentially like any serial, so sequences resync and post-seed
147
+ // inserts reference real parents.
148
+ if (col.runtime.kind === "string" && col.runtime.format === "bigint") {
149
+ return String(i + 1);
150
+ }
151
+ const rand = mulberry32(hashString(`${t.schema}.${t.name}.${pkName}.${i}`));
152
+ return literalFor(col.runtime, col, t, i, rand);
153
+ };
154
+ const { ordered, cyclic } = topoSort(tables);
155
+ for (const t of cyclic) {
156
+ lines.push(`-- skipped ${t.schema}.${t.name}: required foreign keys form a cycle`);
157
+ }
158
+ for (const table of ordered) {
159
+ const q = (s) => '"' + s.replace(/"/g, '""') + '"';
160
+ const ident = `${q(table.schema)}.${q(table.name)}`;
161
+ const pk = table.primaryKey.length === 1 ? table.primaryKey[0] : null;
162
+ const skipReasons = [];
163
+ const cols = [];
164
+ for (const col of table.columns) {
165
+ if (col.generated)
166
+ continue;
167
+ // A non-PK identity-always column cannot take a value without
168
+ // OVERRIDING applying to it too; let the database fill it.
169
+ if (col.identity === "always" && col.name !== pk)
170
+ continue;
171
+ const fk = table.foreignKeys.find((f) => f.columns.includes(col.name));
172
+ if (fk && f_multi(fk)) {
173
+ if (!col.nullable && !col.hasDefault) {
174
+ skipReasons.push(`multi-column foreign key on ${col.name}`);
175
+ }
176
+ continue;
177
+ }
178
+ if (col.name === pk) {
179
+ cols.push(col);
180
+ continue;
181
+ }
182
+ if (fk) {
183
+ cols.push(col);
184
+ continue;
185
+ }
186
+ const probe = literalFor(col.runtime, col, table, 0, mulberry32(1));
187
+ if (probe === null) {
188
+ if (!col.nullable && !col.hasDefault) {
189
+ skipReasons.push(`no honest value for ${col.name} (${col.sqlType})`);
190
+ }
191
+ continue; // defaulted or nullable: let the database fill it
192
+ }
193
+ cols.push(col);
194
+ }
195
+ if (skipReasons.length > 0) {
196
+ lines.push(`-- skipped ${table.schema}.${table.name}: ${skipReasons.join("; ")}`);
197
+ continue;
198
+ }
199
+ if (cols.length === 0)
200
+ continue;
201
+ const hasIdentityAlways = table.columns.some((c) => c.identity === "always" && c.name === pk);
202
+ const overriding = hasIdentityAlways ? " overriding system value" : "";
203
+ const colList = cols.map((c) => q(c.name)).join(", ");
204
+ for (let i = 0; i < rows; i++) {
205
+ const values = cols.map((col) => {
206
+ if (col.name === pk) {
207
+ const ref = pkLiteral(table, i);
208
+ if (ref !== null)
209
+ return ref;
210
+ }
211
+ const fk = table.foreignKeys.find((f) => f.columns.includes(col.name));
212
+ if (fk) {
213
+ if (col.nullable && i === rows - 1)
214
+ return "null";
215
+ const parent = byName.get(`${fk.referencedSchema}.${fk.referencedTable}`);
216
+ const ref = parent ? pkLiteral(parent, i % rows) : null;
217
+ return ref ?? "null";
218
+ }
219
+ if (col.nullable && i === rows - 1)
220
+ return "null";
221
+ const rand = mulberry32(hashString(`${table.schema}.${table.name}.${col.name}.${i}`));
222
+ return literalFor(col.runtime, col, table, i, rand) ?? "null";
223
+ });
224
+ lines.push(`insert into ${ident} (${colList})${overriding} values (${values.join(", ")});`);
225
+ }
226
+ if (pk) {
227
+ const pkCol = table.columns.find((c) => c.name === pk);
228
+ const pkIsSequential = pkCol &&
229
+ (pkCol.runtime.kind === "number" ||
230
+ (pkCol.runtime.kind === "string" && pkCol.runtime.format === "bigint"));
231
+ if (pkCol && (pkCol.identity || pkCol.hasDefault) && pkIsSequential) {
232
+ lines.push(`select setval(pg_get_serial_sequence('${ident.replace(/'/g, "''")}', ${sqlString(pk)}), ${rows}, true);`);
233
+ }
234
+ }
235
+ }
236
+ lines.push("commit;", "");
237
+ return [{ file: "seed.sql", content: lines.join("\n") }];
238
+ }
239
+ }
240
+ function f_multi(fk) {
241
+ return fk.columns.length > 1;
242
+ }
243
+ export default SeedTarget;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@supawatch/target-seed",
3
+ "version": "0.3.0",
4
+ "description": "Deterministic FK-aware seed.sql generated from live Postgres by supawatch: topological order, identity overriding, sequence resync.",
5
+ "keywords": [
6
+ "supabase",
7
+ "postgres",
8
+ "postgresql",
9
+ "codegen",
10
+ "schema",
11
+ "typescript",
12
+ "seed",
13
+ "fixtures",
14
+ "seed-sql",
15
+ "supabase-seed"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "Omar Dulaimi",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/omar-dulaimi/supawatch.git",
22
+ "directory": "packages/target-seed"
23
+ },
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "dependencies": {
37
+ "@supawatch/core": "0.4.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.20.1",
41
+ "typescript": "^5.9.0"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json"
45
+ }
46
+ }