@metaobjectsdev/cli 1.0.12 → 1.1.0

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.
Files changed (186) hide show
  1. package/README.md +32 -22
  2. package/dist/bin/meta.js +14 -2
  3. package/dist/bin/meta.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts +1 -14
  5. package/dist/src/commands/docs.d.ts.map +1 -1
  6. package/dist/src/commands/docs.js +11 -835
  7. package/dist/src/commands/docs.js.map +1 -1
  8. package/dist/src/commands/eject.d.ts +24 -1
  9. package/dist/src/commands/eject.d.ts.map +1 -1
  10. package/dist/src/commands/eject.js +240 -69
  11. package/dist/src/commands/eject.js.map +1 -1
  12. package/dist/src/commands/gen.d.ts +7 -0
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +91 -21
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.js +2 -2
  17. package/dist/src/commands/generator.js.map +1 -1
  18. package/dist/src/commands/init.d.ts +7 -0
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +71 -36
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts +1 -1
  23. package/dist/src/commands/migrate.d.ts.map +1 -1
  24. package/dist/src/commands/migrate.js +224 -125
  25. package/dist/src/commands/migrate.js.map +1 -1
  26. package/dist/src/commands/types.d.ts.map +1 -1
  27. package/dist/src/commands/types.js +16 -2
  28. package/dist/src/commands/types.js.map +1 -1
  29. package/dist/src/commands/upgrade.d.ts.map +1 -1
  30. package/dist/src/commands/upgrade.js +4 -1
  31. package/dist/src/commands/upgrade.js.map +1 -1
  32. package/dist/src/commands/verify.d.ts.map +1 -1
  33. package/dist/src/commands/verify.js +189 -125
  34. package/dist/src/commands/verify.js.map +1 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +59 -72
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lib/agent-context-staleness.d.ts +8 -1
  39. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  40. package/dist/src/lib/agent-context-staleness.js +20 -5
  41. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  42. package/dist/src/lib/agent-schema.d.ts +9 -0
  43. package/dist/src/lib/agent-schema.d.ts.map +1 -0
  44. package/dist/src/lib/agent-schema.js +61 -0
  45. package/dist/src/lib/agent-schema.js.map +1 -0
  46. package/dist/src/lib/allow.d.ts +6 -0
  47. package/dist/src/lib/allow.d.ts.map +1 -1
  48. package/dist/src/lib/allow.js +49 -5
  49. package/dist/src/lib/allow.js.map +1 -1
  50. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  51. package/dist/src/lib/anti-patterns.js +4 -2
  52. package/dist/src/lib/anti-patterns.js.map +1 -1
  53. package/dist/src/lib/args.d.ts +51 -10
  54. package/dist/src/lib/args.d.ts.map +1 -1
  55. package/dist/src/lib/args.js +44 -18
  56. package/dist/src/lib/args.js.map +1 -1
  57. package/dist/src/lib/catalog-listing.d.ts +21 -1
  58. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  59. package/dist/src/lib/catalog-listing.js +85 -16
  60. package/dist/src/lib/catalog-listing.js.map +1 -1
  61. package/dist/src/lib/catalog.d.ts +30 -0
  62. package/dist/src/lib/catalog.d.ts.map +1 -1
  63. package/dist/src/lib/catalog.js +40 -5
  64. package/dist/src/lib/catalog.js.map +1 -1
  65. package/dist/src/lib/codegen-drift.d.ts +31 -2
  66. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  67. package/dist/src/lib/codegen-drift.js +109 -4
  68. package/dist/src/lib/codegen-drift.js.map +1 -1
  69. package/dist/src/lib/collection-load-options.d.ts +1 -1
  70. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  71. package/dist/src/lib/collection-load-options.js +1 -0
  72. package/dist/src/lib/collection-load-options.js.map +1 -1
  73. package/dist/src/lib/config.d.ts +4 -2
  74. package/dist/src/lib/config.d.ts.map +1 -1
  75. package/dist/src/lib/config.js +4 -2
  76. package/dist/src/lib/config.js.map +1 -1
  77. package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
  78. package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
  79. package/dist/src/lib/decimal-precision-advisory.js +34 -0
  80. package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
  81. package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
  82. package/dist/src/lib/deprecation-lint.js +17 -26
  83. package/dist/src/lib/deprecation-lint.js.map +1 -1
  84. package/dist/src/lib/docs-removed.d.ts +13 -0
  85. package/dist/src/lib/docs-removed.d.ts.map +1 -0
  86. package/dist/src/lib/docs-removed.js +31 -0
  87. package/dist/src/lib/docs-removed.js.map +1 -0
  88. package/dist/src/lib/docs-site-eject.d.ts +13 -0
  89. package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
  90. package/dist/src/lib/docs-site-eject.js +40 -0
  91. package/dist/src/lib/docs-site-eject.js.map +1 -0
  92. package/dist/src/lib/field-lint.d.ts +25 -0
  93. package/dist/src/lib/field-lint.d.ts.map +1 -0
  94. package/dist/src/lib/field-lint.js +102 -0
  95. package/dist/src/lib/field-lint.js.map +1 -0
  96. package/dist/src/lib/fk-index-advisory.d.ts +1 -2
  97. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  98. package/dist/src/lib/fk-index-advisory.js +4 -3
  99. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  100. package/dist/src/lib/format.d.ts +7 -0
  101. package/dist/src/lib/format.d.ts.map +1 -1
  102. package/dist/src/lib/format.js +13 -0
  103. package/dist/src/lib/format.js.map +1 -1
  104. package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
  105. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  106. package/dist/src/lib/gen-migrate-advice.js +3 -4
  107. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  108. package/dist/src/lib/install-set.d.ts +10 -1
  109. package/dist/src/lib/install-set.d.ts.map +1 -1
  110. package/dist/src/lib/install-set.js +20 -2
  111. package/dist/src/lib/install-set.js.map +1 -1
  112. package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
  113. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  114. package/dist/src/lib/load-metaobjects-config.js +53 -75
  115. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  116. package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
  117. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  118. package/dist/src/lib/manifest-ignored-check.js +9 -4
  119. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  120. package/dist/src/lib/output.d.ts +15 -0
  121. package/dist/src/lib/output.d.ts.map +1 -1
  122. package/dist/src/lib/output.js +9 -1
  123. package/dist/src/lib/output.js.map +1 -1
  124. package/dist/src/lib/payload-field-tree.d.ts +4 -5
  125. package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
  126. package/dist/src/lib/payload-field-tree.js +16 -31
  127. package/dist/src/lib/payload-field-tree.js.map +1 -1
  128. package/dist/src/lib/requirement-check.d.ts +7 -0
  129. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  130. package/dist/src/lib/requirement-check.js +14 -7
  131. package/dist/src/lib/requirement-check.js.map +1 -1
  132. package/dist/src/lib/strict-args.d.ts +28 -0
  133. package/dist/src/lib/strict-args.d.ts.map +1 -0
  134. package/dist/src/lib/strict-args.js +49 -0
  135. package/dist/src/lib/strict-args.js.map +1 -0
  136. package/dist/src/lib/template-eject.d.ts +108 -0
  137. package/dist/src/lib/template-eject.d.ts.map +1 -0
  138. package/dist/src/lib/template-eject.js +438 -0
  139. package/dist/src/lib/template-eject.js.map +1 -0
  140. package/dist/src/lib/version.d.ts +9 -0
  141. package/dist/src/lib/version.d.ts.map +1 -1
  142. package/dist/src/lib/version.js +15 -0
  143. package/dist/src/lib/version.js.map +1 -1
  144. package/package.json +10 -10
  145. package/src/commands/docs.ts +11 -952
  146. package/src/commands/eject.ts +318 -73
  147. package/src/commands/gen.ts +94 -22
  148. package/src/commands/generator.ts +2 -2
  149. package/src/commands/init.ts +80 -38
  150. package/src/commands/migrate.ts +248 -117
  151. package/src/commands/types.ts +18 -2
  152. package/src/commands/upgrade.ts +5 -1
  153. package/src/commands/verify.ts +229 -136
  154. package/src/index.ts +59 -72
  155. package/src/lib/agent-context-staleness.ts +20 -5
  156. package/src/lib/agent-schema.ts +74 -0
  157. package/src/lib/allow.ts +57 -6
  158. package/src/lib/anti-patterns.ts +4 -2
  159. package/src/lib/args.ts +83 -28
  160. package/src/lib/catalog-listing.ts +105 -25
  161. package/src/lib/catalog.ts +51 -6
  162. package/src/lib/codegen-drift.ts +138 -3
  163. package/src/lib/collection-load-options.ts +2 -1
  164. package/src/lib/config.ts +5 -3
  165. package/src/lib/decimal-precision-advisory.ts +50 -0
  166. package/src/lib/deprecation-lint.ts +16 -24
  167. package/src/lib/docs-removed.ts +41 -0
  168. package/src/lib/docs-site-eject.ts +48 -0
  169. package/src/lib/field-lint.ts +122 -0
  170. package/src/lib/fk-index-advisory.ts +4 -4
  171. package/src/lib/format.ts +12 -0
  172. package/src/lib/gen-migrate-advice.ts +6 -4
  173. package/src/lib/install-set.ts +20 -1
  174. package/src/lib/load-metaobjects-config.ts +57 -78
  175. package/src/lib/manifest-ignored-check.ts +14 -10
  176. package/src/lib/output.ts +18 -1
  177. package/src/lib/payload-field-tree.ts +18 -35
  178. package/src/lib/requirement-check.ts +21 -6
  179. package/src/lib/strict-args.ts +52 -0
  180. package/src/lib/template-eject.ts +526 -0
  181. package/src/lib/version.ts +18 -0
  182. package/dist/src/lib/docs-drift.d.ts +0 -42
  183. package/dist/src/lib/docs-drift.d.ts.map +0 -1
  184. package/dist/src/lib/docs-drift.js +0 -264
  185. package/dist/src/lib/docs-drift.js.map +0 -1
  186. package/src/lib/docs-drift.ts +0 -312
