@substrat-run/model-emit 0.0.1 → 0.2.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.
@@ -0,0 +1,49 @@
1
+ import { type EntityDef } from '@substrat-run/contracts';
2
+ export interface EmitSqlOptions {
3
+ /** `IF NOT EXISTS`, for an emitter run against an existing database. */
4
+ readonly ifNotExists?: boolean;
5
+ }
6
+ /**
7
+ * `CREATE TABLE` for every declared entity, in sorted order.
8
+ *
9
+ * - `id` becomes `TEXT PRIMARY KEY NOT NULL`. The `NOT NULL` is deliberate and
10
+ * stricter than most hand-written schemas: in SQLite a non-INTEGER primary key
11
+ * does NOT imply it, so `id TEXT PRIMARY KEY` accepts a NULL id. Every
12
+ * `vertical_*` table written by hand in this repo has that hole.
13
+ * - `key` becomes a `UNIQUE` constraint.
14
+ * - `parents` becomes a `REFERENCES` clause per parent, on `<parent>_id` when the
15
+ * entity declares such a column — never invented if it does not.
16
+ */
17
+ export declare function emitTables<T extends Record<string, EntityDef>>(entities: T, options?: EmitSqlOptions): string;
18
+ /** One emitted column: its name, its full definition, and whether SQLite could ADD it. */
19
+ export interface EmittedColumn {
20
+ readonly name: string;
21
+ /** `owner_id TEXT NOT NULL REFERENCES todo_owners(id)` */
22
+ readonly ddl: string;
23
+ /** NOT NULL with no default cannot be added to a table that already has rows. */
24
+ readonly requiredWithoutDefault: boolean;
25
+ }
26
+ /**
27
+ * The columns one entity emits, shared by `emitTables` and the migration
28
+ * planner.
29
+ *
30
+ * Extracted rather than duplicated: the planner has to render a column the exact
31
+ * way the table would have rendered it, or an `ALTER TABLE ADD COLUMN` produces
32
+ * a schema subtly unlike the one a fresh `CREATE TABLE` would.
33
+ */
34
+ export declare function columnsOf<T extends Record<string, EntityDef>>(name: string, entity: EntityDef, entities: T): EmittedColumn[];
35
+ /**
36
+ * The natural key, as ONE constraint over all its fields.
37
+ *
38
+ * `key: ['list_id', 'principal']` means "one share per person per list" and
39
+ * emits `UNIQUE (list_id, principal)`. It used to emit one UNIQUE per field —
40
+ * "a list may be shared once, ever" AND "a person may receive one share, ever",
41
+ * two wrong constraints silently replacing the composite. Stricter than
42
+ * intended, so it failed closed rather than open, and nothing said so.
43
+ *
44
+ * Every declaration in the fleet was single-field when this changed, so the two
45
+ * readings agreed everywhere and nothing could reveal the difference until an
46
+ * app needed a composite.
47
+ */
48
+ export declare function uniqueConstraints(name: string, entity: EntityDef): string[];
49
+ //# sourceMappingURL=emit-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-sql.d.ts","sourceRoot":"","sources":["../src/emit-sql.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,yBAAyB,CAAC;AA+HtE,MAAM,WAAW,cAAc;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAC5D,QAAQ,EAAE,CAAC,EACX,OAAO,GAAE,cAAmB,GAC3B,MAAM,CAeR;AAED,0FAA0F;AAC1F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,sBAAsB,EAAE,OAAO,CAAC;CAC1C;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAC3D,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,EACjB,QAAQ,EAAE,CAAC,GACV,aAAa,EAAE,CAuBjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,MAAM,EAAE,CAO3E"}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * DDL emitted from the entity registry — the first deterministic emitter
3
+ * (model-phase-plan §9 step 2).
4
+ *
5
+ * Today every adopter hand-writes its `CREATE TABLE` and keeps a test holding it
6
+ * to the registry. That test exists *because* the duplication does; deriving the
7
+ * DDL is what deletes both.
8
+ *
9
+ * ## Reads the TypeScript, not `model.json`
10
+ *
11
+ * It walks the live Zod objects. `z.toJSONSchema` keeps declarative constraints
12
+ * and silently drops `.refine()` and `.brand()`, so an emitter reading the JSON
13
+ * would produce a schema weaker than the model declares.
14
+ *
15
+ * ## No silent defaults
16
+ *
17
+ * A Zod type this cannot map is an ERROR, never a guess. #695's 18 broken events
18
+ * came from an emitter defaulting rather than refusing, applied uniformly and
19
+ * silently — for anything reaching a migration, absent must be loud.
20
+ */
21
+ import { z } from 'zod';
22
+ import { JSON_COLUMN } from '@substrat-run/contracts';
23
+ /** Unwrap the wrappers Zod puts around a base type, recording nullability. */
24
+ function peel(schema) {
25
+ let s = schema;
26
+ let nullable = false;
27
+ // `.optional()` and `.nullable()` both mean "may be absent" in a row.
28
+ for (;;) {
29
+ const def = s._def;
30
+ const t = def?.typeName ?? s.type;
31
+ if ((t === 'ZodOptional' || t === 'ZodNullable' || t === 'optional' || t === 'nullable') && def?.innerType) {
32
+ nullable = true;
33
+ s = def.innerType;
34
+ continue;
35
+ }
36
+ // Zod 4 keeps the wrapped schema on `.unwrap()` for these.
37
+ const unwrap = s.unwrap;
38
+ if (typeof unwrap === 'function' && (t === 'ZodOptional' || t === 'ZodNullable')) {
39
+ nullable = true;
40
+ s = unwrap.call(s);
41
+ continue;
42
+ }
43
+ return { inner: s, nullable };
44
+ }
45
+ }
46
+ function columnFor(name, schema, where) {
47
+ const { inner, nullable } = peel(schema);
48
+ const kind = inner.type ?? inner._def?.typeName;
49
+ if (kind === 'string' || kind === 'ZodString')
50
+ return { name, type: 'TEXT', nullable };
51
+ if (kind === 'boolean' || kind === 'ZodBoolean') {
52
+ // INTEGER is the right COLUMN, and `boolean` is the wrong TYPE to promise:
53
+ // SQLite hands back 0/1, so `EntityRow` would infer a boolean the database
54
+ // can never return. Refused rather than emitted, per rule 1 — a silent
55
+ // default here is a type error that typechecks.
56
+ //
57
+ // Note the asymmetry, which is why the message says it: `z.boolean()` is
58
+ // CORRECT for an operation's input, which crosses JSON. An app can take
59
+ // `done: z.boolean()` and store `done: z.number()`, and both are right.
60
+ throw new Error(`emit-sql: ${where} is z.boolean(), which stores as INTEGER — declare it z.number() so ` +
61
+ 'the row type matches what SQLite returns. (z.boolean() stays correct for an ' +
62
+ "operation's input, which crosses JSON.)");
63
+ }
64
+ if (kind === 'number' || kind === 'ZodNumber') {
65
+ // Ints and reals both land in NUMERIC affinity; INTEGER is the honest
66
+ // default for counts, and money is a string by platform rule (K-14).
67
+ return { name, type: 'INTEGER', nullable };
68
+ }
69
+ if (kind === 'enum' || kind === 'ZodEnum') {
70
+ const values = Object.values(inner.options ??
71
+ inner._def?.values ??
72
+ {});
73
+ if (!values.length)
74
+ throw new Error(`emit-sql: ${where} is an enum with no values`);
75
+ const list = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(',');
76
+ return { name, type: 'TEXT', nullable, check: `CHECK (${name} IN (${list}))` };
77
+ }
78
+ // A column declared with `jsonColumn(because)` — deliberately opaque, stored
79
+ // as TEXT because SQLite has no JSON type. A bare `z.unknown()` still falls
80
+ // through to the error below, so opacity is always something someone chose.
81
+ if (inner.description?.startsWith(JSON_COLUMN)) {
82
+ return { name, type: 'TEXT', nullable };
83
+ }
84
+ // Refuse rather than guess. A column emitted from a shape this does not
85
+ // understand is a migration nobody can reason about.
86
+ throw new Error(`emit-sql: cannot map ${where} (zod kind '${String(kind)}') to a column — ` +
87
+ `map it explicitly, or model the field as one this understands`);
88
+ }
89
+ /**
90
+ * Parents before children, alphabetical within a tier.
91
+ *
92
+ * Sorting by name alone emitted `todo_items` — which REFERENCES `todo_lists` —
93
+ * first. SQLite tolerates a forward reference; a stricter engine does not, and
94
+ * "it happened to work" is not a property to ship. Deterministic either way,
95
+ * which is what lets the output be diffed.
96
+ *
97
+ * A cycle cannot be ordered, so it is reported rather than silently truncated:
98
+ * remaining entities are appended in name order after the error names them.
99
+ */
100
+ function tableOrder(entities) {
101
+ const names = Object.keys(entities).sort();
102
+ const placed = new Set();
103
+ const out = [];
104
+ let progress = true;
105
+ while (progress && out.length < names.length) {
106
+ progress = false;
107
+ for (const name of names) {
108
+ if (placed.has(name))
109
+ continue;
110
+ const parents = entities[name]?.parents ?? [];
111
+ // A parent outside this registry (a composed engine's entity) cannot be
112
+ // emitted here and therefore cannot be waited for.
113
+ const blocked = parents.some((p) => p in entities && !placed.has(String(p)));
114
+ if (blocked)
115
+ continue;
116
+ placed.add(name);
117
+ out.push(name);
118
+ progress = true;
119
+ }
120
+ }
121
+ if (out.length < names.length) {
122
+ const cyclic = names.filter((n) => !placed.has(n));
123
+ throw new Error(`emit-sql: parent cycle among ${cyclic.join(', ')} — a table cannot be created before itself`);
124
+ }
125
+ return out;
126
+ }
127
+ /**
128
+ * `CREATE TABLE` for every declared entity, in sorted order.
129
+ *
130
+ * - `id` becomes `TEXT PRIMARY KEY NOT NULL`. The `NOT NULL` is deliberate and
131
+ * stricter than most hand-written schemas: in SQLite a non-INTEGER primary key
132
+ * does NOT imply it, so `id TEXT PRIMARY KEY` accepts a NULL id. Every
133
+ * `vertical_*` table written by hand in this repo has that hole.
134
+ * - `key` becomes a `UNIQUE` constraint.
135
+ * - `parents` becomes a `REFERENCES` clause per parent, on `<parent>_id` when the
136
+ * entity declares such a column — never invented if it does not.
137
+ */
138
+ export function emitTables(entities, options = {}) {
139
+ const exists = options.ifNotExists ? 'IF NOT EXISTS ' : '';
140
+ const out = [];
141
+ for (const name of tableOrder(entities)) {
142
+ const entity = entities[name];
143
+ if (!entity)
144
+ continue;
145
+ const cols = [
146
+ ...columnsOf(name, entity, entities).map((c) => ` ${c.ddl}`),
147
+ ...uniqueConstraints(name, entity).map((u) => ` ${u}`),
148
+ ];
149
+ out.push(`CREATE TABLE ${exists}${entity.table} (\n${cols.join(',\n')}\n);`);
150
+ }
151
+ return out.join('\n\n');
152
+ }
153
+ /**
154
+ * The columns one entity emits, shared by `emitTables` and the migration
155
+ * planner.
156
+ *
157
+ * Extracted rather than duplicated: the planner has to render a column the exact
158
+ * way the table would have rendered it, or an `ALTER TABLE ADD COLUMN` produces
159
+ * a schema subtly unlike the one a fresh `CREATE TABLE` would.
160
+ */
161
+ export function columnsOf(name, entity, entities) {
162
+ const shape = entity.fields.shape;
163
+ const out = [];
164
+ for (const [field, schema] of Object.entries(shape)) {
165
+ const c = columnFor(field, schema, `${name}.${field}`);
166
+ if (field === 'id') {
167
+ out.push({ name: 'id', ddl: `id ${c.type} PRIMARY KEY NOT NULL`, requiredWithoutDefault: true });
168
+ continue;
169
+ }
170
+ let ddl = `${c.name} ${c.type}`;
171
+ if (!c.nullable)
172
+ ddl += ' NOT NULL';
173
+ if (c.check)
174
+ ddl += ` ${c.check}`;
175
+ // A parent edge whose id column is present becomes a real foreign key.
176
+ for (const parent of entity.parents ?? []) {
177
+ const parentTable = entities[parent]?.table;
178
+ if (parentTable && c.name === `${String(parent).replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()}_id`) {
179
+ ddl += ` REFERENCES ${parentTable}(id)`;
180
+ }
181
+ }
182
+ out.push({ name: c.name, ddl, requiredWithoutDefault: !c.nullable });
183
+ }
184
+ return out;
185
+ }
186
+ /**
187
+ * The natural key, as ONE constraint over all its fields.
188
+ *
189
+ * `key: ['list_id', 'principal']` means "one share per person per list" and
190
+ * emits `UNIQUE (list_id, principal)`. It used to emit one UNIQUE per field —
191
+ * "a list may be shared once, ever" AND "a person may receive one share, ever",
192
+ * two wrong constraints silently replacing the composite. Stricter than
193
+ * intended, so it failed closed rather than open, and nothing said so.
194
+ *
195
+ * Every declaration in the fleet was single-field when this changed, so the two
196
+ * readings agreed everywhere and nothing could reveal the difference until an
197
+ * app needed a composite.
198
+ */
199
+ export function uniqueConstraints(name, entity) {
200
+ const shape = entity.fields.shape;
201
+ const key = entity.key ?? [];
202
+ for (const k of key) {
203
+ if (!(k in shape))
204
+ throw new Error(`emit-sql: ${name}.key names '${k}', which is not a field`);
205
+ }
206
+ return key.length > 0 ? [`UNIQUE (${key.join(', ')})`] : [];
207
+ }
208
+ //# sourceMappingURL=emit-sql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-sql.js","sourceRoot":"","sources":["../src/emit-sql.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { emitTables, columnsOf, uniqueConstraints, type EmitSqlOptions, type EmittedColumn } from './emit-sql.js';
2
+ export { journalColumns, journalUniques } from './journal.js';
3
+ export { planMigration, parseJournal, type Journal, type JournalEntry, type MigrationPlan, } from './plan.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAClH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { emitTables, columnsOf, uniqueConstraints } from './emit-sql.js';
2
+ export { journalColumns, journalUniques } from './journal.js';
3
+ export { planMigration, parseJournal, } from './plan.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,iBAAiB,EAA2C,MAAM,eAAe,CAAC;AAClH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EACL,aAAa,EACb,YAAY,GAIb,MAAM,WAAW,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Reading a migration journal — the verification half of `emitTables`.
3
+ *
4
+ * The emitter's claim is "what this emits is what the database ends up with";
5
+ * this is how that gets checked. They live together because they are two halves
6
+ * of one statement, and a reader that drifts from its emitter checks nothing.
7
+ */
8
+ /**
9
+ * Column names per table, read out of a migration journal's SQL.
10
+ *
11
+ * Three engines had hand-rolled a copy of this and the copies had already drifted — none followed `RENAME TO`, so a journal that
12
+ * rebuilds a table under a temporary name would report the pre-rebuild columns
13
+ * forever.
14
+ *
15
+ * It exists because a registry and a journal are two descriptions of one schema
16
+ * until migrations are derived from the registry. Holding them to each other is
17
+ * what keeps that duplication safe in the meantime.
18
+ *
19
+ * Handles what real journals do: multi-line `CHECK (...)` constraints (tracked
20
+ * by paren depth, so a continuation line is not read as a column), `ADD COLUMN`,
21
+ * `DROP TABLE`, `RENAME COLUMN` and `RENAME TO` — append-only journals rebuild a table by
22
+ * creating a `_new`, copying, dropping the original and renaming onto its name.
23
+ */
24
+ export declare function journalColumns(sql: string): Map<string, Set<string>>;
25
+ /**
26
+ * UNIQUE constraints per table, read out of a journal.
27
+ *
28
+ * `journalColumns` deliberately skips constraint lines — it answers "which
29
+ * columns exist". But a declared `key` is a schema fact too, and adding one to
30
+ * an entity that already has a table is a change SQLite cannot make in place.
31
+ * Without this the planner reported "up to date" over a missing constraint.
32
+ *
33
+ * Normalised to `a, b` (single space, declaration order preserved) so the same
34
+ * constraint written two ways compares equal.
35
+ */
36
+ export declare function journalUniques(sql: string): Map<string, Set<string>>;
37
+ //# sourceMappingURL=journal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.d.ts","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CA4CpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAwDpE"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Reading a migration journal — the verification half of `emitTables`.
3
+ *
4
+ * The emitter's claim is "what this emits is what the database ends up with";
5
+ * this is how that gets checked. They live together because they are two halves
6
+ * of one statement, and a reader that drifts from its emitter checks nothing.
7
+ */
8
+ /**
9
+ * Column names per table, read out of a migration journal's SQL.
10
+ *
11
+ * Three engines had hand-rolled a copy of this and the copies had already drifted — none followed `RENAME TO`, so a journal that
12
+ * rebuilds a table under a temporary name would report the pre-rebuild columns
13
+ * forever.
14
+ *
15
+ * It exists because a registry and a journal are two descriptions of one schema
16
+ * until migrations are derived from the registry. Holding them to each other is
17
+ * what keeps that duplication safe in the meantime.
18
+ *
19
+ * Handles what real journals do: multi-line `CHECK (...)` constraints (tracked
20
+ * by paren depth, so a continuation line is not read as a column), `ADD COLUMN`,
21
+ * `DROP TABLE`, `RENAME COLUMN` and `RENAME TO` — append-only journals rebuild a table by
22
+ * creating a `_new`, copying, dropping the original and renaming onto its name.
23
+ */
24
+ export function journalColumns(sql) {
25
+ const tables = new Map();
26
+ for (const [, table, body] of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?([a-z_][a-z0-9_]*)\s*\(([\s\S]*?)\n\s*\);/gi)) {
27
+ if (!table || !body)
28
+ continue;
29
+ const cols = new Set();
30
+ let depth = 0;
31
+ for (const raw of body.split('\n')) {
32
+ const line = raw.trim();
33
+ const atTop = depth === 0;
34
+ depth += (line.match(/\(/g) ?? []).length - (line.match(/\)/g) ?? []).length;
35
+ if (!atTop)
36
+ continue;
37
+ if (!line || line.startsWith('--') || /^(PRIMARY|FOREIGN|UNIQUE|CHECK|CONSTRAINT)\b/i.test(line))
38
+ continue;
39
+ const name = /^([a-z_][a-z0-9_]*)\b/i.exec(line)?.[1];
40
+ if (name)
41
+ cols.add(name);
42
+ }
43
+ tables.set(table, cols);
44
+ }
45
+ // Replayed in statement order: a journal may add a column and later rename the
46
+ // table, or rename onto a name it has just dropped.
47
+ for (const m of sql.matchAll(
48
+ // `RENAME COLUMN` comes first: `RENAME TO` must not swallow it. Without this
49
+ // branch a renamed column reads as its old name forever, and a planner that
50
+ // derives migrations would emit the same rename on every run.
51
+ /(?:ALTER TABLE ([a-z_][a-z0-9_]*)\s+ADD COLUMN\s+([a-z_][a-z0-9_]*))|(?:ALTER TABLE ([a-z_][a-z0-9_]*)\s+RENAME COLUMN\s+([a-z_][a-z0-9_]*)\s+TO\s+([a-z_][a-z0-9_]*))|(?:ALTER TABLE ([a-z_][a-z0-9_]*)\s+RENAME TO\s+([a-z_][a-z0-9_]*))|(?:DROP TABLE (?:IF EXISTS )?([a-z_][a-z0-9_]*))/gi)) {
52
+ const [, addTable, addCol, renTable, renFrom, renTo, fromTable, toTable, dropped] = m;
53
+ if (addTable && addCol)
54
+ tables.get(addTable)?.add(addCol);
55
+ else if (dropped)
56
+ tables.delete(dropped);
57
+ else if (renTable && renFrom && renTo) {
58
+ const cols = tables.get(renTable);
59
+ if (cols?.delete(renFrom))
60
+ cols.add(renTo);
61
+ }
62
+ else if (fromTable && toTable) {
63
+ const cols = tables.get(fromTable);
64
+ if (cols) {
65
+ tables.delete(fromTable);
66
+ tables.set(toTable, cols);
67
+ }
68
+ }
69
+ }
70
+ return tables;
71
+ }
72
+ /**
73
+ * UNIQUE constraints per table, read out of a journal.
74
+ *
75
+ * `journalColumns` deliberately skips constraint lines — it answers "which
76
+ * columns exist". But a declared `key` is a schema fact too, and adding one to
77
+ * an entity that already has a table is a change SQLite cannot make in place.
78
+ * Without this the planner reported "up to date" over a missing constraint.
79
+ *
80
+ * Normalised to `a, b` (single space, declaration order preserved) so the same
81
+ * constraint written two ways compares equal.
82
+ */
83
+ export function journalUniques(sql) {
84
+ const tables = new Map();
85
+ for (const [, table, body] of sql.matchAll(/CREATE TABLE (?:IF NOT EXISTS )?([a-z_][a-z0-9_]*)\s*\(([\s\S]*?)\n\s*\);/gi)) {
86
+ if (!table || !body)
87
+ continue;
88
+ const found = new Set();
89
+ for (const [, cols] of body.matchAll(/\bUNIQUE\s*\(([^)]*)\)/gi)) {
90
+ if (!cols)
91
+ continue;
92
+ found.add(cols
93
+ .split(',')
94
+ .map((c) => c.trim())
95
+ .filter(Boolean)
96
+ .join(', '));
97
+ }
98
+ tables.set(table, found);
99
+ }
100
+ // Replayed in statement order, like `journalColumns`.
101
+ for (const m of sql.matchAll(/(?:ALTER TABLE ([a-z_][a-z0-9_]*)\s+RENAME COLUMN\s+([a-z_][a-z0-9_]*)\s+TO\s+([a-z_][a-z0-9_]*))|(?:ALTER TABLE ([a-z_][a-z0-9_]*)\s+RENAME TO\s+([a-z_][a-z0-9_]*))|(?:DROP TABLE (?:IF EXISTS )?([a-z_][a-z0-9_]*))/gi)) {
102
+ const [, renTable, renFrom, renTo, fromTable, toTable, dropped] = m;
103
+ if (dropped) {
104
+ tables.delete(dropped);
105
+ }
106
+ else if (renTable && renFrom && renTo) {
107
+ // SQLite rewrites the constraint when a column is renamed — verified
108
+ // against a real database — so the reader has to as well. Missing this
109
+ // makes a renamed key look like a key the journal never had.
110
+ const cs = tables.get(renTable);
111
+ if (cs) {
112
+ tables.set(renTable, new Set([...cs].map((c) => c
113
+ .split(', ')
114
+ .map((col) => (col === renFrom ? renTo : col))
115
+ .join(', '))));
116
+ }
117
+ }
118
+ else if (fromTable && toTable) {
119
+ // A rebuild renames a table onto another's name; constraints travel with it.
120
+ const c = tables.get(fromTable);
121
+ if (c) {
122
+ tables.delete(fromTable);
123
+ tables.set(toTable, c);
124
+ }
125
+ }
126
+ }
127
+ return tables;
128
+ }
129
+ //# sourceMappingURL=journal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journal.js","sourceRoot":"","sources":["../src/journal.ts"],"names":[],"mappings":"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"}
package/dist/plan.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { EntityDef } from '@substrat-run/contracts';
2
+ export interface JournalEntry {
3
+ /** Derived, monotonic, zero-padded: `0001`. Never authored. */
4
+ readonly version: string;
5
+ /** Human label for the diff, derived from what changed. */
6
+ readonly slug: string;
7
+ readonly sql: string;
8
+ /**
9
+ * Shipped. A released entry is frozen — the planner appends after it and never
10
+ * touches it. Set by whatever ships the package, not by the generator.
11
+ */
12
+ readonly released?: boolean;
13
+ }
14
+ export interface Journal {
15
+ readonly entries: readonly JournalEntry[];
16
+ }
17
+ export type MigrationPlan = {
18
+ readonly kind: 'up-to-date';
19
+ } | {
20
+ readonly kind: 'append';
21
+ readonly entry: JournalEntry;
22
+ } | {
23
+ readonly kind: 'refused';
24
+ readonly reasons: readonly string[];
25
+ };
26
+ /**
27
+ * What one entry would have to say to bring the journal up to the model.
28
+ *
29
+ * Pure: same model + same journal → same plan, every time. It reads no clock and
30
+ * mints no id, which is what lets the result be committed and diffed.
31
+ */
32
+ export declare function planMigration<T extends Record<string, EntityDef>>(entities: T, journal: Journal): MigrationPlan;
33
+ /** Parsed hostilely: it is our file, and it is also somebody's merge resolution. */
34
+ export declare function parseJournal(raw: unknown): Journal;
35
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGzD,MAAM,WAAW,YAAY;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;CAC3C;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAA;CAAE,GACzD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAItE;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAC/D,QAAQ,EAAE,CAAC,EACX,OAAO,EAAE,OAAO,GACf,aAAa,CA6If;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAkBlD"}