@metaobjectsdev/migrate-ts 0.8.1-rc.1 → 0.9.0-rc.1
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/README.md +1 -3
- package/dist/apply/apply.d.ts +61 -0
- package/dist/apply/apply.d.ts.map +1 -0
- package/dist/apply/apply.js +241 -0
- package/dist/apply/apply.js.map +1 -0
- package/dist/apply/ledger.d.ts +78 -0
- package/dist/apply/ledger.d.ts.map +1 -0
- package/dist/apply/ledger.js +146 -0
- package/dist/apply/ledger.js.map +1 -0
- package/dist/check-expr-compare.d.ts +13 -0
- package/dist/check-expr-compare.d.ts.map +1 -0
- package/dist/check-expr-compare.js +48 -0
- package/dist/check-expr-compare.js.map +1 -0
- package/dist/diff/index.d.ts +3 -1
- package/dist/diff/index.d.ts.map +1 -1
- package/dist/diff/index.js +57 -14
- package/dist/diff/index.js.map +1 -1
- package/dist/diff/status.js +3 -0
- package/dist/diff/status.js.map +1 -1
- package/dist/drift/classify.d.ts +16 -0
- package/dist/drift/classify.d.ts.map +1 -0
- package/dist/drift/classify.js +44 -0
- package/dist/drift/classify.js.map +1 -0
- package/dist/drift/drift.d.ts +32 -0
- package/dist/drift/drift.d.ts.map +1 -0
- package/dist/drift/drift.js +36 -0
- package/dist/drift/drift.js.map +1 -0
- package/dist/emit/d1-safety-pass.d.ts.map +1 -1
- package/dist/emit/d1-safety-pass.js +15 -45
- package/dist/emit/d1-safety-pass.js.map +1 -1
- package/dist/emit/postgres.d.ts.map +1 -1
- package/dist/emit/postgres.js +47 -4
- package/dist/emit/postgres.js.map +1 -1
- package/dist/emit/sqlite.d.ts.map +1 -1
- package/dist/emit/sqlite.js +22 -0
- package/dist/emit/sqlite.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/expected-schema.d.ts.map +1 -1
- package/dist/expected-schema.js +114 -5
- package/dist/expected-schema.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/introspect/d1.d.ts.map +1 -1
- package/dist/introspect/d1.js +1 -0
- package/dist/introspect/d1.js.map +1 -1
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/postgres.js +38 -2
- package/dist/introspect/postgres.js.map +1 -1
- package/dist/introspect/sqlite.d.ts.map +1 -1
- package/dist/introspect/sqlite.js +13 -2
- package/dist/introspect/sqlite.js.map +1 -1
- package/dist/snapshot/checksum.d.ts +10 -0
- package/dist/snapshot/checksum.d.ts.map +1 -0
- package/dist/snapshot/checksum.js +14 -0
- package/dist/snapshot/checksum.js.map +1 -0
- package/dist/snapshot/plan.d.ts +25 -0
- package/dist/snapshot/plan.d.ts.map +1 -0
- package/dist/snapshot/plan.js +30 -0
- package/dist/snapshot/plan.js.map +1 -0
- package/dist/snapshot/serialize.d.ts +10 -0
- package/dist/snapshot/serialize.d.ts.map +1 -0
- package/dist/snapshot/serialize.js +63 -0
- package/dist/snapshot/serialize.js.map +1 -0
- package/dist/snapshot/store.d.ts +12 -0
- package/dist/snapshot/store.d.ts.map +1 -0
- package/dist/snapshot/store.js +32 -0
- package/dist/snapshot/store.js.map +1 -0
- package/dist/sql/split-statements.d.ts +12 -0
- package/dist/sql/split-statements.d.ts.map +1 -0
- package/dist/sql/split-statements.js +112 -0
- package/dist/sql/split-statements.js.map +1 -0
- package/dist/sql-type.d.ts +2 -0
- package/dist/sql-type.d.ts.map +1 -1
- package/dist/sql-type.js +2 -0
- package/dist/sql-type.js.map +1 -1
- package/dist/types.d.ts +36 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/verify/replay.d.ts +25 -0
- package/dist/verify/replay.d.ts.map +1 -0
- package/dist/verify/replay.js +25 -0
- package/dist/verify/replay.js.map +1 -0
- package/dist/view-sql-compare.d.ts +8 -0
- package/dist/view-sql-compare.d.ts.map +1 -0
- package/dist/view-sql-compare.js +44 -0
- package/dist/view-sql-compare.js.map +1 -0
- package/package.json +2 -2
- package/src/apply/apply.ts +340 -0
- package/src/apply/ledger.ts +241 -0
- package/src/check-expr-compare.ts +49 -0
- package/src/diff/index.ts +59 -15
- package/src/diff/status.ts +3 -0
- package/src/drift/classify.ts +56 -0
- package/src/drift/drift.ts +66 -0
- package/src/emit/d1-safety-pass.ts +16 -45
- package/src/emit/postgres.ts +47 -4
- package/src/emit/sqlite.ts +22 -0
- package/src/errors.ts +4 -0
- package/src/expected-schema.ts +124 -4
- package/src/index.ts +44 -0
- package/src/introspect/d1.ts +1 -0
- package/src/introspect/postgres.ts +38 -4
- package/src/introspect/sqlite.ts +13 -3
- package/src/snapshot/checksum.ts +15 -0
- package/src/snapshot/plan.ts +53 -0
- package/src/snapshot/serialize.ts +81 -0
- package/src/snapshot/store.ts +33 -0
- package/src/sql/split-statements.ts +115 -0
- package/src/sql-type.ts +3 -0
- package/src/types.ts +26 -9
- package/src/verify/replay.ts +43 -0
- package/src/view-sql-compare.ts +46 -0
package/src/emit/postgres.ts
CHANGED
|
@@ -16,6 +16,7 @@ const STAGE_ORDER: Record<Change["kind"], number> = {
|
|
|
16
16
|
"rename-column": 3, "rename-table": 3,
|
|
17
17
|
"add-index": 4, "drop-index": 4,
|
|
18
18
|
"add-fk": 5, "drop-fk": 5,
|
|
19
|
+
"add-check": 5, "drop-check": 5,
|
|
19
20
|
"drop-table": 6,
|
|
20
21
|
"create-view": 7, "replace-view": 7,
|
|
21
22
|
};
|
|
@@ -61,6 +62,12 @@ function renderUp(c: Change): string {
|
|
|
61
62
|
case "drop-index": return `DROP INDEX ${quoteIndexQualified(c.index, c.schema)};`;
|
|
62
63
|
case "add-fk": return renderAddFk(c.table, c.schema, c.fk);
|
|
63
64
|
case "drop-fk": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.fk)};`;
|
|
65
|
+
// add-check / drop-check are declared but NOT yet produced by the diff —
|
|
66
|
+
// checks are create-time-only (inlined in CREATE TABLE via renderCreateTable).
|
|
67
|
+
// These arms exist for future existing-table CHECK evolution support, mirroring
|
|
68
|
+
// the create-view/drop-view "declared, not yet produced" pattern.
|
|
69
|
+
case "add-check": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} ADD CONSTRAINT ${quote(c.check.name)} CHECK (${c.check.expression});`;
|
|
70
|
+
case "drop-check": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.check)};`;
|
|
64
71
|
case "create-view": return renderCreateView(c.view, c.schema, /* orReplace */ false);
|
|
65
72
|
case "drop-view": return `DROP VIEW ${quoteQualifiedView(c.view, c.schema)};`;
|
|
66
73
|
case "replace-view": return renderCreateView(c.view, c.schema, /* orReplace */ true);
|
|
@@ -70,10 +77,29 @@ function renderUp(c: Change): string {
|
|
|
70
77
|
function renderDown(c: Change): string {
|
|
71
78
|
switch (c.kind) {
|
|
72
79
|
case "create-table": return `DROP TABLE ${quoteQualified(c.table.name, c.table.schema)};`;
|
|
73
|
-
case "drop-table":
|
|
80
|
+
case "drop-table": {
|
|
81
|
+
if (!c.restore) {
|
|
82
|
+
return `-- WARNING: down migration cannot restore data\n-- TODO: restore table "${c.table}" structure manually`;
|
|
83
|
+
}
|
|
84
|
+
// renderCreateTable emits only columns + PK + checks. Indexes and FKs ride
|
|
85
|
+
// as separate add-index/add-fk changes on the up side, so re-create them
|
|
86
|
+
// explicitly here or the down silently loses them.
|
|
87
|
+
const parts = [renderCreateTable(c.restore)];
|
|
88
|
+
for (const index of c.restore.indexes) {
|
|
89
|
+
parts.push(renderCreateIndex(c.restore.name, c.restore.schema, index));
|
|
90
|
+
}
|
|
91
|
+
for (const fk of c.restore.foreignKeys) {
|
|
92
|
+
parts.push(renderAddFk(c.restore.name, c.restore.schema, fk));
|
|
93
|
+
}
|
|
94
|
+
parts.push("-- NOTE: table data is not restored by this down migration.");
|
|
95
|
+
return parts.join("\n");
|
|
96
|
+
}
|
|
74
97
|
case "rename-table": return `ALTER TABLE ${quoteQualified(c.to, c.schema)} RENAME TO ${quote(c.from)};`;
|
|
75
98
|
case "add-column": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP COLUMN ${quote(c.column.name)};`;
|
|
76
|
-
case "drop-column":
|
|
99
|
+
case "drop-column":
|
|
100
|
+
return c.restore
|
|
101
|
+
? `ALTER TABLE ${quoteQualified(c.table, c.schema)} ADD COLUMN ${renderColumn(c.restore)};\n-- NOTE: column data is not restored by this down migration.`
|
|
102
|
+
: `-- WARNING: down migration cannot restore data\n-- TODO: re-add dropped column "${c.column}" manually with original type/nullable/default`;
|
|
77
103
|
case "rename-column": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} RENAME COLUMN ${quote(c.to)} TO ${quote(c.from)};`;
|
|
78
104
|
case "change-column-type": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} ALTER COLUMN ${quote(c.column)} TYPE ${pgType(c.from)};`;
|
|
79
105
|
case "change-column-nullable":
|
|
@@ -85,9 +111,22 @@ function renderDown(c: Change): string {
|
|
|
85
111
|
? `ALTER TABLE ${quoteQualified(c.table, c.schema)} ALTER COLUMN ${quote(c.column)} SET DEFAULT ${renderDefault(c.from)};`
|
|
86
112
|
: `ALTER TABLE ${quoteQualified(c.table, c.schema)} ALTER COLUMN ${quote(c.column)} DROP DEFAULT;`;
|
|
87
113
|
case "add-index": return `DROP INDEX ${quoteIndexQualified(c.index.name, c.schema)};`;
|
|
88
|
-
case "drop-index":
|
|
114
|
+
case "drop-index":
|
|
115
|
+
return c.restore
|
|
116
|
+
? renderCreateIndex(c.table, c.schema, c.restore)
|
|
117
|
+
: `-- WARNING: down migration cannot restore the original index definition`;
|
|
89
118
|
case "add-fk": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.fk.name)};`;
|
|
90
|
-
case "drop-fk":
|
|
119
|
+
case "drop-fk":
|
|
120
|
+
return c.restore
|
|
121
|
+
? renderAddFk(c.table, c.schema, c.restore)
|
|
122
|
+
: `-- WARNING: down migration cannot restore the original FK definition`;
|
|
123
|
+
// add-check / drop-check down arms: declared but not yet produced by the diff
|
|
124
|
+
// (checks are create-time-only; see renderUp note).
|
|
125
|
+
case "add-check": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.check.name)};`;
|
|
126
|
+
case "drop-check":
|
|
127
|
+
return c.restore
|
|
128
|
+
? `ALTER TABLE ${quoteQualified(c.table, c.schema)} ADD CONSTRAINT ${quote(c.restore.name)} CHECK (${c.restore.expression});`
|
|
129
|
+
: `-- WARNING: down migration cannot restore the original CHECK definition`;
|
|
91
130
|
case "create-view": return `DROP VIEW ${quoteQualifiedView(c.view.name, c.schema)};`;
|
|
92
131
|
case "drop-view": return `-- WARNING: down migration cannot restore the original view definition`;
|
|
93
132
|
case "replace-view": return `-- WARNING: down migration cannot restore the original view definition`;
|
|
@@ -99,6 +138,9 @@ function renderCreateTable(t: TableDescriptor): string {
|
|
|
99
138
|
if (t.primaryKey.length > 0) {
|
|
100
139
|
colDefs.push(` CONSTRAINT ${quote(t.name + "_pkey")} PRIMARY KEY (${t.primaryKey.map(quote).join(", ")})`);
|
|
101
140
|
}
|
|
141
|
+
for (const chk of t.checks ?? []) {
|
|
142
|
+
colDefs.push(` CONSTRAINT ${quote(chk.name)} CHECK (${chk.expression})`);
|
|
143
|
+
}
|
|
102
144
|
const create = `CREATE TABLE ${quoteQualified(t.name, t.schema)} (\n${colDefs.join(",\n")}\n);`;
|
|
103
145
|
const comments = renderTableComments(t);
|
|
104
146
|
return comments.length === 0 ? create : `${create}\n${comments.join("\n")}`;
|
|
@@ -156,6 +198,7 @@ function pgType(t: SqlType): string {
|
|
|
156
198
|
case "boolean": return "BOOLEAN";
|
|
157
199
|
case "timestamp": return t.withTimezone ? "TIMESTAMPTZ" : "TIMESTAMP";
|
|
158
200
|
case "date": return "DATE";
|
|
201
|
+
case "time": return "TIME";
|
|
159
202
|
case "json": return "JSONB";
|
|
160
203
|
case "blob": return "BYTEA";
|
|
161
204
|
case "uuid": return "UUID";
|
package/src/emit/sqlite.ts
CHANGED
|
@@ -13,6 +13,7 @@ const STAGE_ORDER: Record<Change["kind"], number> = {
|
|
|
13
13
|
"rename-column": 3, "rename-table": 3,
|
|
14
14
|
"add-index": 4, "drop-index": 4,
|
|
15
15
|
"add-fk": 5, "drop-fk": 5,
|
|
16
|
+
"add-check": 5, "drop-check": 5,
|
|
16
17
|
"drop-table": 6,
|
|
17
18
|
"create-view": 99, "drop-view": 99, "replace-view": 99,
|
|
18
19
|
};
|
|
@@ -110,6 +111,8 @@ function changeTable(c: Change): string | undefined {
|
|
|
110
111
|
case "drop-index":
|
|
111
112
|
case "add-fk":
|
|
112
113
|
case "drop-fk":
|
|
114
|
+
case "add-check":
|
|
115
|
+
case "drop-check":
|
|
113
116
|
return c.table;
|
|
114
117
|
default:
|
|
115
118
|
return undefined;
|
|
@@ -188,6 +191,12 @@ function renderUpNative(c: Change): string {
|
|
|
188
191
|
case "rename-column": return `ALTER TABLE ${quote(c.table)} RENAME COLUMN ${quote(c.from)} TO ${quote(c.to)};`;
|
|
189
192
|
case "add-index": return renderCreateIndex(c.table, c.index);
|
|
190
193
|
case "drop-index": return `DROP INDEX ${quote(c.index)};`;
|
|
194
|
+
case "add-check":
|
|
195
|
+
case "drop-check":
|
|
196
|
+
// Declared for future existing-table support; the diff does not yet produce
|
|
197
|
+
// these (checks are create-time-only, inlined in CREATE TABLE). Unreachable
|
|
198
|
+
// today — throw rather than silently mis-emit if one ever arrives here.
|
|
199
|
+
throw new Error("CHECK migration not implemented for sqlite (recreate path pending)");
|
|
191
200
|
case "change-column-type":
|
|
192
201
|
case "change-column-nullable":
|
|
193
202
|
case "change-column-default":
|
|
@@ -212,6 +221,12 @@ function renderDownNative(c: Change): string {
|
|
|
212
221
|
case "rename-column": return `ALTER TABLE ${quote(c.table)} RENAME COLUMN ${quote(c.to)} TO ${quote(c.from)};`;
|
|
213
222
|
case "add-index": return `DROP INDEX ${quote(c.index.name)};`;
|
|
214
223
|
case "drop-index": return `-- WARNING: down migration cannot restore the original index definition`;
|
|
224
|
+
case "add-check":
|
|
225
|
+
case "drop-check":
|
|
226
|
+
// Declared for future existing-table support; the diff does not yet produce
|
|
227
|
+
// these (checks are create-time-only, inlined in CREATE TABLE). Unreachable
|
|
228
|
+
// today — throw rather than silently mis-emit if one ever arrives here.
|
|
229
|
+
throw new Error("CHECK migration not implemented for sqlite (recreate path pending)");
|
|
215
230
|
case "change-column-type":
|
|
216
231
|
case "change-column-nullable":
|
|
217
232
|
case "change-column-default":
|
|
@@ -243,6 +258,12 @@ function renderCreateTable(t: TableDescriptor): string {
|
|
|
243
258
|
if (fk.onUpdate) clause += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
|
|
244
259
|
colDefs.push(clause);
|
|
245
260
|
}
|
|
261
|
+
// CHECK constraints are inlined into the CREATE TABLE DDL (SQLite supports
|
|
262
|
+
// inline named CHECK). Checks are create-time-only; the diff never produces
|
|
263
|
+
// add-check / drop-check, so this is the sole place SQLite emits a CHECK.
|
|
264
|
+
for (const chk of t.checks ?? []) {
|
|
265
|
+
colDefs.push(` CONSTRAINT ${quote(chk.name)} CHECK (${chk.expression})`);
|
|
266
|
+
}
|
|
246
267
|
return `CREATE TABLE ${quote(t.name)} (\n${colDefs.join(",\n")}\n);`;
|
|
247
268
|
}
|
|
248
269
|
|
|
@@ -286,6 +307,7 @@ function sqliteType(t: SqlType, identity: ColumnDescriptor["identity"]): string
|
|
|
286
307
|
case "boolean": return "BOOLEAN"; // SQLite stores as 0/1 but preserves declared type for round-trip
|
|
287
308
|
case "timestamp": return "TIMESTAMP";
|
|
288
309
|
case "date": return "DATE";
|
|
310
|
+
case "time": return "TIME";
|
|
289
311
|
case "json": return "TEXT"; // SQLite has JSON1 but stores as text
|
|
290
312
|
case "blob": return "BLOB";
|
|
291
313
|
case "uuid": return "TEXT";
|
package/src/errors.ts
CHANGED
|
@@ -65,6 +65,10 @@ function changeLocator(c: Change): string {
|
|
|
65
65
|
return `${c.table}.${c.fk.name}`;
|
|
66
66
|
case "drop-fk":
|
|
67
67
|
return `${c.table}.${c.fk}`;
|
|
68
|
+
case "add-check":
|
|
69
|
+
return `${c.table}.${c.check.name}`;
|
|
70
|
+
case "drop-check":
|
|
71
|
+
return `${c.table}.${c.check}`;
|
|
68
72
|
case "create-view":
|
|
69
73
|
case "replace-view":
|
|
70
74
|
return c.view.name;
|
package/src/expected-schema.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { ColumnNamingStrategy, MetaData, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
1
|
+
import type { ColumnNamingStrategy, MetaData, MetaObject, MetaRoot, MetaValidator } from "@metaobjectsdev/metadata";
|
|
2
2
|
import {
|
|
3
|
+
VALIDATOR_SUBTYPE_NUMERIC, VALIDATOR_SUBTYPE_LENGTH, VALIDATOR_SUBTYPE_REGEX,
|
|
4
|
+
VALIDATOR_ATTR_PATTERN,
|
|
3
5
|
TYPE_OBJECT,
|
|
4
6
|
MetaSource,
|
|
5
7
|
IDENTITY_ATTR_GENERATION,
|
|
6
8
|
IDENTITY_ATTR_UNIQUE,
|
|
7
9
|
FIELD_ATTR_DEFAULT,
|
|
8
10
|
FIELD_ATTR_MAX_LENGTH,
|
|
11
|
+
FIELD_ATTR_PRECISION,
|
|
12
|
+
FIELD_ATTR_SCALE,
|
|
9
13
|
FIELD_ATTR_UNIQUE,
|
|
10
14
|
FIELD_SUBTYPE_STRING,
|
|
11
15
|
FIELD_SUBTYPE_INT,
|
|
@@ -22,8 +26,15 @@ import {
|
|
|
22
26
|
FIELD_SUBTYPE_TIMESTAMP,
|
|
23
27
|
FIELD_SUBTYPE_OBJECT,
|
|
24
28
|
FIELD_SUBTYPE_CLASS,
|
|
29
|
+
FIELD_SUBTYPE_UUID,
|
|
30
|
+
FIELD_SUBTYPE_ENUM,
|
|
31
|
+
FIELD_ATTR_VALUES,
|
|
25
32
|
FIELD_ATTR_OBJECT_REF,
|
|
26
33
|
FIELD_ATTR_STORAGE,
|
|
34
|
+
FIELD_ATTR_DB_COLUMN_TYPE,
|
|
35
|
+
DB_COLUMN_TYPE_UUID,
|
|
36
|
+
DB_COLUMN_TYPE_JSONB,
|
|
37
|
+
DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ,
|
|
27
38
|
STORAGE_FLATTENED,
|
|
28
39
|
DOC_ATTR_DESCRIPTION,
|
|
29
40
|
applyColumnNamingStrategy, DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
@@ -32,6 +43,7 @@ import {
|
|
|
32
43
|
import type { SqlType } from "./sql-type.js";
|
|
33
44
|
import type {
|
|
34
45
|
Dialect, SchemaSnapshot, TableDescriptor, ColumnDescriptor, IndexDescriptor, FkDescriptor,
|
|
46
|
+
CheckDescriptor,
|
|
35
47
|
} from "./types.js";
|
|
36
48
|
import { buildExpectedViews } from "./expected-views.js";
|
|
37
49
|
import {
|
|
@@ -97,7 +109,7 @@ export function buildExpectedSchema(
|
|
|
97
109
|
// Schema is resolved here (not stored in Pass 1) to avoid exactOptionalPropertyTypes
|
|
98
110
|
// issues with `string | undefined` vs `schema?: string`.
|
|
99
111
|
const tables: TableDescriptor[] = entities.map(({ entity, tableName }) => {
|
|
100
|
-
const t = buildTable(entity, tableName, resolveTargetTable, root as MetaRoot, strategy);
|
|
112
|
+
const t = buildTable(entity, tableName, resolveTargetTable, root as MetaRoot, strategy, dialect);
|
|
101
113
|
const schema = resolveTableSchema(entity);
|
|
102
114
|
if (schema !== undefined) t.schema = schema;
|
|
103
115
|
return t;
|
|
@@ -142,6 +154,7 @@ function normalizeForSqlite(sqlType: SqlType): SqlType {
|
|
|
142
154
|
return { kind: "integer", bits: 64 };
|
|
143
155
|
case "timestamp":
|
|
144
156
|
case "date":
|
|
157
|
+
case "time":
|
|
145
158
|
return { kind: "text" };
|
|
146
159
|
case "integer":
|
|
147
160
|
// SQLite stores every INTEGER as a 64-bit value and Drizzle's int() emits
|
|
@@ -155,6 +168,12 @@ function normalizeForSqlite(sqlType: SqlType): SqlType {
|
|
|
155
168
|
// what the SQLite introspector produces, preventing a phantom
|
|
156
169
|
// change-column-type diff on every field.float column.
|
|
157
170
|
return { kind: "real" };
|
|
171
|
+
case "uuid":
|
|
172
|
+
// SQLite has no native uuid type; uuid values are stored as TEXT (the
|
|
173
|
+
// conformance corpus is Postgres-only, but TS supports a sqlite dialect).
|
|
174
|
+
// Collapse uuid → text so the expected snapshot matches what the SQLite
|
|
175
|
+
// introspector produces, preventing a phantom change-column-type diff.
|
|
176
|
+
return { kind: "text" };
|
|
158
177
|
default:
|
|
159
178
|
return sqlType;
|
|
160
179
|
}
|
|
@@ -166,6 +185,7 @@ function buildTable(
|
|
|
166
185
|
resolveTargetTable: (entityName: string) => string | undefined,
|
|
167
186
|
root: MetaRoot,
|
|
168
187
|
strategy: ColumnNamingStrategy,
|
|
188
|
+
dialect: Dialect | undefined,
|
|
169
189
|
): TableDescriptor {
|
|
170
190
|
// Use effective accessors so inherited fields/identities (from `extends:` /
|
|
171
191
|
// abstract bases like BaseEntity) are included.
|
|
@@ -201,6 +221,7 @@ function buildTable(
|
|
|
201
221
|
columns,
|
|
202
222
|
indexes: buildSecondaryIndexes(entity, tableName, strategy),
|
|
203
223
|
foreignKeys: buildForeignKeys(entity, tableName, resolveTargetTable, root, strategy),
|
|
224
|
+
checks: buildChecks(entity, tableName, strategy, dialect),
|
|
204
225
|
primaryKey,
|
|
205
226
|
};
|
|
206
227
|
const entityDesc = readDescription(entity);
|
|
@@ -258,6 +279,80 @@ function buildSecondaryIndexes(
|
|
|
258
279
|
return indexes;
|
|
259
280
|
}
|
|
260
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Derive a CHECK constraint per `field.enum` field: `CHECK (<col> IN ('A', 'B'))`,
|
|
284
|
+
* constraining the column to the declared `@values` members. The constraint name
|
|
285
|
+
* is `<table>_<column>_chk`, mirroring the FK/index naming conventions.
|
|
286
|
+
*
|
|
287
|
+
* `@values` is read effective (`field.attr`) so a concrete field that extends an
|
|
288
|
+
* abstract `field.enum` super inherits its members. The loader rejects a
|
|
289
|
+
* `field.enum` without `@values` (ERR_MISSING_REQUIRED_ATTR), so a present enum
|
|
290
|
+
* field always yields a non-empty member set; a defensive guard skips any edge
|
|
291
|
+
* case where the array is absent rather than emitting `IN ()`.
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* Map a single declared validator to a DB CHECK descriptor, or null when it has
|
|
295
|
+
* no SQL-expressible form on this dialect. The constraint name is
|
|
296
|
+
* `<table>_<col>_<validator>_chk`. The expression references the resolved physical
|
|
297
|
+
* column name verbatim (matching the enum-check convention).
|
|
298
|
+
*/
|
|
299
|
+
function validatorCheck(
|
|
300
|
+
v: MetaValidator, col: string, tableName: string, dialect: Dialect | undefined,
|
|
301
|
+
): CheckDescriptor | null {
|
|
302
|
+
switch (v.subType) {
|
|
303
|
+
case VALIDATOR_SUBTYPE_NUMERIC: {
|
|
304
|
+
const parts: string[] = [];
|
|
305
|
+
if (v.min !== undefined) parts.push(`${col} >= ${v.min}`);
|
|
306
|
+
if (v.max !== undefined) parts.push(`${col} <= ${v.max}`);
|
|
307
|
+
if (parts.length === 0) return null;
|
|
308
|
+
return { name: `${tableName}_${col}_numeric_chk`, expression: parts.join(" AND ") };
|
|
309
|
+
}
|
|
310
|
+
case VALIDATOR_SUBTYPE_LENGTH: {
|
|
311
|
+
const parts: string[] = [];
|
|
312
|
+
if (v.min !== undefined) parts.push(`length(${col}) >= ${v.min}`);
|
|
313
|
+
if (v.max !== undefined) parts.push(`length(${col}) <= ${v.max}`);
|
|
314
|
+
if (parts.length === 0) return null;
|
|
315
|
+
return { name: `${tableName}_${col}_length_chk`, expression: parts.join(" AND ") };
|
|
316
|
+
}
|
|
317
|
+
case VALIDATOR_SUBTYPE_REGEX: {
|
|
318
|
+
// Postgres-only: SQLite has no native regex operator.
|
|
319
|
+
if (dialect === "sqlite" || dialect === "d1") return null;
|
|
320
|
+
const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
|
|
321
|
+
if (typeof pattern !== "string" || pattern.length === 0) return null;
|
|
322
|
+
return {
|
|
323
|
+
name: `${tableName}_${col}_regex_chk`,
|
|
324
|
+
expression: `${col} ~ '${pattern.replace(/'/g, "''")}'`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
default:
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function buildChecks(
|
|
333
|
+
entity: MetaObject, tableName: string, strategy: ColumnNamingStrategy, dialect: Dialect | undefined,
|
|
334
|
+
): CheckDescriptor[] {
|
|
335
|
+
const checks: CheckDescriptor[] = [];
|
|
336
|
+
for (const field of entity.fields()) {
|
|
337
|
+
const col = resolveColumnName(field, strategy);
|
|
338
|
+
// Enum membership check (unchanged).
|
|
339
|
+
if (field.subType === FIELD_SUBTYPE_ENUM) {
|
|
340
|
+
const raw = field.attr(FIELD_ATTR_VALUES);
|
|
341
|
+
if (Array.isArray(raw) && raw.length > 0) {
|
|
342
|
+
const values = raw.map((v) => String(v));
|
|
343
|
+
const expression = `${col} IN (${values.map((v) => `'${v.replace(/'/g, "''")}'`).join(", ")})`;
|
|
344
|
+
checks.push({ name: `${tableName}_${col}_chk`, expression });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Validator-derived checks.
|
|
348
|
+
for (const v of field.validators()) {
|
|
349
|
+
const check = validatorCheck(v, col, tableName, dialect);
|
|
350
|
+
if (check) checks.push(check);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return checks;
|
|
354
|
+
}
|
|
355
|
+
|
|
261
356
|
function buildForeignKeys(
|
|
262
357
|
entity: MetaObject,
|
|
263
358
|
tableName: string,
|
|
@@ -375,6 +470,18 @@ function buildColumn(
|
|
|
375
470
|
}
|
|
376
471
|
|
|
377
472
|
function subtypeToSqlType(field: MetaData): SqlType {
|
|
473
|
+
// R6 Plan 2b: a physical @dbColumnType override selects the DB column type
|
|
474
|
+
// instead of the subtype default (the loader has already validated the
|
|
475
|
+
// (subtype × value) pairing, so an unrecognized value never reaches here).
|
|
476
|
+
const dbColumnType = field.ownAttr(FIELD_ATTR_DB_COLUMN_TYPE);
|
|
477
|
+
if (typeof dbColumnType === "string") {
|
|
478
|
+
switch (dbColumnType) {
|
|
479
|
+
case DB_COLUMN_TYPE_UUID: return { kind: "uuid" };
|
|
480
|
+
case DB_COLUMN_TYPE_JSONB: return { kind: "json" };
|
|
481
|
+
case DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ: return { kind: "timestamp", withTimezone: true };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
378
485
|
const subType = field.subType;
|
|
379
486
|
switch (subType) {
|
|
380
487
|
case FIELD_SUBTYPE_STRING: {
|
|
@@ -389,13 +496,26 @@ function subtypeToSqlType(field: MetaData): SqlType {
|
|
|
389
496
|
case FIELD_SUBTYPE_CURRENCY: return { kind: "integer", bits: 64 };
|
|
390
497
|
case FIELD_SUBTYPE_DOUBLE: return { kind: "real" };
|
|
391
498
|
case FIELD_SUBTYPE_FLOAT: return { kind: "real4" };
|
|
392
|
-
case FIELD_SUBTYPE_DECIMAL:
|
|
499
|
+
case FIELD_SUBTYPE_DECIMAL: {
|
|
500
|
+
// @precision/@scale are declared as ATTR_SUBTYPE_INT so the loader coerces them
|
|
501
|
+
// to numbers. Both present → NUMERIC(p,s); absent → bare NUMERIC (back-compat).
|
|
502
|
+
const precision = field.ownAttr(FIELD_ATTR_PRECISION);
|
|
503
|
+
const scale = field.ownAttr(FIELD_ATTR_SCALE);
|
|
504
|
+
if (typeof precision === "number" && typeof scale === "number") {
|
|
505
|
+
return { kind: "numeric", precision, scale };
|
|
506
|
+
}
|
|
507
|
+
if (typeof precision === "number") {
|
|
508
|
+
return { kind: "numeric", precision };
|
|
509
|
+
}
|
|
510
|
+
return { kind: "numeric" };
|
|
511
|
+
}
|
|
393
512
|
case FIELD_SUBTYPE_BOOLEAN: return { kind: "boolean" };
|
|
394
513
|
case FIELD_SUBTYPE_DATE: return { kind: "date" };
|
|
395
|
-
case FIELD_SUBTYPE_TIME: return { kind: "
|
|
514
|
+
case FIELD_SUBTYPE_TIME: return { kind: "time" }; // Postgres native TIME (whole-second wire form)
|
|
396
515
|
case FIELD_SUBTYPE_TIMESTAMP: return { kind: "timestamp", withTimezone: false };
|
|
397
516
|
case FIELD_SUBTYPE_OBJECT:
|
|
398
517
|
case FIELD_SUBTYPE_CLASS: return { kind: "json" };
|
|
518
|
+
case FIELD_SUBTYPE_UUID: return { kind: "uuid" }; // R6 Plan 2a — Postgres native uuid
|
|
399
519
|
default: return { kind: "text" }; // unknown → text fallback
|
|
400
520
|
}
|
|
401
521
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,10 +11,24 @@
|
|
|
11
11
|
export { buildExpectedSchema } from "./expected-schema.js";
|
|
12
12
|
export { introspect, introspectPostgres, introspectSqlite } from "./introspect/index.js";
|
|
13
13
|
export { diff } from "./diff/index.js";
|
|
14
|
+
export { computeDrift, type ComputeDriftOptions } from "./drift/drift.js";
|
|
15
|
+
export { classifyDrift, driftAgainstSnapshot } from "./drift/classify.js";
|
|
16
|
+
export type { DriftClassification } from "./drift/classify.js";
|
|
14
17
|
export { emit } from "./emit/index.js";
|
|
15
18
|
export { writeMigration } from "./write-migration.js";
|
|
16
19
|
export { writeMigrationD1 } from "./write-migration-d1.js";
|
|
17
20
|
|
|
21
|
+
// Reference-snapshot generation (offline, deterministic).
|
|
22
|
+
export {
|
|
23
|
+
serializeSnapshot,
|
|
24
|
+
parseSnapshot,
|
|
25
|
+
SNAPSHOT_FORMAT_VERSION,
|
|
26
|
+
} from "./snapshot/serialize.js";
|
|
27
|
+
export { snapshotChecksum } from "./snapshot/checksum.js";
|
|
28
|
+
export { snapshotPath, readSnapshot, writeSnapshot } from "./snapshot/store.js";
|
|
29
|
+
export { planOffline, baselineFromMetadata } from "./snapshot/plan.js";
|
|
30
|
+
export type { PlanOfflineArgs, PlanOfflineResult } from "./snapshot/plan.js";
|
|
31
|
+
|
|
18
32
|
// Errors
|
|
19
33
|
export { BlockedChangesError, SetNullNotNullableError } from "./errors.js";
|
|
20
34
|
|
|
@@ -37,6 +51,7 @@ export type { WriteMigrationOptions, WriteMigrationResult } from "./write-migrat
|
|
|
37
51
|
export type { WriteMigrationD1Options, WriteMigrationD1Result } from "./write-migration-d1.js";
|
|
38
52
|
|
|
39
53
|
// View diff + dialect emitters
|
|
54
|
+
export { normalizeViewSql, viewSqlEquals } from "./view-sql-compare.js";
|
|
40
55
|
export { classifyViewDiff } from "./view-diff.js";
|
|
41
56
|
export type { ViewShape, ViewDiffClass, ViewMigrationOpts } from "./view-diff.js";
|
|
42
57
|
export { emitPostgresViewMigration } from "./view-ddl-postgres.js";
|
|
@@ -60,6 +75,35 @@ export {
|
|
|
60
75
|
// D1 introspection
|
|
61
76
|
export { introspectD1, type D1Runner, type IntrospectD1Options } from "./introspect/d1.js";
|
|
62
77
|
|
|
78
|
+
// Migration-history ledger + ordered transactional apply (postgres/sqlite)
|
|
79
|
+
export {
|
|
80
|
+
ensureLedger,
|
|
81
|
+
recordApplied,
|
|
82
|
+
deleteApplied,
|
|
83
|
+
appliedNames,
|
|
84
|
+
appliedRecords,
|
|
85
|
+
recordBaseline,
|
|
86
|
+
baselineRecord,
|
|
87
|
+
BASELINE_NAME,
|
|
88
|
+
MIGRATIONS_TABLE,
|
|
89
|
+
DEFAULT_LEDGER_SCHEMA,
|
|
90
|
+
type LedgerRow,
|
|
91
|
+
type LedgerOptions,
|
|
92
|
+
type LedgerDialect,
|
|
93
|
+
} from "./apply/ledger.js";
|
|
94
|
+
export {
|
|
95
|
+
applyPending,
|
|
96
|
+
rollbackTo,
|
|
97
|
+
type ApplyPendingOptions,
|
|
98
|
+
type ApplyPendingResult,
|
|
99
|
+
type RollbackToOptions,
|
|
100
|
+
type RollbackToResult,
|
|
101
|
+
} from "./apply/apply.js";
|
|
102
|
+
|
|
103
|
+
// Snapshot-integrity: replay migrations and assert == committed snapshot.
|
|
104
|
+
export { verifyReplay } from "./verify/replay.js";
|
|
105
|
+
export type { VerifyReplayArgs, VerifyReplayResult } from "./verify/replay.js";
|
|
106
|
+
|
|
63
107
|
// Wrangler config helpers
|
|
64
108
|
export {
|
|
65
109
|
findWranglerConfig,
|
package/src/introspect/d1.ts
CHANGED
|
@@ -71,6 +71,7 @@ export async function introspectD1(opts: IntrospectD1Options): Promise<SchemaSna
|
|
|
71
71
|
columns: cols,
|
|
72
72
|
indexes: await readIndexes(exec, name),
|
|
73
73
|
foreignKeys: await readForeignKeys(exec, name),
|
|
74
|
+
checks: [], // CHECK introspection is out of scope; expected-side derives them
|
|
74
75
|
primaryKey: pk,
|
|
75
76
|
});
|
|
76
77
|
}
|
|
@@ -28,8 +28,10 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { Kysely } from "kysely";
|
|
30
30
|
import { sql } from "kysely";
|
|
31
|
-
import type { SchemaSnapshot, TableDescriptor, ColumnDescriptor, ColumnDefault, IndexDescriptor, FkDescriptor, FkAction, ViewDescriptor } from "../types.js";
|
|
31
|
+
import type { SchemaSnapshot, TableDescriptor, ColumnDescriptor, ColumnDefault, IndexDescriptor, FkDescriptor, FkAction, ViewDescriptor, CheckDescriptor } from "../types.js";
|
|
32
32
|
import type { SqlType } from "../sql-type.js";
|
|
33
|
+
import { MIGRATIONS_TABLE } from "../apply/ledger.js";
|
|
34
|
+
import { stripCheckWrapper } from "../check-expr-compare.js";
|
|
33
35
|
|
|
34
36
|
// ---------------------------------------------------------------------------
|
|
35
37
|
// Public API
|
|
@@ -51,6 +53,7 @@ export async function introspectPostgres(db: Kysely<Record<string, unknown>>): P
|
|
|
51
53
|
columns,
|
|
52
54
|
indexes: await readPgIndexes(k, schema, name),
|
|
53
55
|
foreignKeys: await readPgForeignKeys(k, schema, name),
|
|
56
|
+
checks: await readPgChecks(k, schema, name),
|
|
54
57
|
primaryKey,
|
|
55
58
|
});
|
|
56
59
|
}
|
|
@@ -121,6 +124,7 @@ export function pgTypeToSqlType(dataType: string, maxLength?: number | null): Sq
|
|
|
121
124
|
|
|
122
125
|
// Date + time
|
|
123
126
|
if (dt === "date") return { kind: "date" };
|
|
127
|
+
if (dt === "time" || dt === "time without time zone") return { kind: "time" };
|
|
124
128
|
if (dt === "timestamp" || dt === "timestamp without time zone") {
|
|
125
129
|
return { kind: "timestamp", withTimezone: false };
|
|
126
130
|
}
|
|
@@ -207,6 +211,7 @@ async function readTableNames(k: Kysely<any>): Promise<SchemaTableRef[]> {
|
|
|
207
211
|
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
208
212
|
AND table_schema NOT LIKE 'pg_%'
|
|
209
213
|
AND table_type = 'BASE TABLE'
|
|
214
|
+
AND table_name <> ${MIGRATIONS_TABLE}
|
|
210
215
|
ORDER BY table_schema, table_name
|
|
211
216
|
`.execute(k);
|
|
212
217
|
return rows.rows.map((r) => ({ schema: r.table_schema, name: r.table_name }));
|
|
@@ -217,13 +222,20 @@ async function readPgViews(k: RawKysely): Promise<ViewDescriptor[]> {
|
|
|
217
222
|
// "relation views does not exist". We catch and return [] so other tests
|
|
218
223
|
// still pass on pg-mem. Real PG (Postgres 16) handles this correctly.
|
|
219
224
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
225
|
+
// information_schema.views.view_definition is the SELECT body (not the
|
|
226
|
+
// full CREATE VIEW statement). We carry it through on the descriptor so the
|
|
227
|
+
// diff can detect view-body drift (not just name presence).
|
|
228
|
+
const rows = await sql<{ table_name: string; table_schema: string; view_definition: string | null }>`
|
|
229
|
+
SELECT table_name, table_schema, view_definition FROM information_schema.views
|
|
222
230
|
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
223
231
|
AND table_schema NOT LIKE 'pg_%'
|
|
224
232
|
ORDER BY table_schema, table_name
|
|
225
233
|
`.execute(k);
|
|
226
|
-
return rows.rows.map((r) =>
|
|
234
|
+
return rows.rows.map((r) => {
|
|
235
|
+
const view: ViewDescriptor = { name: r.table_name, schema: r.table_schema };
|
|
236
|
+
if (r.view_definition) view.sql = r.view_definition;
|
|
237
|
+
return view;
|
|
238
|
+
});
|
|
227
239
|
} catch {
|
|
228
240
|
// pg-mem: information_schema.views not supported — return empty view list.
|
|
229
241
|
return [];
|
|
@@ -426,3 +438,25 @@ function pgRuleToAction(rule: string): FkAction {
|
|
|
426
438
|
if (r === "RESTRICT") return "restrict";
|
|
427
439
|
return "no-action";
|
|
428
440
|
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Read CHECK constraints for a table from pg_constraint. pg-mem does not support
|
|
444
|
+
* pg_constraint, so this catches and returns [] there (same accepted gap as
|
|
445
|
+
* readPgForeignKeys/readPgIndexes); real-DB coverage is the MIGRATE_TS_PG_URL-gated
|
|
446
|
+
* integration test. `pg_get_constraintdef` returns `CHECK (<expr>)`; the wrapper is
|
|
447
|
+
* stripped to the expression, compared via normalizeCheckExpr at diff time.
|
|
448
|
+
*/
|
|
449
|
+
async function readPgChecks(k: RawKysely, schema: string, table: string): Promise<CheckDescriptor[]> {
|
|
450
|
+
try {
|
|
451
|
+
const rows = await sql<{ name: string; def: string }>`
|
|
452
|
+
SELECT con.conname AS name, pg_get_constraintdef(con.oid) AS def
|
|
453
|
+
FROM pg_constraint con
|
|
454
|
+
JOIN pg_class rel ON rel.oid = con.conrelid
|
|
455
|
+
JOIN pg_namespace ns ON ns.oid = rel.relnamespace
|
|
456
|
+
WHERE con.contype = 'c' AND rel.relname = ${table} AND ns.nspname = ${schema}
|
|
457
|
+
`.execute(k);
|
|
458
|
+
return rows.rows.map((r) => ({ name: r.name, expression: stripCheckWrapper(r.def) }));
|
|
459
|
+
} catch {
|
|
460
|
+
return []; // pg-mem: pg_constraint unsupported
|
|
461
|
+
}
|
|
462
|
+
}
|
package/src/introspect/sqlite.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
IndexDescriptor, FkDescriptor, FkAction, ViewDescriptor,
|
|
6
6
|
} from "../types.js";
|
|
7
7
|
import { parseSqliteDefault, sqliteTypeToSqlType, sqliteRuleToAction } from "./sqlite-shared.js";
|
|
8
|
+
import { MIGRATIONS_TABLE } from "../apply/ledger.js";
|
|
8
9
|
|
|
9
10
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
11
|
type RawKysely = Kysely<any>;
|
|
@@ -18,6 +19,7 @@ export async function introspectSqlite(db: Kysely<Record<string, unknown>>): Pro
|
|
|
18
19
|
const tableNamesRows = await sql<{ name: string; sql: string | null }>`
|
|
19
20
|
SELECT name, sql FROM sqlite_master
|
|
20
21
|
WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__new_%'
|
|
22
|
+
AND name <> ${MIGRATIONS_TABLE}
|
|
21
23
|
ORDER BY name
|
|
22
24
|
`.execute(k);
|
|
23
25
|
|
|
@@ -36,6 +38,7 @@ export async function introspectSqlite(db: Kysely<Record<string, unknown>>): Pro
|
|
|
36
38
|
columns: cols,
|
|
37
39
|
indexes: await readSqliteIndexes(k, t.name),
|
|
38
40
|
foreignKeys: await readSqliteForeignKeys(k, t.name),
|
|
41
|
+
checks: [], // CHECK introspection is out of scope; expected-side derives them
|
|
39
42
|
primaryKey: pk,
|
|
40
43
|
});
|
|
41
44
|
}
|
|
@@ -45,11 +48,18 @@ export async function introspectSqlite(db: Kysely<Record<string, unknown>>): Pro
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
async function readSqliteViews(k: RawKysely): Promise<ViewDescriptor[]> {
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
// sqlite_master.sql holds the full `CREATE VIEW <name> AS <body>` statement.
|
|
52
|
+
// We carry it through on the descriptor so the diff can detect view-body
|
|
53
|
+
// drift (not just name presence).
|
|
54
|
+
const rows = await sql<{ name: string; sql: string | null }>`
|
|
55
|
+
SELECT name, sql FROM sqlite_master WHERE type='view' AND name NOT LIKE 'sqlite_%'
|
|
50
56
|
ORDER BY name
|
|
51
57
|
`.execute(k);
|
|
52
|
-
return rows.rows.map((r) =>
|
|
58
|
+
return rows.rows.map((r) => {
|
|
59
|
+
const view: ViewDescriptor = { name: r.name };
|
|
60
|
+
if (r.sql) view.sql = r.sql;
|
|
61
|
+
return view;
|
|
62
|
+
});
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
async function readSqliteColumns(k: RawKysely, table: string): Promise<ColumnDescriptor[]> {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/snapshot/checksum.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import type { SchemaSnapshot } from "../types.js";
|
|
4
|
+
import { serializeSnapshot } from "./serialize.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deterministic sha256 of a schema snapshot. Reuses the canonical
|
|
8
|
+
* (order-stable, byte-identical) serializer, so the hash is independent of
|
|
9
|
+
* table/column ordering and depends only on the schema's content. Used to make
|
|
10
|
+
* the committed snapshot tamper-evident (record the hash; a later hand-edit
|
|
11
|
+
* changes it) and as the baseline marker's payload.
|
|
12
|
+
*/
|
|
13
|
+
export function snapshotChecksum(snapshot: SchemaSnapshot): string {
|
|
14
|
+
return createHash("sha256").update(serializeSnapshot(snapshot), "utf8").digest("hex");
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/snapshot/plan.ts
|
|
2
|
+
import type { ColumnNamingStrategy, MetaData } from "@metaobjectsdev/metadata";
|
|
3
|
+
import { buildExpectedSchema } from "../expected-schema.js";
|
|
4
|
+
import { diff, type DiffArgs } from "../diff/index.js";
|
|
5
|
+
import type { Dialect, DiffResult, SchemaSnapshot } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export interface PlanOfflineArgs extends Pick<DiffArgs, "allow" | "onAmbiguous" | "ignoreTables"> {
|
|
8
|
+
metadata: MetaData;
|
|
9
|
+
dialect: Dialect;
|
|
10
|
+
/** The stored reference snapshot (the "from" side). Use `{ tables: [], views: [] }` for a fresh project. */
|
|
11
|
+
snapshot: SchemaSnapshot;
|
|
12
|
+
columnNamingStrategy?: ColumnNamingStrategy;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PlanOfflineResult {
|
|
16
|
+
/** The change set to emit, from diffing metadata-expected against the snapshot. */
|
|
17
|
+
diff: DiffResult;
|
|
18
|
+
/** The schema the migration brings us to — write this back as the new snapshot on accept. */
|
|
19
|
+
nextSnapshot: SchemaSnapshot;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Plan a migration offline: build the expected schema from metadata and diff it
|
|
24
|
+
* against the stored snapshot. No database. The caller emits `diff` and, on
|
|
25
|
+
* accept, persists `nextSnapshot` via writeSnapshot.
|
|
26
|
+
*/
|
|
27
|
+
export async function planOffline(args: PlanOfflineArgs): Promise<PlanOfflineResult> {
|
|
28
|
+
const nextSnapshot = buildExpectedSchema(args.metadata, {
|
|
29
|
+
dialect: args.dialect,
|
|
30
|
+
...(args.columnNamingStrategy ? { columnNamingStrategy: args.columnNamingStrategy } : {}),
|
|
31
|
+
});
|
|
32
|
+
const result = await diff({
|
|
33
|
+
expected: nextSnapshot,
|
|
34
|
+
actual: args.snapshot,
|
|
35
|
+
dialect: args.dialect,
|
|
36
|
+
...(args.allow ? { allow: args.allow } : {}),
|
|
37
|
+
...(args.onAmbiguous ? { onAmbiguous: args.onAmbiguous } : {}),
|
|
38
|
+
...(args.ignoreTables ? { ignoreTables: args.ignoreTables } : {}),
|
|
39
|
+
});
|
|
40
|
+
return { diff: result, nextSnapshot };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Seed an initial reference snapshot from metadata (greenfield baseline). */
|
|
44
|
+
export function baselineFromMetadata(
|
|
45
|
+
metadata: MetaData,
|
|
46
|
+
dialect: Dialect,
|
|
47
|
+
columnNamingStrategy?: ColumnNamingStrategy,
|
|
48
|
+
): SchemaSnapshot {
|
|
49
|
+
return buildExpectedSchema(metadata, {
|
|
50
|
+
dialect,
|
|
51
|
+
...(columnNamingStrategy ? { columnNamingStrategy } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|