@metaobjectsdev/cli 1.0.13 → 1.1.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
436
|
-
//
|
|
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
|
|
441
|
-
//
|
|
442
|
-
//
|
|
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
|
|
459
|
-
//
|
|
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.
|
|
558
|
-
//
|
|
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
|
|
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
|
|
609
|
-
// and projection view DDL —
|
|
610
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
943
|
+
narrate(fmt, `migrate: applied ${appliedNames.length} migration(s): ${appliedNames.join(", ")}`);
|
|
896
944
|
}
|
|
897
945
|
else {
|
|
898
|
-
|
|
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
|
|
981
|
-
//
|
|
982
|
-
//
|
|
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
|
|
1063
|
+
return 1;
|
|
1013
1064
|
}
|
|
1014
|
-
const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect
|
|
1015
|
-
snapshot = baselineFromMetadata(metadata, config.dialect,
|
|
1065
|
+
const baselineViews = buildProjectionViews(metadata, { dialect: config.dialect });
|
|
1066
|
+
snapshot = baselineFromMetadata(metadata, config.dialect, baselineViews);
|
|
1016
1067
|
}
|
|
1017
1068
|
if (config.dryRun) {
|
|
1018
|
-
|
|
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
|
-
|
|
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
|
|
1114
|
-
//
|
|
1115
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1304
|
+
finishMigrate(fmt, [`migrate: no changes`], migrateResultToData(offlineResult({})));
|
|
1220
1305
|
return 0;
|
|
1221
1306
|
}
|
|
1222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
|
1470
|
+
return 1;
|
|
1380
1471
|
}
|
|
1381
1472
|
warnReferentialActionConflicts(metadata, collection);
|
|
1473
|
+
warnDecimalPrecision(metadata, collection, "d1");
|
|
1382
1474
|
// 4. Build expected schema + introspect actual.
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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
|
-
|
|
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"];
|