@metaobjectsdev/migrate-ts 0.6.0 → 0.7.0-rc.10
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/introspect/d1.d.ts.map +1 -1
- package/dist/introspect/d1.js +16 -4
- package/dist/introspect/d1.js.map +1 -1
- 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/introspect/d1.ts +14 -4
- package/src/types.ts +11 -4
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
|
-
}
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
|
|
18
|
+
import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
|
|
19
|
+
import {
|
|
20
|
+
type ColumnNamingStrategy,
|
|
21
|
+
MetaPassthroughOrigin,
|
|
22
|
+
MetaAggregateOrigin,
|
|
23
|
+
MetaCollectionOrigin,
|
|
24
|
+
MetaSource,
|
|
25
|
+
SOURCE_ROLE_PRIMARY,
|
|
26
|
+
TYPE_OBJECT,
|
|
27
|
+
TYPE_ORIGIN,
|
|
28
|
+
resolveColumnName,
|
|
29
|
+
resolveTableName,
|
|
30
|
+
stripPackage,
|
|
31
|
+
} from "@metaobjectsdev/metadata";
|
|
32
|
+
import type { ViewDescriptor } from "./types.js";
|
|
33
|
+
|
|
34
|
+
export function buildExpectedViews(root: MetaRoot, strategy: ColumnNamingStrategy): ViewDescriptor[] {
|
|
35
|
+
const out: ViewDescriptor[] = [];
|
|
36
|
+
for (const child of root.ownChildren()) {
|
|
37
|
+
if (child.type !== TYPE_OBJECT) continue;
|
|
38
|
+
const proj = child as MetaObject;
|
|
39
|
+
if (proj.isAbstract) continue;
|
|
40
|
+
if (!isReadOnlyProjection(proj)) continue;
|
|
41
|
+
const view = buildView(proj, root, strategy);
|
|
42
|
+
if (view !== null) out.push(view);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isReadOnlyProjection(entity: MetaObject): boolean {
|
|
48
|
+
const sources = entity.ownChildren().filter((c): c is MetaSource => c instanceof MetaSource);
|
|
49
|
+
if (sources.length === 0) return false;
|
|
50
|
+
const hasReadOnly = sources.some((s) => s.isReadOnly());
|
|
51
|
+
const hasWritable = sources.some((s) => !s.isReadOnly());
|
|
52
|
+
return hasReadOnly && !hasWritable;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildView(
|
|
56
|
+
projection: MetaObject, root: MetaRoot, strategy: ColumnNamingStrategy,
|
|
57
|
+
): ViewDescriptor | null {
|
|
58
|
+
const primarySource = projection.ownChildren().find(
|
|
59
|
+
(c): c is MetaSource => c instanceof MetaSource && c.role === SOURCE_ROLE_PRIMARY,
|
|
60
|
+
);
|
|
61
|
+
if (primarySource?.tableName === undefined) return null;
|
|
62
|
+
const viewName = primarySource.tableName;
|
|
63
|
+
|
|
64
|
+
const cols: string[] = [];
|
|
65
|
+
let baseEntity: string | undefined;
|
|
66
|
+
let blocked: string | undefined;
|
|
67
|
+
const T = "t"; // target alias in correlated subqueries
|
|
68
|
+
|
|
69
|
+
const baseTable = () => {
|
|
70
|
+
if (baseEntity === undefined) return undefined;
|
|
71
|
+
const found = root.findObject(baseEntity);
|
|
72
|
+
return found ? resolveTableName(found) : baseEntity;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
for (const f of projection.fields()) {
|
|
76
|
+
const origin = f.ownChildren().find((c) => c.type === TYPE_ORIGIN);
|
|
77
|
+
const fieldCol = resolveColumnName(f, strategy);
|
|
78
|
+
|
|
79
|
+
if (origin instanceof MetaPassthroughOrigin && origin.via === undefined &&
|
|
80
|
+
origin.from !== undefined && origin.from.includes(".")) {
|
|
81
|
+
const [ent, field] = splitDot(origin.from);
|
|
82
|
+
const bare = stripPackage(ent);
|
|
83
|
+
baseEntity ??= bare;
|
|
84
|
+
if (bare !== baseEntity) { blocked = "passthrough from multiple base entities"; break; }
|
|
85
|
+
const srcEntity = root.findObject(baseEntity);
|
|
86
|
+
const srcCol = resolveColumnByName(srcEntity, field, strategy);
|
|
87
|
+
cols.push(` "${srcCol}" AS "${fieldCol}"`);
|
|
88
|
+
} else if (origin instanceof MetaAggregateOrigin && origin.agg !== undefined &&
|
|
89
|
+
origin.of !== undefined && origin.via !== undefined &&
|
|
90
|
+
origin.of.includes(".") && origin.via.includes(".")) {
|
|
91
|
+
const [baseEnt, relName] = splitDot(origin.via);
|
|
92
|
+
const bareBase = stripPackage(baseEnt);
|
|
93
|
+
baseEntity ??= bareBase;
|
|
94
|
+
if (bareBase !== baseEntity) { blocked = "aggregate over a different base entity"; break; }
|
|
95
|
+
const fk = resolveToManyFk(root, baseEntity, relName, strategy);
|
|
96
|
+
if (fk === null) {
|
|
97
|
+
blocked = `unresolved to-many FK for @via "${origin.via}" (target needs an identity.reference back to ${baseEntity})`;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const ofCol = resolveColumnByName(fk.target, splitDot(origin.of)[1], strategy);
|
|
101
|
+
cols.push(
|
|
102
|
+
` (SELECT ${origin.agg}(${T}."${ofCol}") ` +
|
|
103
|
+
`FROM "${fk.targetTable}" ${T} ` +
|
|
104
|
+
`WHERE ${T}."${fk.fkCol}" = "${baseTable()}"."${fk.parentCol}") AS "${fieldCol}"`,
|
|
105
|
+
);
|
|
106
|
+
} else if (origin instanceof MetaPassthroughOrigin || origin instanceof MetaCollectionOrigin) {
|
|
107
|
+
blocked = `field "${f.name}" uses an origin shape not yet supported by TS migrate-ts (passthrough-via / collection — deferred)`;
|
|
108
|
+
break;
|
|
109
|
+
} else {
|
|
110
|
+
blocked = `field "${f.name}" has no resolvable origin`;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (blocked !== undefined || baseEntity === undefined || cols.length === 0) {
|
|
116
|
+
// Caller decides what to do with a null result. For v1 the runner just
|
|
117
|
+
// omits it from expected.views — diff sees no expected view, no
|
|
118
|
+
// create-view change, and an actual leftover view (if any) gets dropped.
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sql = `SELECT\n${cols.join(",\n")}\nFROM "${baseTable()}"`;
|
|
123
|
+
const view: ViewDescriptor = { name: viewName, sql };
|
|
124
|
+
return view;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function splitDot(s: string): [string, string] {
|
|
128
|
+
const i = s.indexOf(".");
|
|
129
|
+
return [s.slice(0, i), s.slice(i + 1)];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveColumnByName(
|
|
133
|
+
owner: MetaObject | undefined, fieldName: string, strategy: ColumnNamingStrategy,
|
|
134
|
+
): string {
|
|
135
|
+
if (owner === undefined) return fieldName;
|
|
136
|
+
const field = owner.fields().find((f) => f.name === fieldName);
|
|
137
|
+
return field ? resolveColumnName(field, strategy) : fieldName;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface ToManyFk {
|
|
141
|
+
target: MetaObject;
|
|
142
|
+
targetTable: string;
|
|
143
|
+
/** Column on the target entity holding the FK back to the base. */
|
|
144
|
+
fkCol: string;
|
|
145
|
+
/** Column on the base entity the FK references (usually the base's PK). */
|
|
146
|
+
parentCol: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveToManyFk(
|
|
150
|
+
root: MetaRoot, baseEntityName: string, relName: string, strategy: ColumnNamingStrategy,
|
|
151
|
+
): ToManyFk | null {
|
|
152
|
+
const baseObj = root.findObject(baseEntityName);
|
|
153
|
+
if (baseObj === undefined) return null;
|
|
154
|
+
const rel = baseObj.relationships().find((r) => r.name === relName);
|
|
155
|
+
if (rel === undefined || rel.objectRef === undefined) return null;
|
|
156
|
+
const target = root.findObject(stripPackage(rel.objectRef));
|
|
157
|
+
if (target === undefined) return null;
|
|
158
|
+
|
|
159
|
+
const fkRef = target.referenceIdentities().find(
|
|
160
|
+
(r) => r.targetEntity !== undefined && stripPackage(r.targetEntity) === baseEntityName,
|
|
161
|
+
);
|
|
162
|
+
if (fkRef === undefined) return null;
|
|
163
|
+
const fkFields = fkRef.fields;
|
|
164
|
+
if (fkFields.length === 0) return null;
|
|
165
|
+
const fkCol = resolveColumnByName(target, fkFields[0]!, strategy);
|
|
166
|
+
|
|
167
|
+
const parentFieldName = fkRef.targetFields.length > 0
|
|
168
|
+
? fkRef.targetFields[0]!
|
|
169
|
+
: baseObj.primaryIdentity()?.fields[0];
|
|
170
|
+
if (parentFieldName === undefined) return null;
|
|
171
|
+
const parentCol = resolveColumnByName(baseObj, parentFieldName, strategy);
|
|
172
|
+
|
|
173
|
+
return { target, targetTable: resolveTableName(target), fkCol, parentCol };
|
|
174
|
+
}
|
|
175
|
+
|
package/src/introspect/d1.ts
CHANGED
|
@@ -33,10 +33,20 @@ export async function introspectD1(opts: IntrospectD1Options): Promise<SchemaSna
|
|
|
33
33
|
return parseEnvelope(stdout);
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// sqlite_version() is blocked by workerd's local D1 sandbox, so we fall back
|
|
37
|
+
// to a known-good static version (Cloudflare D1 ships a recent SQLite). Remote
|
|
38
|
+
// wrangler executions answer the function, so we try once and only fall back
|
|
39
|
+
// on failure. Keep this string ≥ any version-gated downstream feature checks
|
|
40
|
+
// (see emit/sqlite.ts → parseVersion).
|
|
41
|
+
let sqliteVersion = "3.44.0";
|
|
42
|
+
try {
|
|
43
|
+
const versionRows = await exec("SELECT sqlite_version() AS v");
|
|
44
|
+
const v = versionRows[0]?.v;
|
|
45
|
+
if (v !== undefined && v !== null) sqliteVersion = String(v);
|
|
46
|
+
} catch {
|
|
47
|
+
// Fall through to the static default — workerd local sandbox path.
|
|
48
|
+
}
|
|
49
|
+
const meta: SnapshotMeta = { sqliteVersion };
|
|
40
50
|
|
|
41
51
|
const tableRows = await exec(
|
|
42
52
|
"SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '__new_%' ORDER BY name",
|
package/src/types.ts
CHANGED
|
@@ -82,7 +82,14 @@ export interface ViewDescriptor {
|
|
|
82
82
|
name: string;
|
|
83
83
|
/** Same semantics as TableDescriptor.schema. */
|
|
84
84
|
schema?: string;
|
|
85
|
-
|
|
85
|
+
/**
|
|
86
|
+
* View body: everything between `CREATE VIEW <name> AS` and the trailing `;`
|
|
87
|
+
* (the SELECT clause through the FROM/WHERE/GROUP-BY tail). Populated by
|
|
88
|
+
* `buildExpectedSchema` from projection metadata; omitted by introspect
|
|
89
|
+
* (body-level comparison isn't implemented yet — diff matches by name only,
|
|
90
|
+
* so a body change does NOT trigger replace-view today).
|
|
91
|
+
*/
|
|
92
|
+
sql?: string;
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
// ---------------------------------------------------------------------------
|
|
@@ -118,9 +125,9 @@ export type Change =
|
|
|
118
125
|
| { kind: "add-fk"; table: string; schema?: string; fk: FkDescriptor; status: ChangeStatus }
|
|
119
126
|
| { kind: "drop-fk"; table: string; schema?: string; fk: string; status: ChangeStatus }
|
|
120
127
|
// Declared for v0.3, never produced in v0.1:
|
|
121
|
-
| { kind: "create-view"; view: ViewDescriptor; status: ChangeStatus }
|
|
122
|
-
| { kind: "drop-view"; view: string; status: ChangeStatus }
|
|
123
|
-
| { kind: "replace-view"; view: ViewDescriptor; status: ChangeStatus };
|
|
128
|
+
| { kind: "create-view"; view: ViewDescriptor; schema?: string; status: ChangeStatus }
|
|
129
|
+
| { kind: "drop-view"; view: string; schema?: string; status: ChangeStatus }
|
|
130
|
+
| { kind: "replace-view"; view: ViewDescriptor; schema?: string; status: ChangeStatus };
|
|
124
131
|
|
|
125
132
|
export type ChangeKind = Change["kind"];
|
|
126
133
|
|