@@ -4,20 +4,21 @@ import { existsSync } from "node:fs";
4
4
  import { spawn } from "node:child_process";
5
5
  import { parseMigrateArgs } from "../lib/args.js";
6
6
  import { resolveMigrateConfig, MIGRATE_DEFAULT_OUT_DIR } from "../lib/config.js";
7
- import { formatMigrateResult, formatMigrateResultToon } from "../lib/output.js";
7
+ import { formatMigrateResult, formatMigrateResultToon, migrateResultToData } from "../lib/output.js";
8
8
  import { formatMigrateResultJson } from "../lib/output-json.js";
9
- import { toonEncode } from "../lib/format.js";
10
- import { buildKyselyFromUrl, redactUrl } from "../lib/kysely.js";
9
+ import { emitStructured, narrate, toonEncode } from "../lib/format.js";
10
+ import { buildKyselyFromUrl, inferDialect, redactUrl } from "../lib/kysely.js";
11
11
  import { log } from "../lib/log.js";
12
12
  import { loadMemory, resolveCollection, resolveConfigDir } from "@metaobjectsdev/sdk";
13
- import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
13
+ import { loadMemoryOptionsFrom, loadMetaobjectsConfig, RemovedNamingConfigError, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
14
14
  import { collectionLoadOptions } from "../lib/collection-load-options.js";
15
15
  import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
16
- import { isBlockedChangesError, isPrimaryKeyChangeError, isDeclaredRenameError, isDuplicateSqlNameError, buildExpectedSchemaWithProvenance, scopeExpectedSchema, scopedDiffInputs, introspect, diff, collectUnmanagedNames, carryForwardOutOfScope, emit, writeMigration, baselineFromMetadata, planOffline, snapshotPath, readSnapshot, writeSnapshot, qualifiedDbName, renderD1, writeMigrationD1, writeMigrationFlyway, introspectD1, applyPending, rollbackTo, findWranglerConfig, parseWranglerConfig, resolveD1Binding, } from "@metaobjectsdev/migrate-ts";
16
+ import { isBlockedChangesError, isPrimaryKeyChangeError, isDeclaredRenameError, isDuplicateSqlNameError, buildExpectedSchemaWithProvenance, scopeExpectedSchema, scopedDiffInputs, introspect, diff, collectUnmanagedNames, carryForwardOutOfScope, emit, writeMigration, baselineFromMetadata, planOffline, snapshotPath, readSnapshot, writeSnapshot, qualifiedDbName, renderD1, writeMigrationD1, writeMigrationFlyway, introspectD1, withoutFlywayHistory, applyPending, rollbackTo, findWranglerConfig, parseWranglerConfig, resolveD1Binding, postgresViewReplaceProbe, } from "@metaobjectsdev/migrate-ts";
17
17
  import { buildWranglerExecuteArgs, defaultWranglerRunner, } from "../lib/wrangler.js";
18
18
  import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
19
- import { tokensToAllowOptions, blockedEntriesFor, blockedHintLines } from "../lib/allow.js";
19
+ import { tokensToAllowOptions, blockedEntriesFor, blockedHintLines, viewRecreateNote } from "../lib/allow.js";
20
20
  import { reportLoadError } from "../lib/load-error.js";
21
+ import { decimalPrecisionWarnings } from "../lib/decimal-precision-advisory.js";
21
22
  import { scanForReferentialActionConflicts } from "../lib/referential-action-advisory.js";
22
23
  import { describeError } from "../lib/error-text.js";
23
24
  /**
@@ -55,7 +56,7 @@ MIGRATE FLAGS:
55
56
  .metaobjects/config.json.
56
57
  --out-dir <path> Migration directory (default: ./.metaobjects/migrations;
57
58
  flyway: src/main/resources/db/migration)
58
- --slug <name> Required when changes are present (e.g., --slug add-user-shipping)
59
+ --slug <name> Required to WRITE a migration (not for --dry-run) (e.g., --slug add-user-shipping)
59
60
  --allow <csv> Comma-separated destructive-change permissions:
60
61
  drop-column,drop-table,type-change,drop-index,drop-fk,
61
62
  drop-check,drop-view,drop-view-cascade,
@@ -86,7 +87,7 @@ MIGRATE FLAGS:
86
87
  --d1 <binding> D1 binding name from wrangler.toml (only with --dialect d1)
87
88
  --remote Target remote D1 instead of local (only with --dialect d1)
88
89
  --yes Skip the --remote --apply confirmation pause
89
- --dry-run Print SQL to stdout, don't write
90
+ --dry-run Print the SQL, don't write (json/toon: in the document's sql field)
90
91
  --help, -h Print this help
91
92
 
92
93
  EXAMPLES:
@@ -171,12 +172,37 @@ function warnIfLedgerRelocated(cwd, resolvedOutDir) {
171
172
  * piped and CI run, which is most of them.
172
173
  */
173
174
  function logOutOfScope(names, fromDependencies, fmt) {
174
- for (const msg of exclusionNotes("migrate", names, fromDependencies)) {
175
- if (fmt === "text")
176
- log.info(msg);
177
- else
178
- log.warn(msg);
179
- }
175
+ for (const msg of exclusionNotes("migrate", names, fromDependencies))
176
+ narrate(fmt, msg);
177
+ }
178
+ /**
179
+ * End a migrate path that reports in prose. Text format prints the lines on stdout as it
180
+ * always has. A structured format sends the same lines to STDERR and puts exactly ONE
181
+ * document on stdout — `doc` — so `--format json | jq` always has something to parse
182
+ * and never a sentence in front of it (the rule `logOutOfScope` states above).
183
+ */
184
+ function finishMigrate(fmt, lines, doc) {
185
+ for (const line of lines)
186
+ narrate(fmt, line);
187
+ if (fmt !== "text")
188
+ emitStructured(doc, fmt);
189
+ }
190
+ /** The result fields both the offline and the d1 pipeline report when they have none of their own. */
191
+ function migrateResultDefaults(dryRun) {
192
+ return { blocked: [], ambiguous: [], writtenPaths: [], dryRun };
193
+ }
194
+ /** `viewRecreateNote` as the `notes` list a result carries: empty when there is nothing to say. */
195
+ function viewRecreateNotes(changes) {
196
+ const note = viewRecreateNote(changes);
197
+ return note === undefined ? [] : [note];
198
+ }
199
+ /** A result's notes as prose lines, for the paths that report through `finishMigrate`. */
200
+ function narratedNotes(notes) {
201
+ return notes.map((n) => `migrate: ${n}`);
202
+ }
203
+ /** The `-- UP -- / -- DOWN --` preview a dry run prints in text format. */
204
+ function sqlPreview(sql) {
205
+ return `-- UP --\n${sql.up}\n\n-- DOWN --\n${sql.down}`;
180
206
  }
181
207
  /**
182
208
  * Warn about each foreign key whose two sides declare relationships that disagree on its
@@ -195,6 +221,12 @@ function warnReferentialActionConflicts(metadata, collection) {
195
221
  for (const f of findings)
196
222
  log.warn(`migrate: ${f.message}`);
197
223
  }
224
+ /** W4 (ADR-0019 Amendment 1): warn once per wide decimal on a dialect that keeps 15 digits. */
225
+ function warnDecimalPrecision(metadata, collection, dialect) {
226
+ for (const w of decimalPrecisionWarnings(metadata, dialect, { skip: collection.imported })) {
227
+ log.warn(`migrate: ${w}`);
228
+ }
229
+ }
198
230
  function emitStructuredError(error, hint, fmt) {
199
231
  const payload = { error, hint };
200
232
  if (fmt === "json") {
@@ -296,12 +328,15 @@ function nextStepsDetail(dialect) {
296
328
  return `run \`${greenfieldCreateCmd(dialect)}\` to create tables, or \`${adoptExistingDbCmd()}\` to adopt an existing database`;
297
329
  }
298
330
  /** Table count of the target DB, or undefined when it can't be reached/introspected
299
- * (any failure ⇒ unknown ⇒ never block; the caller warns instead of refusing). */
300
- async function countLiveTables(url, dialect) {
331
+ * (any failure ⇒ unknown ⇒ never block; the caller warns instead of refusing). Under the
332
+ * flyway format Flyway's own history table is not counted: a database that holds only that
333
+ * table has none of the project's tables. */
334
+ async function countLiveTables(url, dialect, format) {
301
335
  try {
302
336
  const kysely = await buildKyselyFromUrl(url, dialect);
303
337
  try {
304
- return (await introspect(kysely.db, kysely.dialect)).tables.length;
338
+ const live = await introspect(kysely.db, kysely.dialect);
339
+ return (format === "flyway" ? withoutFlywayHistory(live) : live).tables.length;
305
340
  }
306
341
  finally {
307
342
  await kysely.close();
@@ -432,14 +467,14 @@ wranglerRunner, fmt = "text") {
432
467
  // this run — the same directory `resolveCollection` resolves the metadata
433
468
  // from, found the same way (design §4.6.1: "Per-port generator config is then
434
469
  // read from that same directory"). Everything below is relative to it: the
435
- // `.metaobjects/config.json` operational block, `metaobjects.config.ts`'s
436
- // `columnNamingStrategy`, the migrations `outDir`, `wrangler.toml` discovery.
470
+ // `.metaobjects/config.json` operational block and its `naming` (ADR-0058), the
471
+ // migrations `outDir`, `wrangler.toml` discovery.
437
472
  //
438
473
  // Read from ambient cwd instead, as this did, they DIVERGE the moment the two
439
474
  // differ: run `meta migrate` from a subdirectory of a project whose root
440
- // declares `columnNamingStrategy: "literal"` and the metadata resolves from
441
- // the ancestor while the strategy silently defaults to snake_case — emitting a
442
- // migration that renames every column. Newly reachable, too: before metadata
475
+ // declares literal column naming and the metadata resolves from the ancestor
476
+ // while the rule silently defaults to snake_case — emitting a migration that
477
+ // renames every column. Newly reachable, too: before metadata
443
478
  // sources were resolvable that invocation just failed with "no metaobjects/
444
479
  // found".
445
480
  //
@@ -455,10 +490,8 @@ wranglerRunner, fmt = "text") {
455
490
  // answers a different question from `.metaobjects/config.json` (design §4.6 —
456
491
  // per-port "how is code generated here?" versus port-neutral "where does metadata
457
492
  // come from?"), and in a Maven- or pip-rooted monorepo the two legitimately sit in
458
- // different directories. Reading it from `metaRoot` there silently defaults
459
- // `columnNamingStrategy` for a package that declares one — the exact
460
- // rename-every-column failure the co-location was introduced to prevent, arrived
461
- // at from the other side (#326). Nearest wins, so a subdirectory declaring nothing
493
+ // different directories. Reading it from `metaRoot` there silently drops the
494
+ // providers and libraries a package declares (#326). Nearest wins, so a subdirectory declaring nothing
462
495
  // still walks up to the project root's config; identical to `metaRoot` in every
463
496
  // `meta init` project, where the two files sit together.
464
497
  const genRoot = resolveGenConfigDir(cwd, metaRoot);
@@ -551,16 +584,20 @@ wranglerRunner, fmt = "text") {
551
584
  // every applied migration NEWER than <target> (target retained), in reverse
552
585
  // order, ledger-tracked + advisory-locked. postgres/sqlite only.
553
586
  if (config.rollback !== undefined) {
554
- return await runRollback(config, metaRoot);
587
+ return await runRollback(config, metaRoot, fmt);
555
588
  }
556
589
  // Best-effort load of metaobjects.config.ts to pick up consumer-supplied
557
- // providers. migrate's postgres/sqlite path also reads the config later
558
- // for columnNamingStrategy; we load it once here and reuse below.
590
+ // providers. Column naming is not read here: it is `naming` in
591
+ // `.metaobjects/config.json`, set on the loaded root (ADR-0058).
559
592
  let postgresLoadOptions = {};
560
593
  try {
561
594
  postgresLoadOptions = loadMemoryOptionsFrom(await loadMetaobjectsConfig(genRoot));
562
595
  }
563
- catch {
596
+ catch (e) {
597
+ if (e instanceof RemovedNamingConfigError) {
598
+ log.error(e.message);
599
+ return 2;
600
+ }
564
601
  postgresLoadOptions = {};
565
602
  }
566
603
  // Discovery and load are two separate failure modes, kept in separate try blocks
@@ -585,7 +622,7 @@ wranglerRunner, fmt = "text") {
585
622
  }
586
623
  catch (err) {
587
624
  reportLoadError(log, "failed to load metadata", err);
588
- return 2;
625
+ return 1;
589
626
  }
590
627
  warnReferentialActionConflicts(metadata, collection);
591
628
  let kysely;
@@ -596,8 +633,12 @@ wranglerRunner, fmt = "text") {
596
633
  log.error(`migrate: ${describeError(err)}`);
597
634
  return 2;
598
635
  }
636
+ warnDecimalPrecision(metadata, collection, kysely.dialect);
599
637
  let exitCode = 0;
600
638
  let writtenPaths = [];
639
+ /** A dry run's SQL: printed in text format, carried in the document otherwise. */
640
+ let dryRunSql;
641
+ const notes = [];
601
642
  let appliedNames = [];
602
643
  let applyFailed = false;
603
644
  let blocked = [];
@@ -605,25 +646,16 @@ wranglerRunner, fmt = "text") {
605
646
  let hazardWarnings = [];
606
647
  let changeCounts = {};
607
648
  try {
608
- // Column-naming strategy (from metaobjects.config) drives BOTH the table schema
609
- // and projection view DDL — derive it once, up front, so every view path agrees.
610
- let columnNamingStrategy = "snake_case";
611
- try {
612
- const cfg = await loadMetaobjectsConfig(genRoot);
613
- if (cfg.columnNamingStrategy)
614
- columnNamingStrategy = cfg.columnNamingStrategy;
615
- }
616
- catch {
617
- // metaobjects.config.ts absent or invalid — use default snake_case
618
- }
649
+ // Column naming (ADR-0058: `naming` in .metaobjects/config.json, carried by the loaded
650
+ // root) drives BOTH the table schema and projection view DDL — read it once, up front,
651
+ // so every view path agrees. A config.ts still setting the 1.0 key is refused, loudly.
619
652
  // Expected views from the SINGLE view-SQL source (codegen-ts emitViewDdl, via
620
653
  // buildProjectionViews). Threaded into the schema-diff so the diff produces all
621
654
  // view DDL (create/drop/replace + dependency-recreate) and emit() renders it —
622
655
  // there is no separate view-migration emitter.
623
- const expectedViews = buildProjectionViews(metadata, { dialect: kysely.dialect, columnNamingStrategy });
656
+ const expectedViews = buildProjectionViews(metadata, { dialect: kysely.dialect });
624
657
  const built = buildExpectedSchemaWithProvenance(metadata, {
625
658
  dialect: kysely.dialect,
626
- columnNamingStrategy,
627
659
  views: expectedViews,
628
660
  });
629
661
  const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
@@ -639,6 +671,9 @@ wranglerRunner, fmt = "text") {
639
671
  let actual;
640
672
  try {
641
673
  actual = await introspect(kysely.db, kysely.dialect);
674
+ // Flyway owns its history table; diffing it proposes DROP TABLE flyway_schema_history.
675
+ if (config.format === "flyway")
676
+ actual = withoutFlywayHistory(actual);
642
677
  }
643
678
  catch (err) {
644
679
  log.error(`migrate: failed to connect to ${kysely.displayUrl}: ${describeError(err)}`);
@@ -658,6 +693,9 @@ wranglerRunner, fmt = "text") {
658
693
  ...scopedDiffInputs(scoped, collectUnmanagedNames(metadata)),
659
694
  actual,
660
695
  dialect: kysely.dialect,
696
+ // Postgres decides whether a changed view can be replaced in place: the probe
697
+ // creates the new definition as a temporary view in a transaction it rolls back.
698
+ ...(kysely.dialect === "postgres" ? { viewReplaceProbe: postgresViewReplaceProbe(kysely.db) } : {}),
661
699
  allow: tokensToAllowOptions(config.allow),
662
700
  renames: config.renames,
663
701
  // #258 — adopting a live DB whose PRIMARY KEY differs from the metadata identity
@@ -745,6 +783,7 @@ wranglerRunner, fmt = "text") {
745
783
  // no-op — output will say "No schema changes"
746
784
  }
747
785
  else {
786
+ notes.push(...viewRecreateNotes(diffResult.changes));
748
787
  let emitted;
749
788
  try {
750
789
  emitted = emit(diffResult.changes, {
@@ -764,21 +803,26 @@ wranglerRunner, fmt = "text") {
764
803
  }
765
804
  }
766
805
  if (exitCode === 0 && emitted) {
767
- if (config.slug === undefined) {
768
- log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
769
- // The common way to land here is a NEW database (a fresh clone, CI, another
770
- // environment) whose migrations are already committed: the diff against an empty
771
- // database is every table, so it asks to author a new migration. That database
772
- // needs the committed chain replayed, not a new migration.
773
- log.error(` New database, migrations already committed? Apply them instead: ` +
774
- `meta migrate apply-pending --db <url>`);
775
- await kysely.close();
776
- return 2;
777
- }
806
+ notes.push(...(emitted.notices ?? []));
778
807
  if (config.dryRun) {
779
- log.info(`-- UP --\n${emitted.up}\n\n-- DOWN --\n${emitted.down}`);
808
+ dryRunSql = { up: emitted.up, down: emitted.down };
809
+ if (fmt === "text")
810
+ log.info(sqlPreview(dryRunSql));
780
811
  }
781
812
  else {
813
+ // A preview names no migration; only a write needs the slug.
814
+ if (config.slug === undefined) {
815
+ log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
816
+ emitStructuredError("migrate: --slug <name> required when there are changes", "re-run with --slug <name>; for a new database whose migrations are already committed, run `meta migrate apply-pending --db <url>`", fmt);
817
+ // The common way to land here is a NEW database (a fresh clone, CI, another
818
+ // environment) whose migrations are already committed: the diff against an empty
819
+ // database is every table, so it asks to author a new migration. That database
820
+ // needs the committed chain replayed, not a new migration.
821
+ log.error(` New database, migrations already committed? Apply them instead: ` +
822
+ `meta migrate apply-pending --db <url>`);
823
+ await kysely.close();
824
+ return 2;
825
+ }
782
826
  const outDir = resolveFormatOutDir(config, metaRoot);
783
827
  await mkdir(outDir, { recursive: true });
784
828
  // #192 — only the envelope differs; the emitted SQL is identical.
@@ -885,17 +929,21 @@ wranglerRunner, fmt = "text") {
885
929
  applied: appliedNames,
886
930
  applyFailed,
887
931
  warnings: hazardWarnings,
932
+ notes,
933
+ ...(dryRunSql !== undefined ? { sql: dryRunSql } : {}),
888
934
  };
889
935
  const output = fmt === "toon" ? formatMigrateResultToon(migrateResult)
890
936
  : fmt === "json" ? formatMigrateResultJson(migrateResult)
891
937
  : formatMigrateResult(migrateResult, { isTTY: !!process.stdout.isTTY });
892
938
  log.info(output);
893
939
  if (config.apply && exitCode === 0) {
940
+ // The document's summary already says what was applied; this line is narration,
941
+ // so a structured run sends it to stderr rather than after the document.
894
942
  if (appliedNames.length > 0) {
895
- log.info(`migrate: applied ${appliedNames.length} migration(s): ${appliedNames.join(", ")}`);
943
+ narrate(fmt, `migrate: applied ${appliedNames.length} migration(s): ${appliedNames.join(", ")}`);
896
944
  }
897
945
  else {
898
- log.info(`migrate: no pending migrations to apply`);
946
+ narrate(fmt, `migrate: no pending migrations to apply`);
899
947
  }
900
948
  }
901
949
  return exitCode;
@@ -947,6 +995,8 @@ genRoot = metaRoot) {
947
995
  }
948
996
  try {
949
997
  snapshot = await introspect(kysely.db, kysely.dialect);
998
+ if (config.format === "flyway")
999
+ snapshot = withoutFlywayHistory(snapshot);
950
1000
  }
951
1001
  finally {
952
1002
  await kysely.close();
@@ -961,7 +1011,7 @@ genRoot = metaRoot) {
961
1011
  // couldn't reach — we still write the snapshot but WARN: an offline baseline is
962
1012
  // only correct when the database already matches the metadata, and emits no DDL.
963
1013
  const liveTableCount = config.databaseUrl !== undefined
964
- ? await countLiveTables(config.databaseUrl, config.dialect)
1014
+ ? await countLiveTables(config.databaseUrl, config.dialect, config.format)
965
1015
  : undefined;
966
1016
  if (liveTableCount === 0) {
967
1017
  // `liveTableCount === 0` ⇒ databaseUrl was defined (countLiveTables only
@@ -977,18 +1027,19 @@ genRoot = metaRoot) {
977
1027
  `baseline — this emits NO CREATE TABLE. Use it only if your database already matches ` +
978
1028
  `your metadata; for a new/empty database run \`${greenfieldCreateCmd(config.dialect)}\` instead.`);
979
1029
  let metadata;
980
- // Load metaobjects.config.ts ONCE, up front, for BOTH the consumer providers/libraries
981
- // and the columnNamingStrategy — mirroring the DB path (and `meta gen`) so
982
- // offline baseline resolves config-registered custom subtypes too (#157).
1030
+ // Load metaobjects.config.ts ONCE, up front, for the consumer providers/libraries —
1031
+ // mirroring the DB path (and `meta gen`) so offline baseline resolves
1032
+ // config-registered custom subtypes too (#157).
983
1033
  let baselineLoadOptions = {};
984
- let baselineStrategy = "snake_case";
985
1034
  try {
986
1035
  const cfg = await loadMetaobjectsConfig(genRoot);
987
1036
  baselineLoadOptions = loadMemoryOptionsFrom(cfg);
988
- if (cfg.columnNamingStrategy)
989
- baselineStrategy = cfg.columnNamingStrategy;
990
1037
  }
991
- catch {
1038
+ catch (e) {
1039
+ if (e instanceof RemovedNamingConfigError) {
1040
+ log.error(e.message);
1041
+ return 2;
1042
+ }
992
1043
  // config absent — no custom providers, no libraries, default snake_case
993
1044
  }
994
1045
  // `baseline` records a STARTING POINT, so it is deliberately NOT scoped: the
@@ -1009,17 +1060,21 @@ genRoot = metaRoot) {
1009
1060
  }
1010
1061
  catch (err) {
1011
1062
  reportLoadError(log, "migrate baseline: failed to load metadata", err);
1012
- return 2;
1063
+ return 1;
1013
1064
  }
1014
- const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect, columnNamingStrategy: baselineStrategy });
1015
- snapshot = baselineFromMetadata(metadata, config.dialect, baselineStrategy, baselineViews);
1065
+ const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect });
1066
+ snapshot = baselineFromMetadata(metadata, config.dialect, baselineViews);
1016
1067
  }
1017
1068
  if (config.dryRun) {
1018
- log.info(`migrate baseline (dry-run): would write schema snapshot ${path}`);
1069
+ finishMigrate(fmt, [`migrate baseline (dry-run): would write schema snapshot ${path}`], {
1070
+ snapshot: path, written: [], summary: "baseline preview only (nothing written)", help: ["re-run without --dry-run to write the snapshot"],
1071
+ });
1019
1072
  return 0;
1020
1073
  }
1021
1074
  await writeSnapshot(path, snapshot);
1022
- log.info(`migrate: wrote schema snapshot ${path}`);
1075
+ finishMigrate(fmt, [`migrate: wrote schema snapshot ${path}`], {
1076
+ snapshot: path, written: [path], summary: "wrote schema snapshot", help: ["commit the snapshot; later runs diff against it"],
1077
+ });
1023
1078
  return 0;
1024
1079
  }
1025
1080
  /**
@@ -1106,22 +1161,38 @@ export async function runOfflineGenerate(config, metaRoot, fmt = "text",
1106
1161
  /** Directory whose `metaobjects.config.ts` governs — see `migrateCommand`'s
1107
1162
  * `genRoot`. Defaults to `metaRoot`, the co-located case. */
1108
1163
  genRoot = metaRoot) {
1164
+ // `--dialect` is optional wherever a URL is known: the offline path reads the
1165
+ // committed snapshot rather than the database, but `--db` (or DATABASE_URL /
1166
+ // migrate.databaseUrl) still names which dialect that snapshot is for. The help has
1167
+ // always said "auto-detected from URL scheme"; this path used to refuse instead.
1168
+ if (config.dialect === undefined && config.databaseUrl !== undefined) {
1169
+ try {
1170
+ config = { ...config, dialect: inferDialect(config.databaseUrl) };
1171
+ }
1172
+ catch (err) {
1173
+ log.error(`migrate: ${describeError(err)}`);
1174
+ emitStructuredError(`migrate: ${describeError(err)}`, "pass --dialect sqlite|postgres|d1", fmt);
1175
+ return 2;
1176
+ }
1177
+ }
1109
1178
  if (config.dialect === undefined) {
1110
- log.error(`migrate: --dialect required for offline generation (or use --from-db)`);
1179
+ log.error(`migrate: --dialect required for offline generation — pass --dialect sqlite|postgres, or --db <url> to infer it (or use --from-db)`);
1180
+ emitStructuredError("migrate: --dialect required for offline generation", "pass --dialect sqlite|postgres, or --db <url> to infer it from the URL scheme", fmt);
1111
1181
  return 2;
1112
1182
  }
1113
- // Load metaobjects.config.ts ONCE, up front, for BOTH the consumer providers/libraries
1114
- // and the columnNamingStrategy — mirroring the DB path (and `meta gen`) so
1115
- // offline generate resolves config-registered custom subtypes too (#157).
1183
+ // Load metaobjects.config.ts ONCE, up front, for the consumer providers/libraries —
1184
+ // mirroring the DB path (and `meta gen`) so offline generate resolves
1185
+ // config-registered custom subtypes too (#157).
1116
1186
  let offlineLoadOptions = {};
1117
- let offlineStrategy = "snake_case";
1118
1187
  try {
1119
1188
  const cfg = await loadMetaobjectsConfig(genRoot);
1120
1189
  offlineLoadOptions = loadMemoryOptionsFrom(cfg);
1121
- if (cfg.columnNamingStrategy)
1122
- offlineStrategy = cfg.columnNamingStrategy;
1123
1190
  }
1124
- catch {
1191
+ catch (e) {
1192
+ if (e instanceof RemovedNamingConfigError) {
1193
+ log.error(e.message);
1194
+ return 2;
1195
+ }
1125
1196
  // config absent — no custom providers, no libraries, default snake_case
1126
1197
  }
1127
1198
  let metadata;
@@ -1135,14 +1206,14 @@ genRoot = metaRoot) {
1135
1206
  }
1136
1207
  catch (err) {
1137
1208
  reportLoadError(log, "migrate: failed to load metadata", err);
1138
- return 2;
1209
+ return 1;
1139
1210
  }
1140
1211
  warnReferentialActionConflicts(metadata, collection);
1212
+ warnDecimalPrecision(metadata, collection, config.dialect);
1141
1213
  const offlineDialect = config.dialect;
1142
- const offlineViews = buildProjectionViews(metadata, { dialect: offlineDialect, columnNamingStrategy: offlineStrategy });
1214
+ const offlineViews = buildProjectionViews(metadata, { dialect: offlineDialect });
1143
1215
  const scopeRc = refuseScopeMismatch(collection, () => buildExpectedSchemaWithProvenance(metadata, {
1144
1216
  dialect: offlineDialect,
1145
- columnNamingStrategy: offlineStrategy,
1146
1217
  views: offlineViews,
1147
1218
  }).provenance, fmt);
1148
1219
  if (scopeRc !== undefined)
@@ -1169,6 +1240,8 @@ genRoot = metaRoot) {
1169
1240
  emitStructuredError("no schema snapshot", nextStepsDetail(config.dialect), fmt);
1170
1241
  return 2;
1171
1242
  }
1243
+ if (config.format === "flyway")
1244
+ snapshot = withoutFlywayHistory(snapshot);
1172
1245
  const collectedAmbiguous = [];
1173
1246
  const onAmbiguousResolution = mapOnAmbiguous(config.onAmbiguous);
1174
1247
  const offlineScope = collection.inMigrateScope;
@@ -1178,7 +1251,6 @@ genRoot = metaRoot) {
1178
1251
  metadata,
1179
1252
  dialect: config.dialect,
1180
1253
  snapshot,
1181
- columnNamingStrategy: offlineStrategy,
1182
1254
  views: offlineViews,
1183
1255
  // Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
1184
1256
  ...(offlineScope !== undefined ? { inScope: offlineScope } : {}),
@@ -1211,29 +1283,47 @@ genRoot = metaRoot) {
1211
1283
  // for an unscoped run.
1212
1284
  const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
1213
1285
  logOutOfScope(plan.outOfScope, plan.importedOutOfScope ?? [], fmt);
1286
+ const offlineNotes = viewRecreateNotes(diffResult.changes);
1287
+ const offlineResult = (extra) => ({
1288
+ dialect: offlineDialect,
1289
+ displayUrl: "",
1290
+ changeCounts: summarizeChanges(diffResult.changes),
1291
+ ...migrateResultDefaults(config.dryRun),
1292
+ format: config.format,
1293
+ notes: offlineNotes,
1294
+ ...extra,
1295
+ });
1214
1296
  if (diffResult.blocked.length > 0) {
1215
- logBlocked(blockedEntriesFor(diffResult.blocked, diffResult.changes));
1297
+ const blocked = blockedEntriesFor(diffResult.blocked, diffResult.changes);
1298
+ logBlocked(blocked);
1299
+ if (fmt !== "text")
1300
+ emitStructured(migrateResultToData(offlineResult({ blocked })), fmt);
1216
1301
  return 1;
1217
1302
  }
1218
1303
  if (diffResult.changes.length === 0) {
1219
- log.info(`migrate: no changes`);
1304
+ finishMigrate(fmt, [`migrate: no changes`], migrateResultToData(offlineResult({})));
1220
1305
  return 0;
1221
1306
  }
1222
- if (config.slug === undefined) {
1223
- log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
1224
- return 2;
1225
- }
1226
- warnDataHazards(diffResult.hazards);
1307
+ const offlineWarnings = warnDataHazards(diffResult.hazards);
1227
1308
  const emitResult = emit(diffResult.changes, {
1228
1309
  dialect: config.dialect,
1229
1310
  expectedSchema: governedExpected,
1230
1311
  actualSchema: snapshot,
1231
1312
  ...(snapshot.meta ? { actualMeta: snapshot.meta } : {}),
1232
1313
  });
1314
+ // The emitter's own notices (e.g. a view dropped and recreated, its grants carried over).
1315
+ offlineNotes.push(...(emitResult.notices ?? []));
1233
1316
  if (config.dryRun) {
1234
- log.info(`-- UP --\n${emitResult.up}\n\n-- DOWN --\n${emitResult.down}`);
1317
+ const sql = { up: emitResult.up, down: emitResult.down };
1318
+ finishMigrate(fmt, [...narratedNotes(offlineNotes), sqlPreview(sql)], migrateResultToData(offlineResult({ sql, warnings: offlineWarnings })));
1235
1319
  return 0;
1236
1320
  }
1321
+ // A preview (above) names no migration; only a write needs the slug.
1322
+ if (config.slug === undefined) {
1323
+ log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
1324
+ emitStructuredError("migrate: --slug <name> required when there are changes", "re-run with --slug <name>", fmt);
1325
+ return 2;
1326
+ }
1237
1327
  // #192 — the MIGRATION FILES follow the active format's layout, while the
1238
1328
  // snapshot stays in `outDir` (it is metaobjects' own state). With the default
1239
1329
  // out-dir that separates them: snapshot in .metaobjects/migrations/, migrations
@@ -1245,8 +1335,7 @@ genRoot = metaRoot) {
1245
1335
  ? await writeMigrationFlyway({ up: emitResult.up, down: emitResult.down }, { dir: writeDir, slug: config.slug })
1246
1336
  : await writeMigration({ up: emitResult.up, down: emitResult.down }, { dir: writeDir, slug: config.slug });
1247
1337
  await writeSnapshot(path, nextSnapshot);
1248
- log.info(`migrate: wrote ${res.upPath}`);
1249
- log.info(`migrate: wrote ${res.downPath}`);
1338
+ finishMigrate(fmt, [`migrate: wrote ${res.upPath}`, `migrate: wrote ${res.downPath}`, ...narratedNotes(offlineNotes)], migrateResultToData(offlineResult({ writtenPaths: [res.upPath, res.downPath], warnings: offlineWarnings })));
1250
1339
  return 0;
1251
1340
  }
1252
1341
  /**
@@ -1256,7 +1345,7 @@ genRoot = metaRoot) {
1256
1345
  *
1257
1346
  * Pass `--rollback ""` (empty target) is treated as null → roll back everything.
1258
1347
  */
1259
- async function runRollback(config, metaRoot) {
1348
+ async function runRollback(config, metaRoot, fmt) {
1260
1349
  // databaseUrl is guaranteed defined by the caller's guard above.
1261
1350
  const databaseUrl = config.databaseUrl;
1262
1351
  // Rollback is destructive and runs hand-authored down.sql; there is no
@@ -1281,12 +1370,10 @@ async function runRollback(config, metaRoot) {
1281
1370
  const target = config.rollback === "" ? null : (config.rollback ?? null);
1282
1371
  try {
1283
1372
  const result = await rollbackTo(kysely.db, outDir, target, { dialect });
1284
- if (result.rolledBack.length > 0) {
1285
- log.info(`migrate: rolled back ${result.rolledBack.length} migration(s): ${result.rolledBack.join(", ")}`);
1286
- }
1287
- else {
1288
- log.info(`migrate: nothing to roll back${target ? ` newer than '${target}'` : ""}`);
1289
- }
1373
+ const summary = result.rolledBack.length > 0
1374
+ ? `rolled back ${result.rolledBack.length} migration(s): ${result.rolledBack.join(", ")}`
1375
+ : `nothing to roll back${target ? ` newer than '${target}'` : ""}`;
1376
+ finishMigrate(fmt, [`migrate: ${summary}`], { rolledBack: result.rolledBack, summary });
1290
1377
  return 0;
1291
1378
  }
1292
1379
  catch (err) {
@@ -1353,7 +1440,11 @@ async function runD1Migrate(config, metaRoot, cwd, runner, fmt = "text") {
1353
1440
  try {
1354
1441
  d1LoadOptions = loadMemoryOptionsFrom(await loadMetaobjectsConfig(genRoot));
1355
1442
  }
1356
- catch {
1443
+ catch (e) {
1444
+ if (e instanceof RemovedNamingConfigError) {
1445
+ log.error(e.message);
1446
+ return 2;
1447
+ }
1357
1448
  d1LoadOptions = {};
1358
1449
  }
1359
1450
  // Discovery and load are separate failure modes (the `meta gen` pattern);
@@ -1376,21 +1467,13 @@ async function runD1Migrate(config, metaRoot, cwd, runner, fmt = "text") {
1376
1467
  }
1377
1468
  catch (err) {
1378
1469
  reportLoadError(log, "migrate: failed to load metadata", err);
1379
- return 2;
1470
+ return 1;
1380
1471
  }
1381
1472
  warnReferentialActionConflicts(metadata, collection);
1473
+ warnDecimalPrecision(metadata, collection, "d1");
1382
1474
  // 4. Build expected schema + introspect actual.
1383
- let columnNamingStrategy = "snake_case";
1384
- try {
1385
- const cfg = await loadMetaobjectsConfig(genRoot);
1386
- if (cfg.columnNamingStrategy)
1387
- columnNamingStrategy = cfg.columnNamingStrategy;
1388
- }
1389
- catch {
1390
- // metaobjects.config.ts absent or invalid — use default snake_case
1391
- }
1392
- const expectedViews = buildProjectionViews(metadata, { dialect: "d1", columnNamingStrategy });
1393
- const built = buildExpectedSchemaWithProvenance(metadata, { dialect: "d1", columnNamingStrategy, views: expectedViews });
1475
+ const expectedViews = buildProjectionViews(metadata, { dialect: "d1" });
1476
+ const built = buildExpectedSchemaWithProvenance(metadata, { dialect: "d1", views: expectedViews });
1394
1477
  const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
1395
1478
  if (scopeRc !== undefined)
1396
1479
  return scopeRc;
@@ -1454,20 +1537,26 @@ async function runD1Migrate(config, metaRoot, cwd, runner, fmt = "text") {
1454
1537
  throw err;
1455
1538
  }
1456
1539
  const changeCounts = summarizeChanges(diffResult.changes);
1540
+ const d1Notes = viewRecreateNotes(diffResult.changes);
1457
1541
  warnDataHazards(diffResult.hazards);
1458
1542
  // Views are emitted by the one schema-diff path: renderD1 = renderSqlite (which
1459
1543
  // renders view DDL) + the D1 safety pass (applied inside renderD1, stripping the
1460
1544
  // BEGIN/COMMIT + PRAGMA that recreate-and-copy emits). There is no separate
1461
1545
  // view-migration emitter; introspectD1 now reads view bodies so unchanged views
1462
1546
  // produce no change and body changes emit a DROP+CREATE.
1547
+ const d1Result = (extra) => ({
1548
+ dialect: "d1",
1549
+ displayUrl: binding.binding,
1550
+ changeCounts,
1551
+ // No `format` key here on purpose: every d1 output this build ships was produced
1552
+ // without it; adding it would change bytes the tests pin.
1553
+ ...migrateResultDefaults(config.dryRun),
1554
+ ...extra,
1555
+ });
1463
1556
  if (diffResult.changes.length === 0) {
1464
- log.info(`migrate: no schema changes for d1 binding '${binding.binding}'`);
1557
+ finishMigrate(fmt, [`migrate: no schema changes for d1 binding '${binding.binding}'`], migrateResultToData(d1Result({})));
1465
1558
  return 0;
1466
1559
  }
1467
- if (config.slug === undefined) {
1468
- log.error(`migrate: --slug <name> required when there are changes`);
1469
- return 2;
1470
- }
1471
1560
  // 6. Emit (with D1 safety pass) + write Wrangler migration files.
1472
1561
  let emitResult;
1473
1562
  try {
@@ -1480,21 +1569,30 @@ async function runD1Migrate(config, metaRoot, cwd, runner, fmt = "text") {
1480
1569
  }
1481
1570
  throw err;
1482
1571
  }
1572
+ d1Notes.push(...(emitResult.notices ?? []));
1483
1573
  const combinedUp = emitResult.up;
1484
1574
  const combinedDown = emitResult.down;
1485
1575
  // Migration dir resolution — same convention `warnIfLedgerRelocated` was
1486
1576
  // just given above, so the two cannot drift apart.
1487
1577
  const migrationsDir = resolveD1OutDir(config, metaRoot, binding.migrations_dir);
1488
1578
  if (config.dryRun) {
1489
- log.info(`-- UP --\n${combinedUp}\n\n-- DOWN --\n${combinedDown}`);
1579
+ const sql = { up: combinedUp, down: combinedDown };
1580
+ finishMigrate(fmt, [...narratedNotes(d1Notes), sqlPreview(sql)], migrateResultToData(d1Result({ sql, notes: d1Notes })));
1490
1581
  return 0;
1491
1582
  }
1492
- const writeResult = await writeMigrationD1({ up: combinedUp, down: combinedDown }, { dir: migrationsDir, slug: config.slug });
1493
- log.info(`migrate: wrote ${writeResult.upPath}`);
1494
- log.info(`migrate: wrote ${writeResult.downPath}`);
1495
- for (const [kind, count] of Object.entries(changeCounts)) {
1496
- log.info(` ${kind}: ${count}`);
1583
+ // A preview (above) names no migration; only a write needs the slug.
1584
+ if (config.slug === undefined) {
1585
+ log.error(`migrate: --slug <name> required when there are changes`);
1586
+ emitStructuredError("migrate: --slug <name> required when there are changes", "re-run with --slug <name>", fmt);
1587
+ return 2;
1497
1588
  }
1589
+ const writeResult = await writeMigrationD1({ up: combinedUp, down: combinedDown }, { dir: migrationsDir, slug: config.slug });
1590
+ finishMigrate(fmt, [
1591
+ `migrate: wrote ${writeResult.upPath}`,
1592
+ `migrate: wrote ${writeResult.downPath}`,
1593
+ ...Object.entries(changeCounts).map(([kind, count]) => ` ${kind}: ${count}`),
1594
+ ...narratedNotes(d1Notes),
1595
+ ], migrateResultToData(d1Result({ writtenPaths: [writeResult.upPath, writeResult.downPath], notes: d1Notes })));
1498
1596
  // 7. Optional --apply: run `wrangler d1 migrations apply`.
1499
1597
  if (config.d1.autoApply) {
1500
1598
  return await runWranglerApply(binding.binding, binding.database_name, config.d1.remote, wranglerConfigPath, config.yes);
@@ -1503,7 +1601,8 @@ async function runD1Migrate(config, metaRoot, cwd, runner, fmt = "text") {
1503
1601
  }
1504
1602
  async function runWranglerApply(bindingName, databaseName, remote, wranglerConfigPath, yes) {
1505
1603
  if (remote && !yes) {
1506
- log.info(`Applying to remote D1 '${databaseName}' (binding=${bindingName}) in 2s — Ctrl+C to abort or pass --yes to skip this pause.`);
1604
+ // Progress, not output: stderr, so it never lands in front of a document on stdout.
1605
+ log.warn(`Applying to remote D1 '${databaseName}' (binding=${bindingName}) in 2s — Ctrl+C to abort or pass --yes to skip this pause.`);
1507
1606
  await new Promise((r) => setTimeout(r, 2000));
1508
1607
  }
1509
1608
  const applyArgs = ["d1", "migrations", "apply", bindingName, remote ? "--remote" : "--local"];