@metaobjectsdev/migrate-ts 0.6.0-rc.1 → 0.7.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 +4 -2
- package/dist/diff/index.d.ts.map +1 -1
- package/dist/diff/index.js +23 -0
- package/dist/diff/index.js.map +1 -1
- package/dist/emit/index.d.ts.map +1 -1
- package/dist/emit/index.js +7 -3
- package/dist/emit/index.js.map +1 -1
- package/dist/emit/postgres.d.ts.map +1 -1
- package/dist/emit/postgres.js +22 -11
- package/dist/emit/postgres.js.map +1 -1
- package/dist/expected-schema.d.ts +7 -1
- package/dist/expected-schema.d.ts.map +1 -1
- package/dist/expected-schema.js +35 -30
- package/dist/expected-schema.js.map +1 -1
- package/dist/expected-views.d.ts +5 -0
- package/dist/expected-views.d.ts.map +1 -0
- package/dist/expected-views.js +147 -0
- package/dist/expected-views.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/diff/index.ts +29 -0
- package/src/emit/index.ts +9 -5
- package/src/emit/postgres.ts +25 -12
- package/src/expected-schema.ts +48 -27
- package/src/expected-views.ts +175 -0
- package/src/types.ts +11 -4
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// expected-views.ts — derive ViewDescriptor[] from projection metadata.
|
|
2
|
+
//
|
|
3
|
+
// Ports the shape of csharp/MetaObjects.Codegen/Schema/PostgresSchema.cs's
|
|
4
|
+
// CreateView. v1 supports:
|
|
5
|
+
//
|
|
6
|
+
// * passthrough origin (no @via) → plain column from the single base entity
|
|
7
|
+
// * aggregate origin (@agg/@of/@via) → correlated subquery over a to-many
|
|
8
|
+
//
|
|
9
|
+
// Deferred to v2 (returns a "-- TODO" view that the runner skips applying):
|
|
10
|
+
//
|
|
11
|
+
// * passthrough origin WITH @via → to-one correlated subquery
|
|
12
|
+
// * collection origin → json_agg over a to-many
|
|
13
|
+
// * multi-base projections → blocked
|
|
14
|
+
//
|
|
15
|
+
// Identifiers are quoted throughout so mixed-case column names (e.g.
|
|
16
|
+
// "programId") survive PG's case-folding pass.
|
|
17
|
+
import { MetaPassthroughOrigin, MetaAggregateOrigin, MetaCollectionOrigin, MetaSource, SOURCE_ROLE_PRIMARY, TYPE_OBJECT, TYPE_ORIGIN, resolveColumnName, resolveTableName, stripPackage, } from "@metaobjectsdev/metadata";
|
|
18
|
+
export function buildExpectedViews(root, strategy) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const child of root.ownChildren()) {
|
|
21
|
+
if (child.type !== TYPE_OBJECT)
|
|
22
|
+
continue;
|
|
23
|
+
const proj = child;
|
|
24
|
+
if (proj.isAbstract)
|
|
25
|
+
continue;
|
|
26
|
+
if (!isReadOnlyProjection(proj))
|
|
27
|
+
continue;
|
|
28
|
+
const view = buildView(proj, root, strategy);
|
|
29
|
+
if (view !== null)
|
|
30
|
+
out.push(view);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function isReadOnlyProjection(entity) {
|
|
35
|
+
const sources = entity.ownChildren().filter((c) => c instanceof MetaSource);
|
|
36
|
+
if (sources.length === 0)
|
|
37
|
+
return false;
|
|
38
|
+
const hasReadOnly = sources.some((s) => s.isReadOnly());
|
|
39
|
+
const hasWritable = sources.some((s) => !s.isReadOnly());
|
|
40
|
+
return hasReadOnly && !hasWritable;
|
|
41
|
+
}
|
|
42
|
+
function buildView(projection, root, strategy) {
|
|
43
|
+
const primarySource = projection.ownChildren().find((c) => c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY);
|
|
44
|
+
if (primarySource?.tableName === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
const viewName = primarySource.tableName;
|
|
47
|
+
const cols = [];
|
|
48
|
+
let baseEntity;
|
|
49
|
+
let blocked;
|
|
50
|
+
const T = "t"; // target alias in correlated subqueries
|
|
51
|
+
const baseTable = () => {
|
|
52
|
+
if (baseEntity === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const found = root.findObject(baseEntity);
|
|
55
|
+
return found ? resolveTableName(found) : baseEntity;
|
|
56
|
+
};
|
|
57
|
+
for (const f of projection.fields()) {
|
|
58
|
+
const origin = f.ownChildren().find((c) => c.type === TYPE_ORIGIN);
|
|
59
|
+
const fieldCol = resolveColumnName(f, strategy);
|
|
60
|
+
if (origin instanceof MetaPassthroughOrigin && origin.via === undefined &&
|
|
61
|
+
origin.from !== undefined && origin.from.includes(".")) {
|
|
62
|
+
const [ent, field] = splitDot(origin.from);
|
|
63
|
+
const bare = stripPackage(ent);
|
|
64
|
+
baseEntity ??= bare;
|
|
65
|
+
if (bare !== baseEntity) {
|
|
66
|
+
blocked = "passthrough from multiple base entities";
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const srcEntity = root.findObject(baseEntity);
|
|
70
|
+
const srcCol = resolveColumnByName(srcEntity, field, strategy);
|
|
71
|
+
cols.push(` "${srcCol}" AS "${fieldCol}"`);
|
|
72
|
+
}
|
|
73
|
+
else if (origin instanceof MetaAggregateOrigin && origin.agg !== undefined &&
|
|
74
|
+
origin.of !== undefined && origin.via !== undefined &&
|
|
75
|
+
origin.of.includes(".") && origin.via.includes(".")) {
|
|
76
|
+
const [baseEnt, relName] = splitDot(origin.via);
|
|
77
|
+
const bareBase = stripPackage(baseEnt);
|
|
78
|
+
baseEntity ??= bareBase;
|
|
79
|
+
if (bareBase !== baseEntity) {
|
|
80
|
+
blocked = "aggregate over a different base entity";
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const fk = resolveToManyFk(root, baseEntity, relName, strategy);
|
|
84
|
+
if (fk === null) {
|
|
85
|
+
blocked = `unresolved to-many FK for @via "${origin.via}" (target needs an identity.reference back to ${baseEntity})`;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
const ofCol = resolveColumnByName(fk.target, splitDot(origin.of)[1], strategy);
|
|
89
|
+
cols.push(` (SELECT ${origin.agg}(${T}."${ofCol}") ` +
|
|
90
|
+
`FROM "${fk.targetTable}" ${T} ` +
|
|
91
|
+
`WHERE ${T}."${fk.fkCol}" = "${baseTable()}"."${fk.parentCol}") AS "${fieldCol}"`);
|
|
92
|
+
}
|
|
93
|
+
else if (origin instanceof MetaPassthroughOrigin || origin instanceof MetaCollectionOrigin) {
|
|
94
|
+
blocked = `field "${f.name}" uses an origin shape not yet supported by TS migrate-ts (passthrough-via / collection — deferred)`;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
blocked = `field "${f.name}" has no resolvable origin`;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (blocked !== undefined || baseEntity === undefined || cols.length === 0) {
|
|
103
|
+
// Caller decides what to do with a null result. For v1 the runner just
|
|
104
|
+
// omits it from expected.views — diff sees no expected view, no
|
|
105
|
+
// create-view change, and an actual leftover view (if any) gets dropped.
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const sql = `SELECT\n${cols.join(",\n")}\nFROM "${baseTable()}"`;
|
|
109
|
+
const view = { name: viewName, sql };
|
|
110
|
+
return view;
|
|
111
|
+
}
|
|
112
|
+
function splitDot(s) {
|
|
113
|
+
const i = s.indexOf(".");
|
|
114
|
+
return [s.slice(0, i), s.slice(i + 1)];
|
|
115
|
+
}
|
|
116
|
+
function resolveColumnByName(owner, fieldName, strategy) {
|
|
117
|
+
if (owner === undefined)
|
|
118
|
+
return fieldName;
|
|
119
|
+
const field = owner.fields().find((f) => f.name === fieldName);
|
|
120
|
+
return field ? resolveColumnName(field, strategy) : fieldName;
|
|
121
|
+
}
|
|
122
|
+
function resolveToManyFk(root, baseEntityName, relName, strategy) {
|
|
123
|
+
const baseObj = root.findObject(baseEntityName);
|
|
124
|
+
if (baseObj === undefined)
|
|
125
|
+
return null;
|
|
126
|
+
const rel = baseObj.relationships().find((r) => r.name === relName);
|
|
127
|
+
if (rel === undefined || rel.objectRef === undefined)
|
|
128
|
+
return null;
|
|
129
|
+
const target = root.findObject(stripPackage(rel.objectRef));
|
|
130
|
+
if (target === undefined)
|
|
131
|
+
return null;
|
|
132
|
+
const fkRef = target.referenceIdentities().find((r) => r.targetEntity !== undefined && stripPackage(r.targetEntity) === baseEntityName);
|
|
133
|
+
if (fkRef === undefined)
|
|
134
|
+
return null;
|
|
135
|
+
const fkFields = fkRef.fields;
|
|
136
|
+
if (fkFields.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
const fkCol = resolveColumnByName(target, fkFields[0], strategy);
|
|
139
|
+
const parentFieldName = fkRef.targetFields.length > 0
|
|
140
|
+
? fkRef.targetFields[0]
|
|
141
|
+
: baseObj.primaryIdentity()?.fields[0];
|
|
142
|
+
if (parentFieldName === undefined)
|
|
143
|
+
return null;
|
|
144
|
+
const parentCol = resolveColumnByName(baseObj, parentFieldName, strategy);
|
|
145
|
+
return { target, targetTable: resolveTableName(target), fkCol, parentCol };
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=expected-views.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expected-views.js","sourceRoot":"","sources":["../src/expected-views.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -68,6 +68,14 @@ export interface ViewDescriptor {
|
|
|
68
68
|
name: string;
|
|
69
69
|
/** Same semantics as TableDescriptor.schema. */
|
|
70
70
|
schema?: string;
|
|
71
|
+
/**
|
|
72
|
+
* View body: everything between `CREATE VIEW <name> AS` and the trailing `;`
|
|
73
|
+
* (the SELECT clause through the FROM/WHERE/GROUP-BY tail). Populated by
|
|
74
|
+
* `buildExpectedSchema` from projection metadata; omitted by introspect
|
|
75
|
+
* (body-level comparison isn't implemented yet — diff matches by name only,
|
|
76
|
+
* so a body change does NOT trigger replace-view today).
|
|
77
|
+
*/
|
|
78
|
+
sql?: string;
|
|
71
79
|
}
|
|
72
80
|
/**
|
|
73
81
|
* Every variant with a `table: string` (or `table: TableDescriptor`) field also
|
|
@@ -166,14 +174,17 @@ export type Change = {
|
|
|
166
174
|
} | {
|
|
167
175
|
kind: "create-view";
|
|
168
176
|
view: ViewDescriptor;
|
|
177
|
+
schema?: string;
|
|
169
178
|
status: ChangeStatus;
|
|
170
179
|
} | {
|
|
171
180
|
kind: "drop-view";
|
|
172
181
|
view: string;
|
|
182
|
+
schema?: string;
|
|
173
183
|
status: ChangeStatus;
|
|
174
184
|
} | {
|
|
175
185
|
kind: "replace-view";
|
|
176
186
|
view: ViewDescriptor;
|
|
187
|
+
schema?: string;
|
|
177
188
|
status: ChangeStatus;
|
|
178
189
|
};
|
|
179
190
|
export type ChangeKind = Change["kind"];
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAO7C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB;;;OAGG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/migrate-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-rc.1",
|
|
4
4
|
"description": "Schema migration tooling for MetaObjects: diff metadata vs DB and emit SQL for Postgres and SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@iarna/toml": "^2.2.5",
|
|
37
|
-
"@metaobjectsdev/metadata": "0.
|
|
37
|
+
"@metaobjectsdev/metadata": "0.7.0-rc.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"kysely": ">=0.27.0"
|
package/src/diff/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SchemaSnapshot, TableDescriptor, ColumnDescriptor, IndexDescriptor, FkDescriptor,
|
|
3
|
+
ViewDescriptor,
|
|
3
4
|
Change, ChangeStatus, DiffResult, AllowOptions, AmbiguousCallback,
|
|
4
5
|
} from "../types.js";
|
|
5
6
|
import type { SqlType } from "../sql-type.js";
|
|
@@ -150,6 +151,13 @@ export async function diff(
|
|
|
150
151
|
diffTableForeignKeys(expectedTable, actualTable, changes);
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
// Pass 2b: views. Identity is (schema, name). Body comparison isn't
|
|
155
|
+
// implemented — introspect doesn't read view bodies today, so a name match
|
|
156
|
+
// is no-change regardless of how the user's projection metadata has evolved.
|
|
157
|
+
// Users who change a view body without renaming need to manually drop+recreate
|
|
158
|
+
// or do a full bootstrap until replace-view-from-body-diff lands.
|
|
159
|
+
diffViews(args.expected.views, args.actual.views, changes);
|
|
160
|
+
|
|
153
161
|
// Pass 3: detect table renames BEFORE column renames — so a renamed table's
|
|
154
162
|
// columns are not scanned as orphaned drop/add pairs.
|
|
155
163
|
await detectTableRenames(changes, args.onAmbiguous);
|
|
@@ -273,6 +281,27 @@ function diffTableForeignKeys(
|
|
|
273
281
|
}
|
|
274
282
|
}
|
|
275
283
|
|
|
284
|
+
function viewIdentity(v: { name: string; schema?: string }): string {
|
|
285
|
+
return (v.schema ?? DEFAULT_DB_SCHEMA_POSTGRES) + "." + v.name;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function diffViews(
|
|
289
|
+
expected: ViewDescriptor[], actual: ViewDescriptor[], changes: Change[],
|
|
290
|
+
): void {
|
|
291
|
+
const exp = new Map(expected.map((v) => [viewIdentity(v), v] as const));
|
|
292
|
+
const act = new Map(actual.map((v) => [viewIdentity(v), v] as const));
|
|
293
|
+
for (const [id, v] of exp) {
|
|
294
|
+
if (!act.has(id)) {
|
|
295
|
+
changes.push({ kind: "create-view", view: v, ...schemaSpread(v.schema), status: ALLOWED });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (const [id, v] of act) {
|
|
299
|
+
if (!exp.has(id)) {
|
|
300
|
+
changes.push({ kind: "drop-view", view: v.name, ...schemaSpread(v.schema), status: ALLOWED });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
276
305
|
function columnDefaultsEqual(a: ColumnDescriptor["default"], b: ColumnDescriptor["default"]): boolean {
|
|
277
306
|
if (a === undefined && b === undefined) return true;
|
|
278
307
|
if (a === undefined || b === undefined) return false;
|
package/src/emit/index.ts
CHANGED
|
@@ -25,11 +25,15 @@ export function emit(changes: Change[], opts: EmitOptions): EmitResult {
|
|
|
25
25
|
const blocked = changes.filter((c) => c.status.state === "blocked");
|
|
26
26
|
if (blocked.length > 0) throw new BlockedChangesError(blocked);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// Views are postgres-only for now (sqlite/d1 DDL renderers don't handle them).
|
|
29
|
+
if (opts.dialect !== "postgres") {
|
|
30
|
+
const viewChanges = changes.filter((c) => VIEW_KINDS.has(c.kind));
|
|
31
|
+
if (viewChanges.length > 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`view migration not implemented for dialect "${opts.dialect}" ` +
|
|
34
|
+
`(${viewChanges.length} view-targeting change(s); postgres-only today)`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
switch (opts.dialect) {
|
package/src/emit/postgres.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Change, EmitResult, ColumnDescriptor, IndexDescriptor, FkDescriptor,
|
|
3
|
-
TableDescriptor, ColumnDefault, FkAction,
|
|
3
|
+
TableDescriptor, ViewDescriptor, ColumnDefault, FkAction,
|
|
4
4
|
} from "../types.js";
|
|
5
5
|
import type { SqlType } from "../sql-type.js";
|
|
6
6
|
import { DEFAULT_DB_SCHEMA_POSTGRES } from "@metaobjectsdev/metadata";
|
|
7
7
|
|
|
8
|
+
// Stages run low → high. drop-view + drop-fk run BEFORE drop-table so a view
|
|
9
|
+
// that depends on a soon-to-be-dropped table is removed first. create-view
|
|
10
|
+
// runs AFTER add-fk so the view can reference the new schema in full.
|
|
8
11
|
const STAGE_ORDER: Record<Change["kind"], number> = {
|
|
12
|
+
"drop-view": 0,
|
|
9
13
|
"create-table": 1,
|
|
10
14
|
"add-column": 2, "drop-column": 2,
|
|
11
15
|
"change-column-type": 2, "change-column-nullable": 2, "change-column-default": 2,
|
|
@@ -13,8 +17,7 @@ const STAGE_ORDER: Record<Change["kind"], number> = {
|
|
|
13
17
|
"add-index": 4, "drop-index": 4,
|
|
14
18
|
"add-fk": 5, "drop-fk": 5,
|
|
15
19
|
"drop-table": 6,
|
|
16
|
-
|
|
17
|
-
"create-view": 99, "drop-view": 99, "replace-view": 99,
|
|
20
|
+
"create-view": 7, "replace-view": 7,
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
export function renderPostgres(changes: Change[]): EmitResult {
|
|
@@ -58,11 +61,9 @@ function renderUp(c: Change): string {
|
|
|
58
61
|
case "drop-index": return `DROP INDEX ${quoteIndexQualified(c.index, c.schema)};`;
|
|
59
62
|
case "add-fk": return renderAddFk(c.table, c.schema, c.fk);
|
|
60
63
|
case "drop-fk": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.fk)};`;
|
|
61
|
-
case "create-view":
|
|
62
|
-
case "drop-view":
|
|
63
|
-
case "replace-view":
|
|
64
|
-
// emit() filters these; defensive throw if reached.
|
|
65
|
-
throw new Error(`unexpected view-kind in renderPostgres: ${c.kind}`);
|
|
64
|
+
case "create-view": return renderCreateView(c.view, c.schema, /* orReplace */ false);
|
|
65
|
+
case "drop-view": return `DROP VIEW ${quoteQualifiedView(c.view, c.schema)};`;
|
|
66
|
+
case "replace-view": return renderCreateView(c.view, c.schema, /* orReplace */ true);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -87,10 +88,9 @@ function renderDown(c: Change): string {
|
|
|
87
88
|
case "drop-index": return `-- WARNING: down migration cannot restore the original index definition`;
|
|
88
89
|
case "add-fk": return `ALTER TABLE ${quoteQualified(c.table, c.schema)} DROP CONSTRAINT ${quote(c.fk.name)};`;
|
|
89
90
|
case "drop-fk": return `-- WARNING: down migration cannot restore the original FK definition`;
|
|
90
|
-
case "create-view":
|
|
91
|
-
case "drop-view":
|
|
92
|
-
case "replace-view":
|
|
93
|
-
throw new Error(`unexpected view-kind in renderPostgres: ${c.kind}`);
|
|
91
|
+
case "create-view": return `DROP VIEW ${quoteQualifiedView(c.view.name, c.schema)};`;
|
|
92
|
+
case "drop-view": return `-- WARNING: down migration cannot restore the original view definition`;
|
|
93
|
+
case "replace-view": return `-- WARNING: down migration cannot restore the original view definition`;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
@@ -219,3 +219,16 @@ function quoteIndexQualified(index: string, schema: string | undefined): string
|
|
|
219
219
|
if (!schema || schema === DEFAULT_DB_SCHEMA_POSTGRES) return quote(index);
|
|
220
220
|
return quote(schema) + "." + quote(index);
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
/** Same shape as quoteQualified, just for view identifiers (kept separate for readability). */
|
|
224
|
+
function quoteQualifiedView(view: string, schema: string | undefined): string {
|
|
225
|
+
return quoteQualified(view, schema);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderCreateView(v: ViewDescriptor, schema: string | undefined, orReplace: boolean): string {
|
|
229
|
+
if (v.sql === undefined || v.sql.trim().length === 0) {
|
|
230
|
+
throw new Error(`view "${v.name}" has no sql body — buildExpectedSchema must populate it before emit`);
|
|
231
|
+
}
|
|
232
|
+
const prefix = orReplace ? "CREATE OR REPLACE VIEW" : "CREATE VIEW";
|
|
233
|
+
return `${prefix} ${quoteQualifiedView(v.name, schema)} AS\n${v.sql};`;
|
|
234
|
+
}
|
package/src/expected-schema.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { MetaData, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
1
|
+
import type { ColumnNamingStrategy, MetaData, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
2
2
|
import {
|
|
3
3
|
TYPE_OBJECT,
|
|
4
4
|
MetaSource,
|
|
5
5
|
IDENTITY_ATTR_GENERATION,
|
|
6
6
|
IDENTITY_ATTR_UNIQUE,
|
|
7
7
|
FIELD_ATTR_DEFAULT,
|
|
8
|
+
FIELD_ATTR_MAX_LENGTH,
|
|
8
9
|
FIELD_ATTR_UNIQUE,
|
|
9
10
|
FIELD_SUBTYPE_STRING,
|
|
10
11
|
FIELD_SUBTYPE_INT,
|
|
@@ -25,12 +26,14 @@ import {
|
|
|
25
26
|
FIELD_ATTR_STORAGE,
|
|
26
27
|
STORAGE_FLATTENED,
|
|
27
28
|
DOC_ATTR_DESCRIPTION,
|
|
29
|
+
applyColumnNamingStrategy, DEFAULT_COLUMN_NAMING_STRATEGY,
|
|
28
30
|
resolveTableName, resolveColumnName, resolveTableSchema,
|
|
29
31
|
} from "@metaobjectsdev/metadata";
|
|
30
32
|
import type { SqlType } from "./sql-type.js";
|
|
31
33
|
import type {
|
|
32
34
|
Dialect, SchemaSnapshot, TableDescriptor, ColumnDescriptor, IndexDescriptor, FkDescriptor,
|
|
33
35
|
} from "./types.js";
|
|
36
|
+
import { buildExpectedViews } from "./expected-views.js";
|
|
34
37
|
import {
|
|
35
38
|
resolveReferentialActions,
|
|
36
39
|
validateSetNullNullability,
|
|
@@ -49,6 +52,12 @@ export interface BuildExpectedSchemaOptions {
|
|
|
49
52
|
* patterns produce INTEGER / TEXT in the actual DB.
|
|
50
53
|
*/
|
|
51
54
|
dialect?: Dialect;
|
|
55
|
+
/**
|
|
56
|
+
* Column-naming strategy for fields with no `@column` override. Defaults to
|
|
57
|
+
* `"snake_case"`. Must match the runtime's `ObjectManager` strategy — a
|
|
58
|
+
* mismatch yields a schema whose columns the runtime can't address.
|
|
59
|
+
*/
|
|
60
|
+
columnNamingStrategy?: ColumnNamingStrategy;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
export function buildExpectedSchema(
|
|
@@ -58,6 +67,7 @@ export function buildExpectedSchema(
|
|
|
58
67
|
// D1 is SQLite at the SQL level; normalize it so downstream dialect checks
|
|
59
68
|
// don't need to handle "d1" separately.
|
|
60
69
|
const dialect = opts?.dialect === "d1" ? "sqlite" : opts?.dialect;
|
|
70
|
+
const strategy: ColumnNamingStrategy = opts?.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY;
|
|
61
71
|
|
|
62
72
|
// Pass 1: collect entities + their resolved table names.
|
|
63
73
|
// Skip:
|
|
@@ -87,7 +97,7 @@ export function buildExpectedSchema(
|
|
|
87
97
|
// Schema is resolved here (not stored in Pass 1) to avoid exactOptionalPropertyTypes
|
|
88
98
|
// issues with `string | undefined` vs `schema?: string`.
|
|
89
99
|
const tables: TableDescriptor[] = entities.map(({ entity, tableName }) => {
|
|
90
|
-
const t = buildTable(entity, tableName, resolveTargetTable, root as MetaRoot);
|
|
100
|
+
const t = buildTable(entity, tableName, resolveTargetTable, root as MetaRoot, strategy);
|
|
91
101
|
const schema = resolveTableSchema(entity);
|
|
92
102
|
if (schema !== undefined) t.schema = schema;
|
|
93
103
|
return t;
|
|
@@ -113,7 +123,12 @@ export function buildExpectedSchema(
|
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
|
|
126
|
+
// Pass 4: views from read-only projections. Built regardless of dialect so
|
|
127
|
+
// the diff produces correct create-view changes; emit() refuses them for
|
|
128
|
+
// sqlite/d1 with a clear error ("view migration not implemented for ...").
|
|
129
|
+
const views = buildExpectedViews(root as MetaRoot, strategy);
|
|
130
|
+
|
|
131
|
+
return { tables, views };
|
|
117
132
|
}
|
|
118
133
|
|
|
119
134
|
/**
|
|
@@ -143,6 +158,7 @@ function buildTable(
|
|
|
143
158
|
tableName: string,
|
|
144
159
|
resolveTargetTable: (entityName: string) => string | undefined,
|
|
145
160
|
root: MetaRoot,
|
|
161
|
+
strategy: ColumnNamingStrategy,
|
|
146
162
|
): TableDescriptor {
|
|
147
163
|
// Use effective accessors so inherited fields/identities (from `extends:` /
|
|
148
164
|
// abstract bases like BaseEntity) are included.
|
|
@@ -155,7 +171,7 @@ function buildTable(
|
|
|
155
171
|
|
|
156
172
|
const primaryKey = pkJsNames.map((jsName) => {
|
|
157
173
|
const field = findField(entity, jsName);
|
|
158
|
-
return field ? resolveColumnName(field) :
|
|
174
|
+
return field ? resolveColumnName(field, strategy) : applyColumnNamingStrategy(jsName, strategy);
|
|
159
175
|
});
|
|
160
176
|
|
|
161
177
|
const columns: ColumnDescriptor[] = [];
|
|
@@ -167,17 +183,17 @@ function buildTable(
|
|
|
167
183
|
) {
|
|
168
184
|
// Flattened storage: expand nested value-object fields as prefixed columns.
|
|
169
185
|
// The parent field.object itself does NOT produce its own column.
|
|
170
|
-
columns.push(...flattenObjectField(field, root));
|
|
186
|
+
columns.push(...flattenObjectField(field, root, strategy));
|
|
171
187
|
} else {
|
|
172
|
-
columns.push(buildColumn(field, isPk, isPk ? pkGeneration : undefined));
|
|
188
|
+
columns.push(buildColumn(field, isPk, isPk ? pkGeneration : undefined, strategy));
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
191
|
|
|
176
192
|
const descriptor: TableDescriptor = {
|
|
177
193
|
name: tableName,
|
|
178
194
|
columns,
|
|
179
|
-
indexes: buildSecondaryIndexes(entity, tableName),
|
|
180
|
-
foreignKeys: buildForeignKeys(entity, tableName, resolveTargetTable, root),
|
|
195
|
+
indexes: buildSecondaryIndexes(entity, tableName, strategy),
|
|
196
|
+
foreignKeys: buildForeignKeys(entity, tableName, resolveTargetTable, root, strategy),
|
|
181
197
|
primaryKey,
|
|
182
198
|
};
|
|
183
199
|
const entityDesc = readDescription(entity);
|
|
@@ -196,7 +212,9 @@ function readDescription(node: { attr: (n: string) => unknown }): string | undef
|
|
|
196
212
|
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
197
213
|
}
|
|
198
214
|
|
|
199
|
-
function buildSecondaryIndexes(
|
|
215
|
+
function buildSecondaryIndexes(
|
|
216
|
+
entity: MetaObject, tableName: string, strategy: ColumnNamingStrategy,
|
|
217
|
+
): IndexDescriptor[] {
|
|
200
218
|
const indexes: IndexDescriptor[] = [];
|
|
201
219
|
|
|
202
220
|
// (a) Implicit unique indexes from @unique fields. Drizzle auto-creates these
|
|
@@ -205,7 +223,7 @@ function buildSecondaryIndexes(entity: MetaObject, tableName: string): IndexDesc
|
|
|
205
223
|
// doesn't see them as drop-only on the actual side.
|
|
206
224
|
for (const field of entity.fields()) {
|
|
207
225
|
if (field.ownAttr(FIELD_ATTR_UNIQUE) !== true) continue;
|
|
208
|
-
const colName = resolveColumnName(field);
|
|
226
|
+
const colName = resolveColumnName(field, strategy);
|
|
209
227
|
indexes.push({
|
|
210
228
|
name: `${tableName}_${colName}_unique`,
|
|
211
229
|
columns: [colName],
|
|
@@ -221,7 +239,7 @@ function buildSecondaryIndexes(entity: MetaObject, tableName: string): IndexDesc
|
|
|
221
239
|
if (fieldNames.length === 0) continue;
|
|
222
240
|
const cols = fieldNames.map((jsName) => {
|
|
223
241
|
const field = findField(entity, jsName);
|
|
224
|
-
return field ? resolveColumnName(field) :
|
|
242
|
+
return field ? resolveColumnName(field, strategy) : applyColumnNamingStrategy(jsName, strategy);
|
|
225
243
|
});
|
|
226
244
|
const uniqueAttr = identity.ownAttr(IDENTITY_ATTR_UNIQUE);
|
|
227
245
|
indexes.push({
|
|
@@ -238,6 +256,7 @@ function buildForeignKeys(
|
|
|
238
256
|
tableName: string,
|
|
239
257
|
resolveTargetTable: (entityName: string) => string | undefined,
|
|
240
258
|
root: MetaRoot,
|
|
259
|
+
strategy: ColumnNamingStrategy,
|
|
241
260
|
): FkDescriptor[] {
|
|
242
261
|
const fks: FkDescriptor[] = [];
|
|
243
262
|
for (const refChild of entity.referenceIdentities()) {
|
|
@@ -253,7 +272,7 @@ function buildForeignKeys(
|
|
|
253
272
|
|
|
254
273
|
const fkCols = fkFieldJsNames.map((jsName) => {
|
|
255
274
|
const fkField = findField(entity, jsName);
|
|
256
|
-
return fkField ? resolveColumnName(fkField) :
|
|
275
|
+
return fkField ? resolveColumnName(fkField, strategy) : applyColumnNamingStrategy(jsName, strategy);
|
|
257
276
|
});
|
|
258
277
|
|
|
259
278
|
// Target columns: prefer explicit multi-field dotted form, else delegate
|
|
@@ -261,8 +280,8 @@ function buildForeignKeys(
|
|
|
261
280
|
// primary identity → "id" fallback).
|
|
262
281
|
const explicitTargetFields = refChild.targetFields;
|
|
263
282
|
const refColumns = explicitTargetFields.length > 1
|
|
264
|
-
? explicitTargetFields.map(
|
|
265
|
-
: [
|
|
283
|
+
? explicitTargetFields.map((n) => applyColumnNamingStrategy(n, strategy))
|
|
284
|
+
: [applyColumnNamingStrategy(refChild.resolvedTargetPkField(root) ?? "id", strategy)];
|
|
266
285
|
|
|
267
286
|
const { onDelete, onUpdate } = resolveReferentialActions(entity, refChild);
|
|
268
287
|
const constraintName = `${tableName}_${fkCols[0]}_fk`;
|
|
@@ -291,15 +310,17 @@ function buildForeignKeys(
|
|
|
291
310
|
* EF OwnsOne pattern: no JSON column for the parent itself; each nested field
|
|
292
311
|
* becomes `<parent_col>_<nested_col>` in the owning entity's table.
|
|
293
312
|
*/
|
|
294
|
-
function flattenObjectField(
|
|
313
|
+
function flattenObjectField(
|
|
314
|
+
field: MetaData, root: MetaRoot, strategy: ColumnNamingStrategy,
|
|
315
|
+
): ColumnDescriptor[] {
|
|
295
316
|
const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
296
317
|
if (typeof ref !== "string" || ref.length === 0) return [];
|
|
297
318
|
const targetObject = root.findObject(ref);
|
|
298
319
|
if (targetObject === undefined) return [];
|
|
299
|
-
const prefix = resolveColumnName(field) + "_";
|
|
320
|
+
const prefix = resolveColumnName(field, strategy) + "_";
|
|
300
321
|
const cols: ColumnDescriptor[] = [];
|
|
301
322
|
for (const nested of targetObject.fields()) {
|
|
302
|
-
const inner = buildColumn(nested, /* isPk */ false, /* pkGeneration */ undefined);
|
|
323
|
+
const inner = buildColumn(nested, /* isPk */ false, /* pkGeneration */ undefined, strategy);
|
|
303
324
|
cols.push({ ...inner, name: prefix + inner.name });
|
|
304
325
|
}
|
|
305
326
|
return cols;
|
|
@@ -317,14 +338,15 @@ function buildColumn(
|
|
|
317
338
|
field: MetaData,
|
|
318
339
|
isPk: boolean,
|
|
319
340
|
pkGeneration: string | undefined,
|
|
341
|
+
strategy: ColumnNamingStrategy,
|
|
320
342
|
): ColumnDescriptor {
|
|
321
343
|
// Both the @required attr and the validator.required child signal NOT NULL.
|
|
322
344
|
const fieldIsRequired = isRequired(field);
|
|
323
345
|
const defaultRaw = field.ownAttr(FIELD_ATTR_DEFAULT);
|
|
324
346
|
|
|
325
347
|
const col: ColumnDescriptor = {
|
|
326
|
-
name: resolveColumnName(field),
|
|
327
|
-
sqlType: subtypeToSqlType(field
|
|
348
|
+
name: resolveColumnName(field, strategy),
|
|
349
|
+
sqlType: subtypeToSqlType(field),
|
|
328
350
|
nullable: !isPk && !fieldIsRequired,
|
|
329
351
|
};
|
|
330
352
|
|
|
@@ -345,9 +367,14 @@ function buildColumn(
|
|
|
345
367
|
return col;
|
|
346
368
|
}
|
|
347
369
|
|
|
348
|
-
function subtypeToSqlType(
|
|
370
|
+
function subtypeToSqlType(field: MetaData): SqlType {
|
|
371
|
+
const subType = field.subType;
|
|
349
372
|
switch (subType) {
|
|
350
|
-
case FIELD_SUBTYPE_STRING:
|
|
373
|
+
case FIELD_SUBTYPE_STRING: {
|
|
374
|
+
// @maxLength is declared as ATTR_SUBTYPE_INT so the loader coerces it to a number.
|
|
375
|
+
const m = field.ownAttr(FIELD_ATTR_MAX_LENGTH);
|
|
376
|
+
return typeof m === "number" ? { kind: "text", maxLength: m } : { kind: "text" };
|
|
377
|
+
}
|
|
351
378
|
case FIELD_SUBTYPE_INT:
|
|
352
379
|
case FIELD_SUBTYPE_SHORT:
|
|
353
380
|
case FIELD_SUBTYPE_BYTE: return { kind: "integer", bits: 32 };
|
|
@@ -366,9 +393,3 @@ function subtypeToSqlType(subType: string): SqlType {
|
|
|
366
393
|
}
|
|
367
394
|
}
|
|
368
395
|
|
|
369
|
-
function toSnake(s: string): string {
|
|
370
|
-
return s
|
|
371
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
372
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
373
|
-
.toLowerCase();
|
|
374
|
-
}
|