@metaobjectsdev/cli 0.11.3 → 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.
@@ -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
- const expected = buildExpectedSchema(metadata, { dialect: kysely.dialect });
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
- // Load metaobjects config to pick up columnNamingStrategy for view DDL emit.
248
- // If metaobjects.config.ts is absent (e.g. in projects that don't use codegen),
249
- // fall back to snake_case so migrate still works without it.
250
- let columnNamingStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
251
- try {
252
- const forgeConfig = await loadMetaobjectsConfig(metaRoot);
253
- if (forgeConfig.columnNamingStrategy) {
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
- // Emit table SQL (may be empty if only views changed).
287
- let tableSql: EmitResult | undefined;
288
- if (hasTableChanges) {
289
- try {
290
- tableSql = emit(diffResult.changes, {
291
- dialect: kysely.dialect,
292
- expectedSchema: expected,
293
- ...(actual.meta !== undefined ? { actualMeta: actual.meta } : {}),
294
- });
295
- } catch (err) {
296
- if (err instanceof BlockedChangesError) {
297
- blocked = blockedToEntries(err);
298
- exitCode = 1;
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
- // Combine table + view SQL into a single migration if no errors.
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${combinedUp}\n\n-- DOWN --\n${combinedDown}`);
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: combinedUp, down: combinedDown },
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
- snapshot = baselineFromMetadata(metadata, config.dialect);
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
- const expected = buildExpectedSchema(metadata, { dialect: "d1" });
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
- // Load metaobjects config to pick up columnNamingStrategy for view DDL emit.
740
- // Defensive try/catch: if metaobjects.config.ts is absent, fall back to snake_case.
741
- let columnNamingStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
742
- try {
743
- const forgeConfig = await loadMetaobjectsConfig(metaRoot);
744
- if (forgeConfig.columnNamingStrategy) {
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
- // Combine view SQL with table SQL.
794
- // Extract view names from CREATE VIEW statements for pre-drop + down SQL.
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
- }
@@ -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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"}