@metaobjectsdev/cli 0.11.4 → 0.11.5-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/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +89 -209
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +6 -1
- package/dist/src/commands/verify.js.map +1 -1
- package/package.json +9 -9
- package/src/commands/migrate.ts +80 -213
- package/src/commands/verify.ts +6 -1
- package/dist/src/lib/projection-migrations.d.ts +0 -35
- package/dist/src/lib/projection-migrations.d.ts.map +0 -1
- package/dist/src/lib/projection-migrations.js +0 -110
- package/dist/src/lib/projection-migrations.js.map +0 -1
- package/src/lib/projection-migrations.ts +0 -158
package/src/commands/migrate.ts
CHANGED
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
writeSnapshot,
|
|
23
23
|
BlockedChangesError,
|
|
24
24
|
renderD1,
|
|
25
|
-
applyD1SafetyPass,
|
|
26
25
|
writeMigrationD1,
|
|
27
26
|
introspectD1,
|
|
28
27
|
applyPending,
|
|
@@ -42,10 +41,7 @@ import {
|
|
|
42
41
|
defaultWranglerRunner,
|
|
43
42
|
type WranglerRunner,
|
|
44
43
|
} from "../lib/wrangler.js";
|
|
45
|
-
import {
|
|
46
|
-
computeProjectionMigrations,
|
|
47
|
-
computeProjectionViewDependencies,
|
|
48
|
-
} from "../lib/projection-migrations.js";
|
|
44
|
+
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
49
45
|
import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
50
46
|
|
|
51
47
|
function mapOnAmbiguous(v: "abort" | "rename" | "drop-add"): AmbiguousResolution {
|
|
@@ -196,7 +192,25 @@ export async function migrateCommand(
|
|
|
196
192
|
let changeCounts: Record<string, number> = {};
|
|
197
193
|
|
|
198
194
|
try {
|
|
199
|
-
|
|
195
|
+
// Column-naming strategy (from metaobjects.config) drives BOTH the table schema
|
|
196
|
+
// and projection view DDL — derive it once, up front, so every view path agrees.
|
|
197
|
+
let columnNamingStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
|
|
198
|
+
try {
|
|
199
|
+
const cfg = await loadMetaobjectsConfig(metaRoot);
|
|
200
|
+
if (cfg.columnNamingStrategy) columnNamingStrategy = cfg.columnNamingStrategy;
|
|
201
|
+
} catch {
|
|
202
|
+
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
203
|
+
}
|
|
204
|
+
// Expected views from the SINGLE view-SQL source (codegen-ts emitViewDdl, via
|
|
205
|
+
// buildProjectionViews). Threaded into the schema-diff so the diff produces all
|
|
206
|
+
// view DDL (create/drop/replace + dependency-recreate) and emit() renders it —
|
|
207
|
+
// there is no separate view-migration emitter.
|
|
208
|
+
const expectedViews = buildProjectionViews(metadata, { dialect: kysely.dialect, columnNamingStrategy });
|
|
209
|
+
const expected = buildExpectedSchema(metadata, {
|
|
210
|
+
dialect: kysely.dialect,
|
|
211
|
+
columnNamingStrategy,
|
|
212
|
+
views: expectedViews,
|
|
213
|
+
});
|
|
200
214
|
let actual;
|
|
201
215
|
try {
|
|
202
216
|
actual = await introspect(kysely.db, kysely.dialect);
|
|
@@ -244,109 +258,32 @@ export async function migrateCommand(
|
|
|
244
258
|
|
|
245
259
|
changeCounts = summarizeChanges(diffResult.changes);
|
|
246
260
|
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
columnNamingStrategy = forgeConfig.columnNamingStrategy;
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Pull existing view CREATE SQL from the DB so unchanged views can be
|
|
261
|
-
// skipped (no DROP+CREATE noise when the body hasn't changed).
|
|
262
|
-
// kysely.dialect is always "sqlite" | "postgres" here — d1 exits above.
|
|
263
|
-
const existingViewSql = await readExistingViewSql(kysely.db, kysely.dialect as "sqlite" | "postgres");
|
|
264
|
-
|
|
265
|
-
// Compute view migrations (projections) independently of table changes.
|
|
266
|
-
const viewResult = computeProjectionMigrations({
|
|
267
|
-
metadata,
|
|
268
|
-
dialect: kysely.dialect,
|
|
269
|
-
allowBreaking: false,
|
|
270
|
-
columnNamingStrategy,
|
|
271
|
-
existingViewSql,
|
|
272
|
-
});
|
|
273
|
-
if (viewResult.errors.length > 0) {
|
|
274
|
-
for (const err of viewResult.errors) log.error(err);
|
|
275
|
-
await kysely.close();
|
|
276
|
-
return 1;
|
|
277
|
-
}
|
|
278
|
-
const viewUpSql = viewResult.migrations.join("\n\n");
|
|
279
|
-
|
|
280
|
-
const hasTableChanges = diffResult.changes.length > 0;
|
|
281
|
-
const hasViewChanges = viewResult.migrations.length > 0;
|
|
282
|
-
|
|
283
|
-
if (!hasTableChanges && !hasViewChanges) {
|
|
261
|
+
// All changes — tables AND views — are emitted by the one schema-diff path.
|
|
262
|
+
// View DDL (create/drop/replace) is produced by diff()'s view passes (2b body
|
|
263
|
+
// comparison, 2c dependency-recreate) and rendered by every dialect's emitter;
|
|
264
|
+
// STAGE_ORDER sequences drop-view before and create-view after any column change
|
|
265
|
+
// a view reads. There is no separate view-migration emitter, and unchanged views
|
|
266
|
+
// produce no change (introspect reads the actual body, diff compares it).
|
|
267
|
+
if (diffResult.changes.length === 0) {
|
|
284
268
|
// no-op — output will say "No schema changes"
|
|
285
269
|
} else {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
} else {
|
|
300
|
-
throw err;
|
|
301
|
-
}
|
|
270
|
+
let emitted: EmitResult | undefined;
|
|
271
|
+
try {
|
|
272
|
+
emitted = emit(diffResult.changes, {
|
|
273
|
+
dialect: kysely.dialect,
|
|
274
|
+
expectedSchema: expected,
|
|
275
|
+
...(actual.meta !== undefined ? { actualMeta: actual.meta } : {}),
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
if (err instanceof BlockedChangesError) {
|
|
279
|
+
blocked = blockedToEntries(err);
|
|
280
|
+
exitCode = 1;
|
|
281
|
+
} else {
|
|
282
|
+
throw err;
|
|
302
283
|
}
|
|
303
284
|
}
|
|
304
285
|
|
|
305
|
-
|
|
306
|
-
if (exitCode === 0) {
|
|
307
|
-
// Extract view names from the CREATE VIEW statements (used by both the
|
|
308
|
-
// pre-drop and down-migration paths below).
|
|
309
|
-
const viewNames = viewResult.migrations
|
|
310
|
-
.map((s) => {
|
|
311
|
-
const m = /CREATE(?:\s+OR\s+REPLACE)?\s+VIEW\s+(\S+)/i.exec(s);
|
|
312
|
-
return m ? m[1] : undefined;
|
|
313
|
-
})
|
|
314
|
-
.filter((n): n is string => Boolean(n));
|
|
315
|
-
|
|
316
|
-
// Pre-drop dependent views, BUT only the ones whose source tables are
|
|
317
|
-
// being recreated. SQLite's ALTER TABLE ... RENAME re-parses dependent
|
|
318
|
-
// view definitions and errors if any of them reference a source table
|
|
319
|
-
// that's mid-recreate (the recreate-and-copy pattern temporarily drops
|
|
320
|
-
// the source table and creates __new_<table>, then RENAMEs). The
|
|
321
|
-
// viewUpSql block below recreates the dropped views fresh.
|
|
322
|
-
const recreatedTables = tableSql?.recreatedTables ?? new Set<string>();
|
|
323
|
-
const viewPreDropSql = recreatedTables.size > 0 && viewNames.length > 0
|
|
324
|
-
? (() => {
|
|
325
|
-
const deps = computeProjectionViewDependencies({
|
|
326
|
-
metadata,
|
|
327
|
-
columnNamingStrategy,
|
|
328
|
-
});
|
|
329
|
-
const affected = viewNames.filter((n) => {
|
|
330
|
-
const sources = deps.get(n);
|
|
331
|
-
if (!sources) return false;
|
|
332
|
-
for (const t of sources) if (recreatedTables.has(t)) return true;
|
|
333
|
-
return false;
|
|
334
|
-
});
|
|
335
|
-
return affected.length > 0
|
|
336
|
-
? affected.map((n) => `DROP VIEW IF EXISTS ${n};`).join("\n")
|
|
337
|
-
: "";
|
|
338
|
-
})()
|
|
339
|
-
: "";
|
|
340
|
-
|
|
341
|
-
const upParts = [viewPreDropSql, tableSql?.up, viewUpSql].filter(Boolean);
|
|
342
|
-
const combinedUp = upParts.join("\n\n");
|
|
343
|
-
// Down SQL: DROP VIEW statements for any views we created.
|
|
344
|
-
const viewDownSql = viewNames
|
|
345
|
-
.map((n) => `DROP VIEW IF EXISTS ${n};`)
|
|
346
|
-
.join("\n");
|
|
347
|
-
const downParts = [viewDownSql, tableSql?.down].filter(Boolean);
|
|
348
|
-
const combinedDown = downParts.join("\n\n");
|
|
349
|
-
|
|
286
|
+
if (exitCode === 0 && emitted) {
|
|
350
287
|
if (config.slug === undefined) {
|
|
351
288
|
log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
|
|
352
289
|
await kysely.close();
|
|
@@ -354,12 +291,12 @@ export async function migrateCommand(
|
|
|
354
291
|
}
|
|
355
292
|
|
|
356
293
|
if (config.dryRun) {
|
|
357
|
-
log.info(`-- UP --\n${
|
|
294
|
+
log.info(`-- UP --\n${emitted.up}\n\n-- DOWN --\n${emitted.down}`);
|
|
358
295
|
} else {
|
|
359
296
|
const outDir = resolvePath(metaRoot, config.outDir);
|
|
360
297
|
await mkdir(outDir, { recursive: true });
|
|
361
298
|
const res = await writeMigration(
|
|
362
|
-
{ up:
|
|
299
|
+
{ up: emitted.up, down: emitted.down },
|
|
363
300
|
{ dir: outDir, slug: config.slug },
|
|
364
301
|
);
|
|
365
302
|
writtenPaths = [res.upPath, res.downPath];
|
|
@@ -462,7 +399,15 @@ export async function runBaseline(
|
|
|
462
399
|
log.error(`migrate baseline: failed to load metadata: ${(err as Error).message}`);
|
|
463
400
|
return 2;
|
|
464
401
|
}
|
|
465
|
-
|
|
402
|
+
let baselineStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
|
|
403
|
+
try {
|
|
404
|
+
const cfg = await loadMetaobjectsConfig(metaRoot);
|
|
405
|
+
if (cfg.columnNamingStrategy) baselineStrategy = cfg.columnNamingStrategy;
|
|
406
|
+
} catch {
|
|
407
|
+
// config absent — default snake_case
|
|
408
|
+
}
|
|
409
|
+
const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect, columnNamingStrategy: baselineStrategy });
|
|
410
|
+
snapshot = baselineFromMetadata(metadata, config.dialect, baselineStrategy, baselineViews);
|
|
466
411
|
}
|
|
467
412
|
|
|
468
413
|
if (config.dryRun) {
|
|
@@ -516,12 +461,23 @@ export async function runOfflineGenerate(
|
|
|
516
461
|
const collectedAmbiguous: AmbiguousChange[] = [];
|
|
517
462
|
const onAmbiguousResolution = mapOnAmbiguous(config.onAmbiguous);
|
|
518
463
|
|
|
464
|
+
let offlineStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
|
|
465
|
+
try {
|
|
466
|
+
const cfg = await loadMetaobjectsConfig(metaRoot);
|
|
467
|
+
if (cfg.columnNamingStrategy) offlineStrategy = cfg.columnNamingStrategy;
|
|
468
|
+
} catch {
|
|
469
|
+
// config absent — default snake_case
|
|
470
|
+
}
|
|
471
|
+
const offlineViews = buildProjectionViews(metadata, { dialect: config.dialect, columnNamingStrategy: offlineStrategy });
|
|
472
|
+
|
|
519
473
|
let plan;
|
|
520
474
|
try {
|
|
521
475
|
plan = await planOffline({
|
|
522
476
|
metadata,
|
|
523
477
|
dialect: config.dialect,
|
|
524
478
|
snapshot,
|
|
479
|
+
columnNamingStrategy: offlineStrategy,
|
|
480
|
+
views: offlineViews,
|
|
525
481
|
allow: tokensToAllowOptions(config.allow),
|
|
526
482
|
onAmbiguous: async (a) => {
|
|
527
483
|
collectedAmbiguous.push(a);
|
|
@@ -694,7 +650,15 @@ async function runD1Migrate(
|
|
|
694
650
|
}
|
|
695
651
|
|
|
696
652
|
// 4. Build expected schema + introspect actual.
|
|
697
|
-
|
|
653
|
+
let columnNamingStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
|
|
654
|
+
try {
|
|
655
|
+
const cfg = await loadMetaobjectsConfig(metaRoot);
|
|
656
|
+
if (cfg.columnNamingStrategy) columnNamingStrategy = cfg.columnNamingStrategy;
|
|
657
|
+
} catch {
|
|
658
|
+
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
659
|
+
}
|
|
660
|
+
const expectedViews = buildProjectionViews(metadata, { dialect: "d1", columnNamingStrategy });
|
|
661
|
+
const expected = buildExpectedSchema(metadata, { dialect: "d1", columnNamingStrategy, views: expectedViews });
|
|
698
662
|
let actual;
|
|
699
663
|
try {
|
|
700
664
|
actual = await introspectD1({
|
|
@@ -736,36 +700,12 @@ async function runD1Migrate(
|
|
|
736
700
|
|
|
737
701
|
const changeCounts = summarizeChanges(diffResult.changes);
|
|
738
702
|
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
columnNamingStrategy = forgeConfig.columnNamingStrategy;
|
|
746
|
-
}
|
|
747
|
-
} catch {
|
|
748
|
-
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// 5b. Compute view migrations (projections). D1 reuses the sqlite emitter.
|
|
752
|
-
// We intentionally skip readExistingViewSql for D1 in v1 — D1 has no
|
|
753
|
-
// introspection path for view bodies, so we accept over-eager DROP+CREATE.
|
|
754
|
-
const viewResult = computeProjectionMigrations({
|
|
755
|
-
metadata,
|
|
756
|
-
dialect: "d1",
|
|
757
|
-
allowBreaking: false,
|
|
758
|
-
columnNamingStrategy,
|
|
759
|
-
});
|
|
760
|
-
if (viewResult.errors.length > 0) {
|
|
761
|
-
for (const err of viewResult.errors) log.error(err);
|
|
762
|
-
return 1;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const hasTableChanges = diffResult.changes.length > 0;
|
|
766
|
-
const hasViewChanges = viewResult.migrations.length > 0;
|
|
767
|
-
|
|
768
|
-
if (!hasTableChanges && !hasViewChanges) {
|
|
703
|
+
// Views are emitted by the one schema-diff path: renderD1 = renderSqlite (which
|
|
704
|
+
// renders view DDL) + the D1 safety pass (applied inside renderD1, stripping the
|
|
705
|
+
// BEGIN/COMMIT + PRAGMA that recreate-and-copy emits). There is no separate
|
|
706
|
+
// view-migration emitter; introspectD1 now reads view bodies so unchanged views
|
|
707
|
+
// produce no change and body changes emit a DROP+CREATE.
|
|
708
|
+
if (diffResult.changes.length === 0) {
|
|
769
709
|
log.info(`migrate: no schema changes for d1 binding '${binding.binding}'`);
|
|
770
710
|
return 0;
|
|
771
711
|
}
|
|
@@ -790,41 +730,8 @@ async function runD1Migrate(
|
|
|
790
730
|
throw err;
|
|
791
731
|
}
|
|
792
732
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const viewNames = viewResult.migrations
|
|
796
|
-
.map((s) => {
|
|
797
|
-
const m = /CREATE(?:\s+OR\s+REPLACE)?\s+VIEW\s+(\S+)/i.exec(s);
|
|
798
|
-
return m ? m[1] : undefined;
|
|
799
|
-
})
|
|
800
|
-
.filter((n): n is string => Boolean(n));
|
|
801
|
-
|
|
802
|
-
// Pre-drop views whose source tables are being recreated (same logic as kysely path).
|
|
803
|
-
const recreatedTables = emitResult.recreatedTables ?? new Set<string>();
|
|
804
|
-
const viewPreDropSql = recreatedTables.size > 0 && viewNames.length > 0
|
|
805
|
-
? (() => {
|
|
806
|
-
const deps = computeProjectionViewDependencies({ metadata, columnNamingStrategy });
|
|
807
|
-
const affected = viewNames.filter((n) => {
|
|
808
|
-
const sources = deps.get(n);
|
|
809
|
-
if (!sources) return false;
|
|
810
|
-
for (const t of sources) if (recreatedTables.has(t)) return true;
|
|
811
|
-
return false;
|
|
812
|
-
});
|
|
813
|
-
return affected.length > 0
|
|
814
|
-
? affected.map((n) => `DROP VIEW IF EXISTS ${n};`).join("\n")
|
|
815
|
-
: "";
|
|
816
|
-
})()
|
|
817
|
-
: "";
|
|
818
|
-
|
|
819
|
-
const viewUpSql = viewResult.migrations.join("\n\n");
|
|
820
|
-
const viewDownSql = viewNames.map((n) => `DROP VIEW IF EXISTS ${n};`).join("\n");
|
|
821
|
-
|
|
822
|
-
// Combine and apply safety pass to the full combined SQL so view DDL
|
|
823
|
-
// (which uses BEGIN/COMMIT from emitSqliteViewMigration) is stripped too.
|
|
824
|
-
const rawUp = [viewPreDropSql, emitResult.up, viewUpSql].filter(Boolean).join("\n\n");
|
|
825
|
-
const rawDown = [viewDownSql, emitResult.down].filter(Boolean).join("\n\n");
|
|
826
|
-
const combinedUp = applyD1SafetyPass(rawUp);
|
|
827
|
-
const combinedDown = applyD1SafetyPass(rawDown);
|
|
733
|
+
const combinedUp = emitResult.up;
|
|
734
|
+
const combinedDown = emitResult.down;
|
|
828
735
|
|
|
829
736
|
// Migration dir resolution: --out-dir > wrangler.toml's migrations_dir > "migrations".
|
|
830
737
|
// The default outDir (./.metaobjects/migrations) is the Kysely-path default; for D1
|
|
@@ -889,43 +796,3 @@ async function runWranglerApply(
|
|
|
889
796
|
child.on("close", (code) => resolve(code ?? 1));
|
|
890
797
|
});
|
|
891
798
|
}
|
|
892
|
-
|
|
893
|
-
/**
|
|
894
|
-
* Read existing view CREATE SQL from the DB. Returns an empty map on any
|
|
895
|
-
* introspection failure — the worst case is over-eager DROP+CREATE
|
|
896
|
-
* (the original behaviour), not data loss.
|
|
897
|
-
*/
|
|
898
|
-
async function readExistingViewSql(
|
|
899
|
-
// biome-ignore lint/suspicious/noExplicitAny: kysely raw query, dialect-dispatched
|
|
900
|
-
db: any,
|
|
901
|
-
dialect: "sqlite" | "postgres",
|
|
902
|
-
): Promise<ReadonlyMap<string, string>> {
|
|
903
|
-
const result = new Map<string, string>();
|
|
904
|
-
try {
|
|
905
|
-
if (dialect === "sqlite") {
|
|
906
|
-
const { sql } = await import("kysely");
|
|
907
|
-
const rows = await sql<{ name: string; sql: string }>`
|
|
908
|
-
SELECT name, sql FROM sqlite_master WHERE type='view' AND name NOT LIKE 'sqlite_%'
|
|
909
|
-
`.execute(db);
|
|
910
|
-
for (const r of rows.rows) {
|
|
911
|
-
if (r.name && r.sql) result.set(r.name, r.sql);
|
|
912
|
-
}
|
|
913
|
-
} else {
|
|
914
|
-
const { sql } = await import("kysely");
|
|
915
|
-
const rows = await sql<{ name: string; def: string }>`
|
|
916
|
-
SELECT viewname AS name, definition AS def
|
|
917
|
-
FROM pg_views
|
|
918
|
-
WHERE schemaname = 'public'
|
|
919
|
-
`.execute(db);
|
|
920
|
-
for (const r of rows.rows) {
|
|
921
|
-
// Postgres pg_views.definition returns only the SELECT body, not the
|
|
922
|
-
// full "CREATE VIEW ... AS ...". Synthesize so comparison is apples-
|
|
923
|
-
// to-apples with what emitViewDdl produces.
|
|
924
|
-
if (r.name && r.def) result.set(r.name, `CREATE VIEW ${r.name} AS ${r.def}`);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
} catch {
|
|
928
|
-
// ignore — empty map means over-eager recreate, same as before this fix
|
|
929
|
-
}
|
|
930
|
-
return result;
|
|
931
|
-
}
|
package/src/commands/verify.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
|
|
|
16
16
|
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
17
17
|
import { computeCodegenDrift } from "../lib/codegen-drift.js";
|
|
18
18
|
import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
19
|
+
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
19
20
|
import { buildKyselyFromUrl, type Dialect } from "../lib/kysely.js";
|
|
20
21
|
import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
21
22
|
import { computeDrift, type Change } from "@metaobjectsdev/migrate-ts";
|
|
@@ -209,9 +210,13 @@ export async function verifyCommand(args: string[], cwd: string): Promise<number
|
|
|
209
210
|
|
|
210
211
|
try {
|
|
211
212
|
const allow = tokensToAllowOptions(flags.allow);
|
|
213
|
+
// Expected views from the single view-SQL source (codegen-ts), so view-body
|
|
214
|
+
// drift is detected against the live DB.
|
|
215
|
+
const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
|
|
216
|
+
const expectedViews = buildProjectionViews(root, { dialect: kysely.dialect, columnNamingStrategy: viewStrategy });
|
|
212
217
|
let driftResult;
|
|
213
218
|
try {
|
|
214
|
-
driftResult = await computeDrift(kysely.db, kysely.dialect, root, { allow });
|
|
219
|
+
driftResult = await computeDrift(kysely.db, kysely.dialect, root, { allow, views: expectedViews });
|
|
215
220
|
} catch (err) {
|
|
216
221
|
log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
|
|
217
222
|
return 1;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { type MetaData } from "@metaobjectsdev/metadata";
|
|
2
|
-
import { type ViewMigrationsResult } from "@metaobjectsdev/migrate-ts";
|
|
3
|
-
import type { Dialect } from "./kysely.js";
|
|
4
|
-
/** view-name → set of source-table names the view's SELECT depends on. */
|
|
5
|
-
export type ProjectionViewDependencies = ReadonlyMap<string, ReadonlySet<string>>;
|
|
6
|
-
export interface ProjectionMigrationsOpts {
|
|
7
|
-
readonly metadata: MetaData;
|
|
8
|
-
readonly dialect: Dialect;
|
|
9
|
-
readonly allowBreaking?: boolean;
|
|
10
|
-
/** Column naming strategy forwarded to extractViewSpec. Defaults to "snake_case". */
|
|
11
|
-
readonly columnNamingStrategy?: "snake_case" | "literal" | "kebab-case";
|
|
12
|
-
/**
|
|
13
|
-
* Existing view SQL keyed by view name (from sqlite_master.sql or
|
|
14
|
-
* pg_views.definition). When provided, projections whose emitted CREATE
|
|
15
|
-
* SQL matches the existing definition (whitespace-normalized) are skipped
|
|
16
|
-
* — no DROP+CREATE noise for unchanged views.
|
|
17
|
-
*/
|
|
18
|
-
readonly existingViewSql?: ReadonlyMap<string, string>;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Walk all projection entities in metadata, extract their ViewSpec, emit CREATE
|
|
22
|
-
* VIEW DDL, and compute view migration SQL via computeViewMigrations.
|
|
23
|
-
*
|
|
24
|
-
* Currently treats every projection as a new view (no previous-shape tracking).
|
|
25
|
-
* Future: introspect existing views from the live DB to do safe-append/replace
|
|
26
|
-
* detection.
|
|
27
|
-
*/
|
|
28
|
-
export declare function computeProjectionMigrations(opts: ProjectionMigrationsOpts): ViewMigrationsResult;
|
|
29
|
-
/**
|
|
30
|
-
* For each projection view, compute the set of source table names that the
|
|
31
|
-
* view's SELECT depends on (base entity + all joined entities). Used by the
|
|
32
|
-
* CLI to pre-drop only the views whose source tables are being recreated.
|
|
33
|
-
*/
|
|
34
|
-
export declare function computeProjectionViewDependencies(opts: Pick<ProjectionMigrationsOpts, "metadata" | "columnNamingStrategy">): ProjectionViewDependencies;
|
|
35
|
-
//# sourceMappingURL=projection-migrations.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"projection-migrations.d.ts","sourceRoot":"","sources":["../../../src/lib/projection-migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,0EAA0E;AAC1E,MAAM,MAAM,0BAA0B,GAAG,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;AAElF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,qFAAqF;IACrF,QAAQ,CAAC,oBAAoB,CAAC,EAAE,YAAY,GAAG,SAAS,GAAG,YAAY,CAAC;IACxE;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxD;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,wBAAwB,GAC7B,oBAAoB,CAsEtB;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,IAAI,CAAC,wBAAwB,EAAE,UAAU,GAAG,sBAAsB,CAAC,GACxE,0BAA0B,CAgC5B"}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { resolveTableName, MetaRoot, } from "@metaobjectsdev/metadata";
|
|
2
|
-
import { isProjection, extractViewSpec, emitViewDdl, } from "@metaobjectsdev/codegen-ts";
|
|
3
|
-
import { computeViewMigrations, viewSqlEquals, } from "@metaobjectsdev/migrate-ts";
|
|
4
|
-
/**
|
|
5
|
-
* Walk all projection entities in metadata, extract their ViewSpec, emit CREATE
|
|
6
|
-
* VIEW DDL, and compute view migration SQL via computeViewMigrations.
|
|
7
|
-
*
|
|
8
|
-
* Currently treats every projection as a new view (no previous-shape tracking).
|
|
9
|
-
* Future: introspect existing views from the live DB to do safe-append/replace
|
|
10
|
-
* detection.
|
|
11
|
-
*/
|
|
12
|
-
export function computeProjectionMigrations(opts) {
|
|
13
|
-
// loadMemory now returns MetaRoot; guard here also covers callers that pass a
|
|
14
|
-
// plain MetaData (e.g. test helpers or external callers with non-MetaRoot roots).
|
|
15
|
-
if (!(opts.metadata instanceof MetaRoot)) {
|
|
16
|
-
throw new Error("computeProjectionMigrations: opts.metadata must be a loaded MetaRoot.");
|
|
17
|
-
}
|
|
18
|
-
// D1 is SQLite at the SQL level; normalize before passing to downstream emitters.
|
|
19
|
-
const dialect = opts.dialect === "d1" ? "sqlite" : opts.dialect;
|
|
20
|
-
const root = opts.metadata;
|
|
21
|
-
const columnNamingStrategy = opts.columnNamingStrategy ?? "snake_case";
|
|
22
|
-
// Collect all writable entities for table name resolution.
|
|
23
|
-
const joinTables = {};
|
|
24
|
-
for (const obj of root.objects()) {
|
|
25
|
-
joinTables[obj.name] = resolveTableName(obj);
|
|
26
|
-
}
|
|
27
|
-
// Find projection entities.
|
|
28
|
-
const projections = root.objects().filter(isProjection);
|
|
29
|
-
if (projections.length === 0) {
|
|
30
|
-
return { migrations: [], errors: [] };
|
|
31
|
-
}
|
|
32
|
-
const views = [];
|
|
33
|
-
for (const projection of projections) {
|
|
34
|
-
const spec = extractViewSpec(projection, root, { columnNamingStrategy });
|
|
35
|
-
const baseTableName = joinTables[spec.joinTree.baseEntity];
|
|
36
|
-
if (!baseTableName) {
|
|
37
|
-
return {
|
|
38
|
-
migrations: [],
|
|
39
|
-
errors: [
|
|
40
|
-
`Projection ${projection.name}: base entity "${spec.joinTree.baseEntity}" has no resolvable table name.`,
|
|
41
|
-
],
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const createSql = emitViewDdl(spec, {
|
|
45
|
-
dialect,
|
|
46
|
-
baseTableName,
|
|
47
|
-
joinTables,
|
|
48
|
-
});
|
|
49
|
-
// Skip if the existing DB view's CREATE SQL matches what we'd emit.
|
|
50
|
-
// Avoids the "every migration re-creates every view" noise when nothing
|
|
51
|
-
// about the view's body actually changed.
|
|
52
|
-
const existing = opts.existingViewSql?.get(spec.viewName);
|
|
53
|
-
if (existing !== undefined && viewSqlEquals(existing, createSql)) {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
views.push({
|
|
57
|
-
viewName: spec.viewName,
|
|
58
|
-
// prevShape intentionally absent — treated as "safe-append" by
|
|
59
|
-
// computeViewMigrations (source-aware-diff.ts line 32). On Postgres
|
|
60
|
-
// this rewrites the emitted "CREATE VIEW" to "CREATE OR REPLACE VIEW",
|
|
61
|
-
// so re-running migrate is idempotent.
|
|
62
|
-
nextShape: {
|
|
63
|
-
columns: spec.selectSpec.columns.map((c) => c.dbColAlias),
|
|
64
|
-
},
|
|
65
|
-
createSql,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return computeViewMigrations({
|
|
69
|
-
dialect,
|
|
70
|
-
allowBreaking: opts.allowBreaking ?? false,
|
|
71
|
-
views,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* For each projection view, compute the set of source table names that the
|
|
76
|
-
* view's SELECT depends on (base entity + all joined entities). Used by the
|
|
77
|
-
* CLI to pre-drop only the views whose source tables are being recreated.
|
|
78
|
-
*/
|
|
79
|
-
export function computeProjectionViewDependencies(opts) {
|
|
80
|
-
if (!(opts.metadata instanceof MetaRoot)) {
|
|
81
|
-
throw new Error("computeProjectionViewDependencies: opts.metadata must be a loaded MetaRoot.");
|
|
82
|
-
}
|
|
83
|
-
const root = opts.metadata;
|
|
84
|
-
const columnNamingStrategy = opts.columnNamingStrategy ?? "snake_case";
|
|
85
|
-
const tableByEntity = {};
|
|
86
|
-
for (const obj of root.objects()) {
|
|
87
|
-
tableByEntity[obj.name] = resolveTableName(obj);
|
|
88
|
-
}
|
|
89
|
-
const result = new Map();
|
|
90
|
-
const projections = root.objects().filter(isProjection);
|
|
91
|
-
for (const projection of projections) {
|
|
92
|
-
const spec = extractViewSpec(projection, root, { columnNamingStrategy });
|
|
93
|
-
const tables = new Set();
|
|
94
|
-
const baseTable = tableByEntity[spec.joinTree.baseEntity];
|
|
95
|
-
if (baseTable)
|
|
96
|
-
tables.add(baseTable);
|
|
97
|
-
const walk = (joins) => {
|
|
98
|
-
for (const j of joins) {
|
|
99
|
-
const t = tableByEntity[j.targetEntity];
|
|
100
|
-
if (t)
|
|
101
|
-
tables.add(t);
|
|
102
|
-
walk(j.children);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
walk(spec.joinTree.joins);
|
|
106
|
-
result.set(spec.viewName, tables);
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
//# sourceMappingURL=projection-migrations.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"projection-migrations.js","sourceRoot":"","sources":["../../../src/lib/projection-migrations.ts"],"names":[],"mappings":"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"}
|