@metaobjectsdev/cli 1.0.13 → 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.
- package/README.md +32 -22
- package/dist/bin/meta.js +14 -2
- package/dist/bin/meta.js.map +1 -1
- package/dist/src/commands/docs.d.ts +1 -14
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +11 -835
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +24 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +240 -69
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts +7 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +91 -21
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/generator.js +2 -2
- package/dist/src/commands/generator.js.map +1 -1
- package/dist/src/commands/init.d.ts +7 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +71 -36
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +224 -125
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +16 -2
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +4 -1
- package/dist/src/commands/upgrade.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +189 -125
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +59 -72
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts +8 -1
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
- package/dist/src/lib/agent-context-staleness.js +20 -5
- package/dist/src/lib/agent-context-staleness.js.map +1 -1
- package/dist/src/lib/agent-schema.d.ts +9 -0
- package/dist/src/lib/agent-schema.d.ts.map +1 -0
- package/dist/src/lib/agent-schema.js +61 -0
- package/dist/src/lib/agent-schema.js.map +1 -0
- package/dist/src/lib/allow.d.ts +6 -0
- package/dist/src/lib/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +49 -5
- package/dist/src/lib/allow.js.map +1 -1
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +4 -2
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +51 -10
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +44 -18
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +21 -1
- package/dist/src/lib/catalog-listing.d.ts.map +1 -1
- package/dist/src/lib/catalog-listing.js +85 -16
- package/dist/src/lib/catalog-listing.js.map +1 -1
- package/dist/src/lib/catalog.d.ts +30 -0
- package/dist/src/lib/catalog.d.ts.map +1 -1
- package/dist/src/lib/catalog.js +40 -5
- package/dist/src/lib/catalog.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +31 -2
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +109 -4
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/collection-load-options.d.ts +1 -1
- package/dist/src/lib/collection-load-options.d.ts.map +1 -1
- package/dist/src/lib/collection-load-options.js +1 -0
- package/dist/src/lib/collection-load-options.js.map +1 -1
- package/dist/src/lib/config.d.ts +4 -2
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +4 -2
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
- package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
- package/dist/src/lib/decimal-precision-advisory.js +34 -0
- package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
- package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
- package/dist/src/lib/deprecation-lint.js +17 -26
- package/dist/src/lib/deprecation-lint.js.map +1 -1
- package/dist/src/lib/docs-removed.d.ts +13 -0
- package/dist/src/lib/docs-removed.d.ts.map +1 -0
- package/dist/src/lib/docs-removed.js +31 -0
- package/dist/src/lib/docs-removed.js.map +1 -0
- package/dist/src/lib/docs-site-eject.d.ts +13 -0
- package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
- package/dist/src/lib/docs-site-eject.js +40 -0
- package/dist/src/lib/docs-site-eject.js.map +1 -0
- package/dist/src/lib/field-lint.d.ts +25 -0
- package/dist/src/lib/field-lint.d.ts.map +1 -0
- package/dist/src/lib/field-lint.js +102 -0
- package/dist/src/lib/field-lint.js.map +1 -0
- package/dist/src/lib/fk-index-advisory.d.ts +1 -2
- package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
- package/dist/src/lib/fk-index-advisory.js +4 -3
- package/dist/src/lib/fk-index-advisory.js.map +1 -1
- package/dist/src/lib/format.d.ts +7 -0
- package/dist/src/lib/format.d.ts.map +1 -1
- package/dist/src/lib/format.js +13 -0
- package/dist/src/lib/format.js.map +1 -1
- package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
- package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
- package/dist/src/lib/gen-migrate-advice.js +3 -4
- package/dist/src/lib/gen-migrate-advice.js.map +1 -1
- package/dist/src/lib/install-set.d.ts +10 -1
- package/dist/src/lib/install-set.d.ts.map +1 -1
- package/dist/src/lib/install-set.js +20 -2
- package/dist/src/lib/install-set.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +53 -75
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
- package/dist/src/lib/manifest-ignored-check.js +9 -4
- package/dist/src/lib/manifest-ignored-check.js.map +1 -1
- package/dist/src/lib/output.d.ts +15 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +9 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/payload-field-tree.d.ts +4 -5
- package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
- package/dist/src/lib/payload-field-tree.js +16 -31
- package/dist/src/lib/payload-field-tree.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts +7 -0
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +14 -7
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/dist/src/lib/strict-args.d.ts +28 -0
- package/dist/src/lib/strict-args.d.ts.map +1 -0
- package/dist/src/lib/strict-args.js +49 -0
- package/dist/src/lib/strict-args.js.map +1 -0
- package/dist/src/lib/template-eject.d.ts +108 -0
- package/dist/src/lib/template-eject.d.ts.map +1 -0
- package/dist/src/lib/template-eject.js +438 -0
- package/dist/src/lib/template-eject.js.map +1 -0
- package/dist/src/lib/version.d.ts +9 -0
- package/dist/src/lib/version.d.ts.map +1 -1
- package/dist/src/lib/version.js +15 -0
- package/dist/src/lib/version.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/docs.ts +11 -952
- package/src/commands/eject.ts +318 -73
- package/src/commands/gen.ts +94 -22
- package/src/commands/generator.ts +2 -2
- package/src/commands/init.ts +80 -38
- package/src/commands/migrate.ts +248 -117
- package/src/commands/types.ts +18 -2
- package/src/commands/upgrade.ts +5 -1
- package/src/commands/verify.ts +229 -136
- package/src/index.ts +59 -72
- package/src/lib/agent-context-staleness.ts +20 -5
- package/src/lib/agent-schema.ts +74 -0
- package/src/lib/allow.ts +57 -6
- package/src/lib/anti-patterns.ts +4 -2
- package/src/lib/args.ts +83 -28
- package/src/lib/catalog-listing.ts +105 -25
- package/src/lib/catalog.ts +51 -6
- package/src/lib/codegen-drift.ts +138 -3
- package/src/lib/collection-load-options.ts +2 -1
- package/src/lib/config.ts +5 -3
- package/src/lib/decimal-precision-advisory.ts +50 -0
- package/src/lib/deprecation-lint.ts +16 -24
- package/src/lib/docs-removed.ts +41 -0
- package/src/lib/docs-site-eject.ts +48 -0
- package/src/lib/field-lint.ts +122 -0
- package/src/lib/fk-index-advisory.ts +4 -4
- package/src/lib/format.ts +12 -0
- package/src/lib/gen-migrate-advice.ts +6 -4
- package/src/lib/install-set.ts +20 -1
- package/src/lib/load-metaobjects-config.ts +57 -78
- package/src/lib/manifest-ignored-check.ts +14 -10
- package/src/lib/output.ts +18 -1
- package/src/lib/payload-field-tree.ts +18 -35
- package/src/lib/requirement-check.ts +21 -6
- package/src/lib/strict-args.ts +52 -0
- package/src/lib/template-eject.ts +526 -0
- package/src/lib/version.ts +18 -0
- package/dist/src/lib/docs-drift.d.ts +0 -42
- package/dist/src/lib/docs-drift.d.ts.map +0 -1
- package/dist/src/lib/docs-drift.js +0 -264
- package/dist/src/lib/docs-drift.js.map +0 -1
- package/src/lib/docs-drift.ts +0 -312
package/src/commands/migrate.ts
CHANGED
|
@@ -5,14 +5,14 @@ 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
7
|
import type { ResolvedMigrateConfig } from "../lib/config.js";
|
|
8
|
-
import { formatMigrateResult, formatMigrateResultToon, type BlockedEntry, type AmbiguousEntry } from "../lib/output.js";
|
|
8
|
+
import { formatMigrateResult, formatMigrateResultToon, type BlockedEntry, type AmbiguousEntry, type MigrateResultShape, migrateResultToData } from "../lib/output.js";
|
|
9
9
|
import { formatMigrateResultJson } from "../lib/output-json.js";
|
|
10
10
|
import type { OutputFormat } from "../lib/format.js";
|
|
11
|
-
import { toonEncode } from "../lib/format.js";
|
|
12
|
-
import { buildKyselyFromUrl, redactUrl } from "../lib/kysely.js";
|
|
11
|
+
import { emitStructured, narrate, toonEncode } from "../lib/format.js";
|
|
12
|
+
import { buildKyselyFromUrl, inferDialect, redactUrl } from "../lib/kysely.js";
|
|
13
13
|
import { log } from "../lib/log.js";
|
|
14
14
|
import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
|
|
15
|
-
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
15
|
+
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, RemovedNamingConfigError, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
16
16
|
import { collectionLoadOptions } from "../lib/collection-load-options.js";
|
|
17
17
|
import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
|
|
18
18
|
import {
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
writeMigrationD1,
|
|
40
40
|
writeMigrationFlyway,
|
|
41
41
|
introspectD1,
|
|
42
|
+
withoutFlywayHistory,
|
|
42
43
|
applyPending,
|
|
43
44
|
rollbackTo,
|
|
44
45
|
findWranglerConfig,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
type SchemaProvenance,
|
|
55
56
|
type SchemaSnapshot,
|
|
56
57
|
type TableDescriptor,
|
|
58
|
+
postgresViewReplaceProbe,
|
|
57
59
|
} from "@metaobjectsdev/migrate-ts";
|
|
58
60
|
import {
|
|
59
61
|
buildWranglerExecuteArgs,
|
|
@@ -61,8 +63,9 @@ import {
|
|
|
61
63
|
type WranglerRunner,
|
|
62
64
|
} from "../lib/wrangler.js";
|
|
63
65
|
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
64
|
-
import { tokensToAllowOptions, blockedEntriesFor, blockedHintLines } from "../lib/allow.js";
|
|
66
|
+
import { tokensToAllowOptions, blockedEntriesFor, blockedHintLines, viewRecreateNote } from "../lib/allow.js";
|
|
65
67
|
import { reportLoadError } from "../lib/load-error.js";
|
|
68
|
+
import { decimalPrecisionWarnings } from "../lib/decimal-precision-advisory.js";
|
|
66
69
|
import { scanForReferentialActionConflicts } from "../lib/referential-action-advisory.js";
|
|
67
70
|
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
68
71
|
import { describeError } from "../lib/error-text.js";
|
|
@@ -104,7 +107,7 @@ MIGRATE FLAGS:
|
|
|
104
107
|
.metaobjects/config.json.
|
|
105
108
|
--out-dir <path> Migration directory (default: ./.metaobjects/migrations;
|
|
106
109
|
flyway: src/main/resources/db/migration)
|
|
107
|
-
--slug <name> Required
|
|
110
|
+
--slug <name> Required to WRITE a migration (not for --dry-run) (e.g., --slug add-user-shipping)
|
|
108
111
|
--allow <csv> Comma-separated destructive-change permissions:
|
|
109
112
|
drop-column,drop-table,type-change,drop-index,drop-fk,
|
|
110
113
|
drop-check,drop-view,drop-view-cascade,
|
|
@@ -135,7 +138,7 @@ MIGRATE FLAGS:
|
|
|
135
138
|
--d1 <binding> D1 binding name from wrangler.toml (only with --dialect d1)
|
|
136
139
|
--remote Target remote D1 instead of local (only with --dialect d1)
|
|
137
140
|
--yes Skip the --remote --apply confirmation pause
|
|
138
|
-
--dry-run Print SQL
|
|
141
|
+
--dry-run Print the SQL, don't write (json/toon: in the document's sql field)
|
|
139
142
|
--help, -h Print this help
|
|
140
143
|
|
|
141
144
|
EXAMPLES:
|
|
@@ -234,10 +237,39 @@ function logOutOfScope(
|
|
|
234
237
|
fromDependencies: readonly string[],
|
|
235
238
|
fmt: OutputFormat,
|
|
236
239
|
): void {
|
|
237
|
-
for (const msg of exclusionNotes("migrate", names, fromDependencies))
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
for (const msg of exclusionNotes("migrate", names, fromDependencies)) narrate(fmt, msg);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* End a migrate path that reports in prose. Text format prints the lines on stdout as it
|
|
245
|
+
* always has. A structured format sends the same lines to STDERR and puts exactly ONE
|
|
246
|
+
* document on stdout — `doc` — so `--format json | jq` always has something to parse
|
|
247
|
+
* and never a sentence in front of it (the rule `logOutOfScope` states above).
|
|
248
|
+
*/
|
|
249
|
+
function finishMigrate(fmt: OutputFormat, lines: readonly string[], doc: Record<string, unknown>): void {
|
|
250
|
+
for (const line of lines) narrate(fmt, line);
|
|
251
|
+
if (fmt !== "text") emitStructured(doc, fmt);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The result fields both the offline and the d1 pipeline report when they have none of their own. */
|
|
255
|
+
function migrateResultDefaults(dryRun: boolean): Pick<MigrateResultShape, "blocked" | "ambiguous" | "writtenPaths" | "dryRun"> {
|
|
256
|
+
return { blocked: [], ambiguous: [], writtenPaths: [], dryRun };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** `viewRecreateNote` as the `notes` list a result carries: empty when there is nothing to say. */
|
|
260
|
+
function viewRecreateNotes(changes: Change[]): string[] {
|
|
261
|
+
const note = viewRecreateNote(changes);
|
|
262
|
+
return note === undefined ? [] : [note];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A result's notes as prose lines, for the paths that report through `finishMigrate`. */
|
|
266
|
+
function narratedNotes(notes: readonly string[]): string[] {
|
|
267
|
+
return notes.map((n) => `migrate: ${n}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The `-- UP -- / -- DOWN --` preview a dry run prints in text format. */
|
|
271
|
+
function sqlPreview(sql: { up: string; down: string }): string {
|
|
272
|
+
return `-- UP --\n${sql.up}\n\n-- DOWN --\n${sql.down}`;
|
|
241
273
|
}
|
|
242
274
|
|
|
243
275
|
/**
|
|
@@ -256,6 +288,13 @@ function warnReferentialActionConflicts(metadata: MetaData, collection: Collecti
|
|
|
256
288
|
for (const f of findings) log.warn(`migrate: ${f.message}`);
|
|
257
289
|
}
|
|
258
290
|
|
|
291
|
+
/** W4 (ADR-0019 Amendment 1): warn once per wide decimal on a dialect that keeps 15 digits. */
|
|
292
|
+
function warnDecimalPrecision(metadata: MetaData, collection: Collection, dialect: string): void {
|
|
293
|
+
for (const w of decimalPrecisionWarnings(metadata, dialect, { skip: collection.imported })) {
|
|
294
|
+
log.warn(`migrate: ${w}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
259
298
|
function emitStructuredError(error: string, hint: string, fmt: OutputFormat): void {
|
|
260
299
|
const payload = { error, hint };
|
|
261
300
|
if (fmt === "json") {
|
|
@@ -369,15 +408,19 @@ function nextStepsDetail(dialect: string | undefined): string {
|
|
|
369
408
|
}
|
|
370
409
|
|
|
371
410
|
/** Table count of the target DB, or undefined when it can't be reached/introspected
|
|
372
|
-
* (any failure ⇒ unknown ⇒ never block; the caller warns instead of refusing).
|
|
411
|
+
* (any failure ⇒ unknown ⇒ never block; the caller warns instead of refusing). Under the
|
|
412
|
+
* flyway format Flyway's own history table is not counted: a database that holds only that
|
|
413
|
+
* table has none of the project's tables. */
|
|
373
414
|
async function countLiveTables(
|
|
374
415
|
url: string,
|
|
375
416
|
dialect: ResolvedMigrateConfig["dialect"],
|
|
417
|
+
format: ResolvedMigrateConfig["format"],
|
|
376
418
|
): Promise<number | undefined> {
|
|
377
419
|
try {
|
|
378
420
|
const kysely = await buildKyselyFromUrl(url, dialect);
|
|
379
421
|
try {
|
|
380
|
-
|
|
422
|
+
const live = await introspect(kysely.db, kysely.dialect);
|
|
423
|
+
return (format === "flyway" ? withoutFlywayHistory(live) : live).tables.length;
|
|
381
424
|
} finally {
|
|
382
425
|
await kysely.close();
|
|
383
426
|
}
|
|
@@ -509,14 +552,14 @@ export async function migrateCommand(
|
|
|
509
552
|
// this run — the same directory `resolveCollection` resolves the metadata
|
|
510
553
|
// from, found the same way (design §4.6.1: "Per-port generator config is then
|
|
511
554
|
// read from that same directory"). Everything below is relative to it: the
|
|
512
|
-
// `.metaobjects/config.json` operational block
|
|
513
|
-
//
|
|
555
|
+
// `.metaobjects/config.json` operational block and its `naming` (ADR-0058), the
|
|
556
|
+
// migrations `outDir`, `wrangler.toml` discovery.
|
|
514
557
|
//
|
|
515
558
|
// Read from ambient cwd instead, as this did, they DIVERGE the moment the two
|
|
516
559
|
// differ: run `meta migrate` from a subdirectory of a project whose root
|
|
517
|
-
// declares
|
|
518
|
-
//
|
|
519
|
-
//
|
|
560
|
+
// declares literal column naming and the metadata resolves from the ancestor
|
|
561
|
+
// while the rule silently defaults to snake_case — emitting a migration that
|
|
562
|
+
// renames every column. Newly reachable, too: before metadata
|
|
520
563
|
// sources were resolvable that invocation just failed with "no metaobjects/
|
|
521
564
|
// found".
|
|
522
565
|
//
|
|
@@ -532,10 +575,8 @@ export async function migrateCommand(
|
|
|
532
575
|
// answers a different question from `.metaobjects/config.json` (design §4.6 —
|
|
533
576
|
// per-port "how is code generated here?" versus port-neutral "where does metadata
|
|
534
577
|
// come from?"), and in a Maven- or pip-rooted monorepo the two legitimately sit in
|
|
535
|
-
// different directories. Reading it from `metaRoot` there silently
|
|
536
|
-
//
|
|
537
|
-
// rename-every-column failure the co-location was introduced to prevent, arrived
|
|
538
|
-
// at from the other side (#326). Nearest wins, so a subdirectory declaring nothing
|
|
578
|
+
// different directories. Reading it from `metaRoot` there silently drops the
|
|
579
|
+
// providers and libraries a package declares (#326). Nearest wins, so a subdirectory declaring nothing
|
|
539
580
|
// still walks up to the project root's config; identical to `metaRoot` in every
|
|
540
581
|
// `meta init` project, where the two files sit together.
|
|
541
582
|
const genRoot = resolveGenConfigDir(cwd, metaRoot);
|
|
@@ -671,16 +712,20 @@ export async function migrateCommand(
|
|
|
671
712
|
// every applied migration NEWER than <target> (target retained), in reverse
|
|
672
713
|
// order, ledger-tracked + advisory-locked. postgres/sqlite only.
|
|
673
714
|
if (config.rollback !== undefined) {
|
|
674
|
-
return await runRollback(config, metaRoot);
|
|
715
|
+
return await runRollback(config, metaRoot, fmt);
|
|
675
716
|
}
|
|
676
717
|
|
|
677
718
|
// Best-effort load of metaobjects.config.ts to pick up consumer-supplied
|
|
678
|
-
// providers.
|
|
679
|
-
//
|
|
719
|
+
// providers. Column naming is not read here: it is `naming` in
|
|
720
|
+
// `.metaobjects/config.json`, set on the loaded root (ADR-0058).
|
|
680
721
|
let postgresLoadOptions: ReturnType<typeof loadMemoryOptionsFrom> = {};
|
|
681
722
|
try {
|
|
682
723
|
postgresLoadOptions = loadMemoryOptionsFrom(await loadMetaobjectsConfig(genRoot));
|
|
683
|
-
} catch {
|
|
724
|
+
} catch (e) {
|
|
725
|
+
if (e instanceof RemovedNamingConfigError) {
|
|
726
|
+
log.error(e.message);
|
|
727
|
+
return 2;
|
|
728
|
+
}
|
|
684
729
|
postgresLoadOptions = {};
|
|
685
730
|
}
|
|
686
731
|
|
|
@@ -705,7 +750,7 @@ export async function migrateCommand(
|
|
|
705
750
|
});
|
|
706
751
|
} catch (err) {
|
|
707
752
|
reportLoadError(log, "failed to load metadata", err);
|
|
708
|
-
return
|
|
753
|
+
return 1;
|
|
709
754
|
}
|
|
710
755
|
warnReferentialActionConflicts(metadata, collection);
|
|
711
756
|
|
|
@@ -716,9 +761,13 @@ export async function migrateCommand(
|
|
|
716
761
|
log.error(`migrate: ${describeError(err)}`);
|
|
717
762
|
return 2;
|
|
718
763
|
}
|
|
764
|
+
warnDecimalPrecision(metadata, collection, kysely.dialect);
|
|
719
765
|
|
|
720
766
|
let exitCode = 0;
|
|
721
767
|
let writtenPaths: string[] = [];
|
|
768
|
+
/** A dry run's SQL: printed in text format, carried in the document otherwise. */
|
|
769
|
+
let dryRunSql: { up: string; down: string } | undefined;
|
|
770
|
+
const notes: string[] = [];
|
|
722
771
|
let appliedNames: string[] = [];
|
|
723
772
|
let applyFailed = false;
|
|
724
773
|
let blocked: BlockedEntry[] = [];
|
|
@@ -727,23 +776,16 @@ export async function migrateCommand(
|
|
|
727
776
|
let changeCounts: Record<string, number> = {};
|
|
728
777
|
|
|
729
778
|
try {
|
|
730
|
-
// Column-naming
|
|
731
|
-
// and projection view DDL —
|
|
732
|
-
|
|
733
|
-
try {
|
|
734
|
-
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
735
|
-
if (cfg.columnNamingStrategy) columnNamingStrategy = cfg.columnNamingStrategy;
|
|
736
|
-
} catch {
|
|
737
|
-
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
738
|
-
}
|
|
779
|
+
// Column naming (ADR-0058: `naming` in .metaobjects/config.json, carried by the loaded
|
|
780
|
+
// root) drives BOTH the table schema and projection view DDL — read it once, up front,
|
|
781
|
+
// so every view path agrees. A config.ts still setting the 1.0 key is refused, loudly.
|
|
739
782
|
// Expected views from the SINGLE view-SQL source (codegen-ts emitViewDdl, via
|
|
740
783
|
// buildProjectionViews). Threaded into the schema-diff so the diff produces all
|
|
741
784
|
// view DDL (create/drop/replace + dependency-recreate) and emit() renders it —
|
|
742
785
|
// there is no separate view-migration emitter.
|
|
743
|
-
const expectedViews = buildProjectionViews(metadata, { dialect: kysely.dialect
|
|
786
|
+
const expectedViews = buildProjectionViews(metadata, { dialect: kysely.dialect });
|
|
744
787
|
const built = buildExpectedSchemaWithProvenance(metadata, {
|
|
745
788
|
dialect: kysely.dialect,
|
|
746
|
-
columnNamingStrategy,
|
|
747
789
|
views: expectedViews,
|
|
748
790
|
});
|
|
749
791
|
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
@@ -758,6 +800,8 @@ export async function migrateCommand(
|
|
|
758
800
|
let actual;
|
|
759
801
|
try {
|
|
760
802
|
actual = await introspect(kysely.db, kysely.dialect);
|
|
803
|
+
// Flyway owns its history table; diffing it proposes DROP TABLE flyway_schema_history.
|
|
804
|
+
if (config.format === "flyway") actual = withoutFlywayHistory(actual);
|
|
761
805
|
} catch (err) {
|
|
762
806
|
log.error(`migrate: failed to connect to ${kysely.displayUrl}: ${describeError(err)}`);
|
|
763
807
|
await kysely.close();
|
|
@@ -778,6 +822,9 @@ export async function migrateCommand(
|
|
|
778
822
|
...scopedDiffInputs(scoped, collectUnmanagedNames(metadata)),
|
|
779
823
|
actual,
|
|
780
824
|
dialect: kysely.dialect,
|
|
825
|
+
// Postgres decides whether a changed view can be replaced in place: the probe
|
|
826
|
+
// creates the new definition as a temporary view in a transaction it rolls back.
|
|
827
|
+
...(kysely.dialect === "postgres" ? { viewReplaceProbe: postgresViewReplaceProbe(kysely.db) } : {}),
|
|
781
828
|
allow: tokensToAllowOptions(config.allow),
|
|
782
829
|
renames: config.renames,
|
|
783
830
|
// #258 — adopting a live DB whose PRIMARY KEY differs from the metadata identity
|
|
@@ -876,6 +923,7 @@ export async function migrateCommand(
|
|
|
876
923
|
if (diffResult.changes.length === 0) {
|
|
877
924
|
// no-op — output will say "No schema changes"
|
|
878
925
|
} else {
|
|
926
|
+
notes.push(...viewRecreateNotes(diffResult.changes));
|
|
879
927
|
let emitted: EmitResult | undefined;
|
|
880
928
|
try {
|
|
881
929
|
emitted = emit(diffResult.changes, {
|
|
@@ -894,23 +942,30 @@ export async function migrateCommand(
|
|
|
894
942
|
}
|
|
895
943
|
|
|
896
944
|
if (exitCode === 0 && emitted) {
|
|
897
|
-
|
|
898
|
-
log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
|
|
899
|
-
// The common way to land here is a NEW database (a fresh clone, CI, another
|
|
900
|
-
// environment) whose migrations are already committed: the diff against an empty
|
|
901
|
-
// database is every table, so it asks to author a new migration. That database
|
|
902
|
-
// needs the committed chain replayed, not a new migration.
|
|
903
|
-
log.error(
|
|
904
|
-
` New database, migrations already committed? Apply them instead: ` +
|
|
905
|
-
`meta migrate apply-pending --db <url>`,
|
|
906
|
-
);
|
|
907
|
-
await kysely.close();
|
|
908
|
-
return 2;
|
|
909
|
-
}
|
|
910
|
-
|
|
945
|
+
notes.push(...(emitted.notices ?? []));
|
|
911
946
|
if (config.dryRun) {
|
|
912
|
-
|
|
947
|
+
dryRunSql = { up: emitted.up, down: emitted.down };
|
|
948
|
+
if (fmt === "text") log.info(sqlPreview(dryRunSql));
|
|
913
949
|
} else {
|
|
950
|
+
// A preview names no migration; only a write needs the slug.
|
|
951
|
+
if (config.slug === undefined) {
|
|
952
|
+
log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
|
|
953
|
+
emitStructuredError(
|
|
954
|
+
"migrate: --slug <name> required when there are changes",
|
|
955
|
+
"re-run with --slug <name>; for a new database whose migrations are already committed, run `meta migrate apply-pending --db <url>`",
|
|
956
|
+
fmt,
|
|
957
|
+
);
|
|
958
|
+
// The common way to land here is a NEW database (a fresh clone, CI, another
|
|
959
|
+
// environment) whose migrations are already committed: the diff against an empty
|
|
960
|
+
// database is every table, so it asks to author a new migration. That database
|
|
961
|
+
// needs the committed chain replayed, not a new migration.
|
|
962
|
+
log.error(
|
|
963
|
+
` New database, migrations already committed? Apply them instead: ` +
|
|
964
|
+
`meta migrate apply-pending --db <url>`,
|
|
965
|
+
);
|
|
966
|
+
await kysely.close();
|
|
967
|
+
return 2;
|
|
968
|
+
}
|
|
914
969
|
const outDir = resolveFormatOutDir(config, metaRoot);
|
|
915
970
|
await mkdir(outDir, { recursive: true });
|
|
916
971
|
// #192 — only the envelope differs; the emitted SQL is identical.
|
|
@@ -1025,6 +1080,8 @@ export async function migrateCommand(
|
|
|
1025
1080
|
applied: appliedNames,
|
|
1026
1081
|
applyFailed,
|
|
1027
1082
|
warnings: hazardWarnings,
|
|
1083
|
+
notes,
|
|
1084
|
+
...(dryRunSql !== undefined ? { sql: dryRunSql } : {}),
|
|
1028
1085
|
};
|
|
1029
1086
|
const output =
|
|
1030
1087
|
fmt === "toon" ? formatMigrateResultToon(migrateResult)
|
|
@@ -1033,10 +1090,12 @@ export async function migrateCommand(
|
|
|
1033
1090
|
|
|
1034
1091
|
log.info(output);
|
|
1035
1092
|
if (config.apply && exitCode === 0) {
|
|
1093
|
+
// The document's summary already says what was applied; this line is narration,
|
|
1094
|
+
// so a structured run sends it to stderr rather than after the document.
|
|
1036
1095
|
if (appliedNames.length > 0) {
|
|
1037
|
-
|
|
1096
|
+
narrate(fmt, `migrate: applied ${appliedNames.length} migration(s): ${appliedNames.join(", ")}`);
|
|
1038
1097
|
} else {
|
|
1039
|
-
|
|
1098
|
+
narrate(fmt, `migrate: no pending migrations to apply`);
|
|
1040
1099
|
}
|
|
1041
1100
|
}
|
|
1042
1101
|
return exitCode;
|
|
@@ -1090,6 +1149,7 @@ export async function runBaseline(
|
|
|
1090
1149
|
}
|
|
1091
1150
|
try {
|
|
1092
1151
|
snapshot = await introspect(kysely.db, kysely.dialect);
|
|
1152
|
+
if (config.format === "flyway") snapshot = withoutFlywayHistory(snapshot);
|
|
1093
1153
|
} finally {
|
|
1094
1154
|
await kysely.close();
|
|
1095
1155
|
}
|
|
@@ -1102,7 +1162,7 @@ export async function runBaseline(
|
|
|
1102
1162
|
// couldn't reach — we still write the snapshot but WARN: an offline baseline is
|
|
1103
1163
|
// only correct when the database already matches the metadata, and emits no DDL.
|
|
1104
1164
|
const liveTableCount = config.databaseUrl !== undefined
|
|
1105
|
-
? await countLiveTables(config.databaseUrl, config.dialect)
|
|
1165
|
+
? await countLiveTables(config.databaseUrl, config.dialect, config.format)
|
|
1106
1166
|
: undefined;
|
|
1107
1167
|
if (liveTableCount === 0) {
|
|
1108
1168
|
// `liveTableCount === 0` ⇒ databaseUrl was defined (countLiveTables only
|
|
@@ -1122,16 +1182,18 @@ export async function runBaseline(
|
|
|
1122
1182
|
`your metadata; for a new/empty database run \`${greenfieldCreateCmd(config.dialect)}\` instead.`,
|
|
1123
1183
|
);
|
|
1124
1184
|
let metadata;
|
|
1125
|
-
// Load metaobjects.config.ts ONCE, up front, for
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1185
|
+
// Load metaobjects.config.ts ONCE, up front, for the consumer providers/libraries —
|
|
1186
|
+
// mirroring the DB path (and `meta gen`) so offline baseline resolves
|
|
1187
|
+
// config-registered custom subtypes too (#157).
|
|
1128
1188
|
let baselineLoadOptions: ReturnType<typeof loadMemoryOptionsFrom> = {};
|
|
1129
|
-
let baselineStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
|
|
1130
1189
|
try {
|
|
1131
1190
|
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
1132
1191
|
baselineLoadOptions = loadMemoryOptionsFrom(cfg);
|
|
1133
|
-
|
|
1134
|
-
|
|
1192
|
+
} catch (e) {
|
|
1193
|
+
if (e instanceof RemovedNamingConfigError) {
|
|
1194
|
+
log.error(e.message);
|
|
1195
|
+
return 2;
|
|
1196
|
+
}
|
|
1135
1197
|
// config absent — no custom providers, no libraries, default snake_case
|
|
1136
1198
|
}
|
|
1137
1199
|
// `baseline` records a STARTING POINT, so it is deliberately NOT scoped: the
|
|
@@ -1151,19 +1213,23 @@ export async function runBaseline(
|
|
|
1151
1213
|
});
|
|
1152
1214
|
} catch (err) {
|
|
1153
1215
|
reportLoadError(log, "migrate baseline: failed to load metadata", err);
|
|
1154
|
-
return
|
|
1216
|
+
return 1;
|
|
1155
1217
|
}
|
|
1156
|
-
const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect
|
|
1157
|
-
snapshot = baselineFromMetadata(metadata, config.dialect,
|
|
1218
|
+
const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect });
|
|
1219
|
+
snapshot = baselineFromMetadata(metadata, config.dialect, baselineViews);
|
|
1158
1220
|
}
|
|
1159
1221
|
|
|
1160
1222
|
if (config.dryRun) {
|
|
1161
|
-
|
|
1223
|
+
finishMigrate(fmt, [`migrate baseline (dry-run): would write schema snapshot ${path}`], {
|
|
1224
|
+
snapshot: path, written: [], summary: "baseline preview only (nothing written)", help: ["re-run without --dry-run to write the snapshot"],
|
|
1225
|
+
});
|
|
1162
1226
|
return 0;
|
|
1163
1227
|
}
|
|
1164
1228
|
|
|
1165
1229
|
await writeSnapshot(path, snapshot);
|
|
1166
|
-
|
|
1230
|
+
finishMigrate(fmt, [`migrate: wrote schema snapshot ${path}`], {
|
|
1231
|
+
snapshot: path, written: [path], summary: "wrote schema snapshot", help: ["commit the snapshot; later runs diff against it"],
|
|
1232
|
+
});
|
|
1167
1233
|
return 0;
|
|
1168
1234
|
}
|
|
1169
1235
|
|
|
@@ -1263,20 +1329,40 @@ export async function runOfflineGenerate(
|
|
|
1263
1329
|
* `genRoot`. Defaults to `metaRoot`, the co-located case. */
|
|
1264
1330
|
genRoot: string = metaRoot,
|
|
1265
1331
|
): Promise<number> {
|
|
1332
|
+
// `--dialect` is optional wherever a URL is known: the offline path reads the
|
|
1333
|
+
// committed snapshot rather than the database, but `--db` (or DATABASE_URL /
|
|
1334
|
+
// migrate.databaseUrl) still names which dialect that snapshot is for. The help has
|
|
1335
|
+
// always said "auto-detected from URL scheme"; this path used to refuse instead.
|
|
1336
|
+
if (config.dialect === undefined && config.databaseUrl !== undefined) {
|
|
1337
|
+
try {
|
|
1338
|
+
config = { ...config, dialect: inferDialect(config.databaseUrl) };
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
log.error(`migrate: ${describeError(err)}`);
|
|
1341
|
+
emitStructuredError(`migrate: ${describeError(err)}`, "pass --dialect sqlite|postgres|d1", fmt);
|
|
1342
|
+
return 2;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1266
1345
|
if (config.dialect === undefined) {
|
|
1267
|
-
log.error(`migrate: --dialect required for offline generation (or use --from-db)`);
|
|
1346
|
+
log.error(`migrate: --dialect required for offline generation — pass --dialect sqlite|postgres, or --db <url> to infer it (or use --from-db)`);
|
|
1347
|
+
emitStructuredError(
|
|
1348
|
+
"migrate: --dialect required for offline generation",
|
|
1349
|
+
"pass --dialect sqlite|postgres, or --db <url> to infer it from the URL scheme",
|
|
1350
|
+
fmt,
|
|
1351
|
+
);
|
|
1268
1352
|
return 2;
|
|
1269
1353
|
}
|
|
1270
|
-
// Load metaobjects.config.ts ONCE, up front, for
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1354
|
+
// Load metaobjects.config.ts ONCE, up front, for the consumer providers/libraries —
|
|
1355
|
+
// mirroring the DB path (and `meta gen`) so offline generate resolves
|
|
1356
|
+
// config-registered custom subtypes too (#157).
|
|
1273
1357
|
let offlineLoadOptions: ReturnType<typeof loadMemoryOptionsFrom> = {};
|
|
1274
|
-
let offlineStrategy: "snake_case" | "literal" | "kebab-case" = "snake_case";
|
|
1275
1358
|
try {
|
|
1276
1359
|
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
1277
1360
|
offlineLoadOptions = loadMemoryOptionsFrom(cfg);
|
|
1278
|
-
|
|
1279
|
-
|
|
1361
|
+
} catch (e) {
|
|
1362
|
+
if (e instanceof RemovedNamingConfigError) {
|
|
1363
|
+
log.error(e.message);
|
|
1364
|
+
return 2;
|
|
1365
|
+
}
|
|
1280
1366
|
// config absent — no custom providers, no libraries, default snake_case
|
|
1281
1367
|
}
|
|
1282
1368
|
|
|
@@ -1290,17 +1376,17 @@ export async function runOfflineGenerate(
|
|
|
1290
1376
|
});
|
|
1291
1377
|
} catch (err) {
|
|
1292
1378
|
reportLoadError(log, "migrate: failed to load metadata", err);
|
|
1293
|
-
return
|
|
1379
|
+
return 1;
|
|
1294
1380
|
}
|
|
1295
1381
|
warnReferentialActionConflicts(metadata, collection);
|
|
1382
|
+
warnDecimalPrecision(metadata, collection, config.dialect);
|
|
1296
1383
|
|
|
1297
1384
|
const offlineDialect = config.dialect;
|
|
1298
|
-
const offlineViews = buildProjectionViews(metadata, { dialect: offlineDialect
|
|
1385
|
+
const offlineViews = buildProjectionViews(metadata, { dialect: offlineDialect });
|
|
1299
1386
|
const scopeRc = refuseScopeMismatch(
|
|
1300
1387
|
collection,
|
|
1301
1388
|
() => buildExpectedSchemaWithProvenance(metadata, {
|
|
1302
1389
|
dialect: offlineDialect,
|
|
1303
|
-
columnNamingStrategy: offlineStrategy,
|
|
1304
1390
|
views: offlineViews,
|
|
1305
1391
|
}).provenance,
|
|
1306
1392
|
fmt,
|
|
@@ -1330,6 +1416,7 @@ export async function runOfflineGenerate(
|
|
|
1330
1416
|
emitStructuredError("no schema snapshot", nextStepsDetail(config.dialect), fmt);
|
|
1331
1417
|
return 2;
|
|
1332
1418
|
}
|
|
1419
|
+
if (config.format === "flyway") snapshot = withoutFlywayHistory(snapshot);
|
|
1333
1420
|
|
|
1334
1421
|
const collectedAmbiguous: AmbiguousChange[] = [];
|
|
1335
1422
|
const onAmbiguousResolution = mapOnAmbiguous(config.onAmbiguous);
|
|
@@ -1342,7 +1429,6 @@ export async function runOfflineGenerate(
|
|
|
1342
1429
|
metadata,
|
|
1343
1430
|
dialect: config.dialect,
|
|
1344
1431
|
snapshot,
|
|
1345
|
-
columnNamingStrategy: offlineStrategy,
|
|
1346
1432
|
views: offlineViews,
|
|
1347
1433
|
// Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
|
|
1348
1434
|
...(offlineScope !== undefined ? { inScope: offlineScope } : {}),
|
|
@@ -1374,19 +1460,27 @@ export async function runOfflineGenerate(
|
|
|
1374
1460
|
const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
|
|
1375
1461
|
logOutOfScope(plan.outOfScope, plan.importedOutOfScope ?? [], fmt);
|
|
1376
1462
|
|
|
1463
|
+
const offlineNotes = viewRecreateNotes(diffResult.changes);
|
|
1464
|
+
const offlineResult = (extra: Partial<MigrateResultShape>): MigrateResultShape => ({
|
|
1465
|
+
dialect: offlineDialect,
|
|
1466
|
+
displayUrl: "",
|
|
1467
|
+
changeCounts: summarizeChanges(diffResult.changes),
|
|
1468
|
+
...migrateResultDefaults(config.dryRun),
|
|
1469
|
+
format: config.format,
|
|
1470
|
+
notes: offlineNotes,
|
|
1471
|
+
...extra,
|
|
1472
|
+
});
|
|
1377
1473
|
if (diffResult.blocked.length > 0) {
|
|
1378
|
-
|
|
1474
|
+
const blocked = blockedEntriesFor(diffResult.blocked, diffResult.changes);
|
|
1475
|
+
logBlocked(blocked);
|
|
1476
|
+
if (fmt !== "text") emitStructured(migrateResultToData(offlineResult({ blocked })), fmt);
|
|
1379
1477
|
return 1;
|
|
1380
1478
|
}
|
|
1381
1479
|
if (diffResult.changes.length === 0) {
|
|
1382
|
-
|
|
1480
|
+
finishMigrate(fmt, [`migrate: no changes`], migrateResultToData(offlineResult({})));
|
|
1383
1481
|
return 0;
|
|
1384
1482
|
}
|
|
1385
|
-
|
|
1386
|
-
log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
|
|
1387
|
-
return 2;
|
|
1388
|
-
}
|
|
1389
|
-
warnDataHazards(diffResult.hazards);
|
|
1483
|
+
const offlineWarnings = warnDataHazards(diffResult.hazards);
|
|
1390
1484
|
|
|
1391
1485
|
const emitResult = emit(diffResult.changes, {
|
|
1392
1486
|
dialect: config.dialect,
|
|
@@ -1395,11 +1489,26 @@ export async function runOfflineGenerate(
|
|
|
1395
1489
|
...(snapshot.meta ? { actualMeta: snapshot.meta } : {}),
|
|
1396
1490
|
});
|
|
1397
1491
|
|
|
1492
|
+
// The emitter's own notices (e.g. a view dropped and recreated, its grants carried over).
|
|
1493
|
+
offlineNotes.push(...(emitResult.notices ?? []));
|
|
1494
|
+
|
|
1398
1495
|
if (config.dryRun) {
|
|
1399
|
-
|
|
1496
|
+
const sql = { up: emitResult.up, down: emitResult.down };
|
|
1497
|
+
finishMigrate(
|
|
1498
|
+
fmt,
|
|
1499
|
+
[...narratedNotes(offlineNotes), sqlPreview(sql)],
|
|
1500
|
+
migrateResultToData(offlineResult({ sql, warnings: offlineWarnings })),
|
|
1501
|
+
);
|
|
1400
1502
|
return 0;
|
|
1401
1503
|
}
|
|
1402
1504
|
|
|
1505
|
+
// A preview (above) names no migration; only a write needs the slug.
|
|
1506
|
+
if (config.slug === undefined) {
|
|
1507
|
+
log.error(`migrate: --slug <name> required when there are changes (e.g., --slug add-user-shipping)`);
|
|
1508
|
+
emitStructuredError("migrate: --slug <name> required when there are changes", "re-run with --slug <name>", fmt);
|
|
1509
|
+
return 2;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1403
1512
|
// #192 — the MIGRATION FILES follow the active format's layout, while the
|
|
1404
1513
|
// snapshot stays in `outDir` (it is metaobjects' own state). With the default
|
|
1405
1514
|
// out-dir that separates them: snapshot in .metaobjects/migrations/, migrations
|
|
@@ -1417,8 +1526,11 @@ export async function runOfflineGenerate(
|
|
|
1417
1526
|
{ dir: writeDir, slug: config.slug },
|
|
1418
1527
|
);
|
|
1419
1528
|
await writeSnapshot(path, nextSnapshot);
|
|
1420
|
-
|
|
1421
|
-
|
|
1529
|
+
finishMigrate(
|
|
1530
|
+
fmt,
|
|
1531
|
+
[`migrate: wrote ${res.upPath}`, `migrate: wrote ${res.downPath}`, ...narratedNotes(offlineNotes)],
|
|
1532
|
+
migrateResultToData(offlineResult({ writtenPaths: [res.upPath, res.downPath], warnings: offlineWarnings })),
|
|
1533
|
+
);
|
|
1422
1534
|
return 0;
|
|
1423
1535
|
}
|
|
1424
1536
|
|
|
@@ -1432,6 +1544,7 @@ export async function runOfflineGenerate(
|
|
|
1432
1544
|
async function runRollback(
|
|
1433
1545
|
config: ResolvedMigrateConfig,
|
|
1434
1546
|
metaRoot: string,
|
|
1547
|
+
fmt: OutputFormat,
|
|
1435
1548
|
): Promise<number> {
|
|
1436
1549
|
// databaseUrl is guaranteed defined by the caller's guard above.
|
|
1437
1550
|
const databaseUrl = config.databaseUrl as string;
|
|
@@ -1459,11 +1572,10 @@ async function runRollback(
|
|
|
1459
1572
|
|
|
1460
1573
|
try {
|
|
1461
1574
|
const result = await rollbackTo(kysely.db, outDir, target, { dialect });
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1575
|
+
const summary = result.rolledBack.length > 0
|
|
1576
|
+
? `rolled back ${result.rolledBack.length} migration(s): ${result.rolledBack.join(", ")}`
|
|
1577
|
+
: `nothing to roll back${target ? ` newer than '${target}'` : ""}`;
|
|
1578
|
+
finishMigrate(fmt, [`migrate: ${summary}`], { rolledBack: result.rolledBack, summary });
|
|
1467
1579
|
return 0;
|
|
1468
1580
|
} catch (err) {
|
|
1469
1581
|
log.error(`migrate: rollback failed: ${describeError(err)}`);
|
|
@@ -1537,7 +1649,11 @@ async function runD1Migrate(
|
|
|
1537
1649
|
let d1LoadOptions: ReturnType<typeof loadMemoryOptionsFrom> = {};
|
|
1538
1650
|
try {
|
|
1539
1651
|
d1LoadOptions = loadMemoryOptionsFrom(await loadMetaobjectsConfig(genRoot));
|
|
1540
|
-
} catch {
|
|
1652
|
+
} catch (e) {
|
|
1653
|
+
if (e instanceof RemovedNamingConfigError) {
|
|
1654
|
+
log.error(e.message);
|
|
1655
|
+
return 2;
|
|
1656
|
+
}
|
|
1541
1657
|
d1LoadOptions = {};
|
|
1542
1658
|
}
|
|
1543
1659
|
|
|
@@ -1560,20 +1676,14 @@ async function runD1Migrate(
|
|
|
1560
1676
|
});
|
|
1561
1677
|
} catch (err) {
|
|
1562
1678
|
reportLoadError(log, "migrate: failed to load metadata", err);
|
|
1563
|
-
return
|
|
1679
|
+
return 1;
|
|
1564
1680
|
}
|
|
1565
1681
|
warnReferentialActionConflicts(metadata, collection);
|
|
1682
|
+
warnDecimalPrecision(metadata, collection, "d1");
|
|
1566
1683
|
|
|
1567
1684
|
// 4. Build expected schema + introspect actual.
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
1571
|
-
if (cfg.columnNamingStrategy) columnNamingStrategy = cfg.columnNamingStrategy;
|
|
1572
|
-
} catch {
|
|
1573
|
-
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
1574
|
-
}
|
|
1575
|
-
const expectedViews = buildProjectionViews(metadata, { dialect: "d1", columnNamingStrategy });
|
|
1576
|
-
const built = buildExpectedSchemaWithProvenance(metadata, { dialect: "d1", columnNamingStrategy, views: expectedViews });
|
|
1685
|
+
const expectedViews = buildProjectionViews(metadata, { dialect: "d1" });
|
|
1686
|
+
const built = buildExpectedSchemaWithProvenance(metadata, { dialect: "d1", views: expectedViews });
|
|
1577
1687
|
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
1578
1688
|
if (scopeRc !== undefined) return scopeRc;
|
|
1579
1689
|
// Per-command scope — both-sided, exactly as on the Kysely path above.
|
|
@@ -1634,6 +1744,7 @@ async function runD1Migrate(
|
|
|
1634
1744
|
}
|
|
1635
1745
|
|
|
1636
1746
|
const changeCounts = summarizeChanges(diffResult.changes);
|
|
1747
|
+
const d1Notes = viewRecreateNotes(diffResult.changes);
|
|
1637
1748
|
warnDataHazards(diffResult.hazards);
|
|
1638
1749
|
|
|
1639
1750
|
// Views are emitted by the one schema-diff path: renderD1 = renderSqlite (which
|
|
@@ -1641,16 +1752,20 @@ async function runD1Migrate(
|
|
|
1641
1752
|
// BEGIN/COMMIT + PRAGMA that recreate-and-copy emits). There is no separate
|
|
1642
1753
|
// view-migration emitter; introspectD1 now reads view bodies so unchanged views
|
|
1643
1754
|
// produce no change and body changes emit a DROP+CREATE.
|
|
1755
|
+
const d1Result = (extra: Partial<MigrateResultShape>): MigrateResultShape => ({
|
|
1756
|
+
dialect: "d1",
|
|
1757
|
+
displayUrl: binding.binding,
|
|
1758
|
+
changeCounts,
|
|
1759
|
+
// No `format` key here on purpose: every d1 output this build ships was produced
|
|
1760
|
+
// without it; adding it would change bytes the tests pin.
|
|
1761
|
+
...migrateResultDefaults(config.dryRun),
|
|
1762
|
+
...extra,
|
|
1763
|
+
});
|
|
1644
1764
|
if (diffResult.changes.length === 0) {
|
|
1645
|
-
|
|
1765
|
+
finishMigrate(fmt, [`migrate: no schema changes for d1 binding '${binding.binding}'`], migrateResultToData(d1Result({})));
|
|
1646
1766
|
return 0;
|
|
1647
1767
|
}
|
|
1648
1768
|
|
|
1649
|
-
if (config.slug === undefined) {
|
|
1650
|
-
log.error(`migrate: --slug <name> required when there are changes`);
|
|
1651
|
-
return 2;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
1769
|
// 6. Emit (with D1 safety pass) + write Wrangler migration files.
|
|
1655
1770
|
let emitResult;
|
|
1656
1771
|
try {
|
|
@@ -1663,6 +1778,7 @@ async function runD1Migrate(
|
|
|
1663
1778
|
throw err;
|
|
1664
1779
|
}
|
|
1665
1780
|
|
|
1781
|
+
d1Notes.push(...(emitResult.notices ?? []));
|
|
1666
1782
|
const combinedUp = emitResult.up;
|
|
1667
1783
|
const combinedDown = emitResult.down;
|
|
1668
1784
|
|
|
@@ -1671,19 +1787,32 @@ async function runD1Migrate(
|
|
|
1671
1787
|
const migrationsDir = resolveD1OutDir(config, metaRoot, binding.migrations_dir);
|
|
1672
1788
|
|
|
1673
1789
|
if (config.dryRun) {
|
|
1674
|
-
|
|
1790
|
+
const sql = { up: combinedUp, down: combinedDown };
|
|
1791
|
+
finishMigrate(fmt, [...narratedNotes(d1Notes), sqlPreview(sql)], migrateResultToData(d1Result({ sql, notes: d1Notes })));
|
|
1675
1792
|
return 0;
|
|
1676
1793
|
}
|
|
1677
1794
|
|
|
1795
|
+
// A preview (above) names no migration; only a write needs the slug.
|
|
1796
|
+
if (config.slug === undefined) {
|
|
1797
|
+
log.error(`migrate: --slug <name> required when there are changes`);
|
|
1798
|
+
emitStructuredError("migrate: --slug <name> required when there are changes", "re-run with --slug <name>", fmt);
|
|
1799
|
+
return 2;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1678
1802
|
const writeResult = await writeMigrationD1(
|
|
1679
1803
|
{ up: combinedUp, down: combinedDown },
|
|
1680
1804
|
{ dir: migrationsDir, slug: config.slug },
|
|
1681
1805
|
);
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1806
|
+
finishMigrate(
|
|
1807
|
+
fmt,
|
|
1808
|
+
[
|
|
1809
|
+
`migrate: wrote ${writeResult.upPath}`,
|
|
1810
|
+
`migrate: wrote ${writeResult.downPath}`,
|
|
1811
|
+
...Object.entries(changeCounts).map(([kind, count]) => ` ${kind}: ${count}`),
|
|
1812
|
+
...narratedNotes(d1Notes),
|
|
1813
|
+
],
|
|
1814
|
+
migrateResultToData(d1Result({ writtenPaths: [writeResult.upPath, writeResult.downPath], notes: d1Notes })),
|
|
1815
|
+
);
|
|
1687
1816
|
|
|
1688
1817
|
// 7. Optional --apply: run `wrangler d1 migrations apply`.
|
|
1689
1818
|
if (config.d1.autoApply) {
|
|
@@ -1707,7 +1836,8 @@ async function runWranglerApply(
|
|
|
1707
1836
|
yes: boolean,
|
|
1708
1837
|
): Promise<number> {
|
|
1709
1838
|
if (remote && !yes) {
|
|
1710
|
-
|
|
1839
|
+
// Progress, not output: stderr, so it never lands in front of a document on stdout.
|
|
1840
|
+
log.warn(
|
|
1711
1841
|
`Applying to remote D1 '${databaseName}' (binding=${bindingName}) in 2s — Ctrl+C to abort or pass --yes to skip this pause.`,
|
|
1712
1842
|
);
|
|
1713
1843
|
await new Promise<void>((r) => setTimeout(r, 2000));
|
|
@@ -1724,3 +1854,4 @@ async function runWranglerApply(
|
|
|
1724
1854
|
child.on("close", (code) => resolve(code ?? 1));
|
|
1725
1855
|
});
|
|
1726
1856
|
}
|
|
1857
|
+
|