@metaobjectsdev/cli 0.23.2 → 0.24.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 +78 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +148 -50
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +28 -6
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +61 -22
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +8 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +142 -52
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +9 -2
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +399 -73
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +40 -18
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts +2 -0
- package/dist/src/commands/upgrade.d.ts.map +1 -0
- package/dist/src/commands/upgrade.js +139 -0
- package/dist/src/commands/upgrade.js.map +1 -0
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +336 -48
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +46 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +4 -0
- package/dist/src/lib/allow.js.map +1 -1
- package/dist/src/lib/args.d.ts +16 -2
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +19 -3
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +8 -1
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +9 -1
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +22 -37
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts +48 -1
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +80 -1
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts +12 -0
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -0
- package/dist/src/lib/manifest-ignored-check.js +74 -0
- package/dist/src/lib/manifest-ignored-check.js.map +1 -0
- package/dist/src/lib/migrate-scope.d.ts +49 -0
- package/dist/src/lib/migrate-scope.d.ts.map +1 -0
- package/dist/src/lib/migrate-scope.js +92 -0
- package/dist/src/lib/migrate-scope.js.map +1 -0
- package/dist/src/lib/output.d.ts +1 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +9 -3
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +10 -57
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +11 -11
- package/src/commands/docs.ts +158 -49
- package/src/commands/export.ts +30 -6
- package/src/commands/gen.ts +62 -22
- package/src/commands/init.ts +161 -58
- package/src/commands/migrate.ts +423 -75
- package/src/commands/prompt-snapshot.ts +43 -18
- package/src/commands/upgrade.ts +156 -0
- package/src/commands/verify.ts +373 -48
- package/src/index.ts +46 -15
- package/src/lib/allow.ts +4 -0
- package/src/lib/args.ts +35 -4
- package/src/lib/codegen-drift.ts +9 -0
- package/src/lib/detect-stack.ts +20 -33
- package/src/lib/load-metaobjects-config.ts +84 -2
- package/src/lib/manifest-ignored-check.ts +75 -0
- package/src/lib/migrate-scope.ts +102 -0
- package/src/lib/output.ts +8 -4
- package/src/lib/requirement-check.ts +10 -58
- package/dist/src/lib/verified-by-scan.d.ts +0 -20
- package/dist/src/lib/verified-by-scan.d.ts.map +0 -1
- package/dist/src/lib/verified-by-scan.js +0 -344
- package/dist/src/lib/verified-by-scan.js.map +0 -1
- package/src/lib/verified-by-scan.ts +0 -375
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve as resolvePath } from "node:path";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { parseMigrateArgs } from "../lib/args.js";
|
|
5
6
|
import { resolveMigrateConfig, MIGRATE_DEFAULT_OUT_DIR } from "../lib/config.js";
|
|
@@ -8,9 +9,10 @@ import { formatMigrateResultJson } from "../lib/output-json.js";
|
|
|
8
9
|
import { toonEncode } from "../lib/format.js";
|
|
9
10
|
import { buildKyselyFromUrl, redactUrl } from "../lib/kysely.js";
|
|
10
11
|
import { log } from "../lib/log.js";
|
|
11
|
-
import { loadMemory } from "@metaobjectsdev/sdk";
|
|
12
|
-
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
13
|
-
import {
|
|
12
|
+
import { loadMemory, resolveCollection, resolveConfigDir } from "@metaobjectsdev/sdk";
|
|
13
|
+
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
14
|
+
import { migrateScopeMismatch, outOfScopeNote } from "../lib/migrate-scope.js";
|
|
15
|
+
import { buildExpectedSchemaWithProvenance, scopeExpectedSchema, scopedDiffInputs, introspect, diff, collectUnmanagedNames, carryForwardOutOfScope, emit, writeMigration, baselineFromMetadata, planOffline, snapshotPath, readSnapshot, writeSnapshot, qualifiedDbName, BlockedChangesError, PrimaryKeyChangeError, renderD1, writeMigrationD1, writeMigrationFlyway, introspectD1, applyPending, rollbackTo, findWranglerConfig, parseWranglerConfig, resolveD1Binding, } from "@metaobjectsdev/migrate-ts";
|
|
14
16
|
import { buildWranglerExecuteArgs, defaultWranglerRunner, } from "../lib/wrangler.js";
|
|
15
17
|
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
16
18
|
import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
@@ -24,8 +26,11 @@ SUBCOMMANDS:
|
|
|
24
26
|
(use with --from-db). NOTE: for a brand-new/empty database use
|
|
25
27
|
the greenfield example below, NOT baseline — an offline baseline
|
|
26
28
|
records your metadata as already-applied and emits no CREATE TABLE.
|
|
27
|
-
apply-pending Replay committed migration files against --db (no diff)
|
|
28
|
-
|
|
29
|
+
apply-pending Replay committed migration files against --db (no diff).
|
|
30
|
+
Provisions a fresh/CI database when the chain BUILDS the
|
|
31
|
+
schema — 'meta verify --replay' proves that it does. A
|
|
32
|
+
database adopted via 'baseline --from-db' has no such
|
|
33
|
+
chain. postgres/sqlite only.
|
|
29
34
|
|
|
30
35
|
MIGRATE FLAGS:
|
|
31
36
|
--db <url> DB connection URL (required for live-introspect / --apply / --rollback)
|
|
@@ -45,7 +50,13 @@ MIGRATE FLAGS:
|
|
|
45
50
|
--allow <csv> Comma-separated destructive-change permissions:
|
|
46
51
|
drop-column,drop-table,type-change,drop-index,drop-fk,
|
|
47
52
|
drop-check,drop-view,drop-view-cascade,
|
|
48
|
-
adopt-view,nullable-to-not-null,drop-identity-default
|
|
53
|
+
adopt-view,nullable-to-not-null,drop-identity-default,
|
|
54
|
+
drop-unmanaged
|
|
55
|
+
drop-unmanaged permits dropping a table/view the committed
|
|
56
|
+
snapshot never contained — one this toolchain never managed.
|
|
57
|
+
Without it that drop is refused, because the migration it
|
|
58
|
+
writes cannot replay against a database where the object
|
|
59
|
+
never existed.
|
|
49
60
|
--on-ambiguous abort|rename|drop-add
|
|
50
61
|
How to handle ambiguous renames (default: abort)
|
|
51
62
|
--from-db Introspect live DB instead of using the committed snapshot
|
|
@@ -83,6 +94,69 @@ function resolveFormatOutDir(config, metaRoot) {
|
|
|
83
94
|
}
|
|
84
95
|
return resolvePath(metaRoot, config.outDir);
|
|
85
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* D1's OWN directory convention — `--out-dir` > `wrangler.toml`'s
|
|
99
|
+
* `migrations_dir` > `"migrations"` — kept apart from `resolveFormatOutDir`
|
|
100
|
+
* because the middle term is not knowable until a wrangler binding has been
|
|
101
|
+
* resolved (`runD1Migrate` step 1), while every other dialect can answer
|
|
102
|
+
* immediately from `config` alone.
|
|
103
|
+
*/
|
|
104
|
+
function resolveD1OutDir(config, metaRoot, migrationsDirHint) {
|
|
105
|
+
const isDefaultOutDir = config.outDir === MIGRATE_DEFAULT_OUT_DIR;
|
|
106
|
+
return resolvePath(metaRoot, isDefaultOutDir ? (migrationsDirHint ?? "migrations") : config.outDir);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Say so when the migrations directory this run will use is NOT the one sitting
|
|
110
|
+
* in the working directory.
|
|
111
|
+
*
|
|
112
|
+
* `metaRoot` is now the discovered project root rather than ambient cwd, and the
|
|
113
|
+
* migrations directory follows it. That is the right call — the ledger belongs
|
|
114
|
+
* with the config that declares it — but it is a behaviour change for the two
|
|
115
|
+
* subcommands that load no metadata at all: `apply-pending` and `--rollback`
|
|
116
|
+
* used cwd unconditionally, so a subdirectory holding `.metaobjects/migrations`
|
|
117
|
+
* under a project root that also has one now replays the ROOT's ledger.
|
|
118
|
+
* Replaying somebody else's migration history silently is the worst outcome
|
|
119
|
+
* available here, so it is announced.
|
|
120
|
+
*
|
|
121
|
+
* Conditioned on the local directory EXISTING, so the ordinary case — a run from
|
|
122
|
+
* anywhere inside a project with one ledger at its root — says nothing.
|
|
123
|
+
* `--out-dir` (and a `migrate.outDir` in the config) is honoured: the caller
|
|
124
|
+
* must pass the directory THIS run will actually WRITE to, never a default
|
|
125
|
+
* that was overridden — a deliberate redirection is compared, not a stale
|
|
126
|
+
* guess. That answer comes from **two** call sites, because there are two
|
|
127
|
+
* directory conventions: every dialect but d1 resolves via
|
|
128
|
+
* `resolveFormatOutDir` before the format/dialect dispatch below; d1 has its
|
|
129
|
+
* own convention (`resolveD1OutDir`, wrangler.toml's `migrations_dir`) that is
|
|
130
|
+
* unknowable until its binding resolves, so it calls this again for itself
|
|
131
|
+
* from inside `runD1Migrate`, once that binding is in hand.
|
|
132
|
+
*/
|
|
133
|
+
function warnIfLedgerRelocated(cwd, resolvedOutDir) {
|
|
134
|
+
const local = resolvePath(cwd, MIGRATE_DEFAULT_OUT_DIR);
|
|
135
|
+
if (resolvedOutDir === local || !existsSync(local))
|
|
136
|
+
return;
|
|
137
|
+
log.warn(`migrate: using the migrations directory ${resolvedOutDir}, not the ${local} ` +
|
|
138
|
+
`in this working directory — the ledger belongs to the project root that declares it. ` +
|
|
139
|
+
`Pass --out-dir ${local} to use the local one.`);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Report what a declared `migrate.scope` left out (wording: `outOfScopeNote`).
|
|
143
|
+
*
|
|
144
|
+
* STDOUT in text format, STDERR otherwise. `--format json` / `--format toon` put a
|
|
145
|
+
* single machine-readable document on stdout, and a prose line ahead of it breaks
|
|
146
|
+
* `| jq` outright — the same split `emitStructuredError` makes just below.
|
|
147
|
+
* Routed to stderr rather than dropped, because the non-TTY default format is toon
|
|
148
|
+
* (`resolveFormat`): suppressing it outright would silence the note for every
|
|
149
|
+
* piped and CI run, which is most of them.
|
|
150
|
+
*/
|
|
151
|
+
function logOutOfScope(names, fmt) {
|
|
152
|
+
if (names.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
const msg = outOfScopeNote("migrate", names);
|
|
155
|
+
if (fmt === "text")
|
|
156
|
+
log.info(msg);
|
|
157
|
+
else
|
|
158
|
+
log.warn(msg);
|
|
159
|
+
}
|
|
86
160
|
function emitStructuredError(error, hint, fmt) {
|
|
87
161
|
const payload = { error, hint };
|
|
88
162
|
if (fmt === "json") {
|
|
@@ -93,6 +167,23 @@ function emitStructuredError(error, hint, fmt) {
|
|
|
93
167
|
}
|
|
94
168
|
// text format: errors go to stderr via log.error() — the caller handles that path
|
|
95
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* The refusal for a `migrate.scope` that matches nothing, as all three of migrate's
|
|
172
|
+
* pipelines (online, offline, D1) issue it.
|
|
173
|
+
*
|
|
174
|
+
* Returns the exit code to return, or `undefined` when there is nothing to refuse.
|
|
175
|
+
* Three byte-identical copies of the report-and-exit differed only in a local
|
|
176
|
+
* variable name; the hint string and the exit code are one decision, recorded once
|
|
177
|
+
* — a configuration error, so exit 2.
|
|
178
|
+
*/
|
|
179
|
+
function refuseScopeMismatch(collection, provenance, fmt) {
|
|
180
|
+
const mismatch = migrateScopeMismatch(collection, provenance);
|
|
181
|
+
if (mismatch === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
log.error(`migrate: ${mismatch}`);
|
|
184
|
+
emitStructuredError(`migrate: ${mismatch}`, "fix or remove migrate.scope in .metaobjects/config.json", fmt);
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
96
187
|
/**
|
|
97
188
|
* Sentinel thrown by sub-functions that have already emitted a structured error
|
|
98
189
|
* via emitStructuredError(). The top-level catch in migrateCommand re-throws
|
|
@@ -151,6 +242,75 @@ function summarizeChanges(changes) {
|
|
|
151
242
|
}
|
|
152
243
|
return counts;
|
|
153
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* The qualified names of tables/views/constraints this diff proposes to DROP that
|
|
247
|
+
* the committed snapshot never contained — i.e. objects this toolchain never
|
|
248
|
+
* managed (#313).
|
|
249
|
+
*
|
|
250
|
+
* FAILS OPEN. No snapshot on disk, or one that cannot be read, yields an empty list:
|
|
251
|
+
* a project that has never generated one is not in an error state, and refusing there
|
|
252
|
+
* would break the first `meta migrate` of every greenfield project. A parse failure is
|
|
253
|
+
* migrate's own error to raise elsewhere, with its own message, not a silent refusal
|
|
254
|
+
* here.
|
|
255
|
+
*
|
|
256
|
+
* Table/view names come from `qualifiedDbName` and nothing else. Three independent
|
|
257
|
+
* sets already have to agree on this spelling — the diff's identity maps, the
|
|
258
|
+
* `@unmanaged` exclusion set, and the out-of-scope set — and a fourth encoding of
|
|
259
|
+
* "absent schema means public" would silently un-guard every object it disagreed
|
|
260
|
+
* about.
|
|
261
|
+
*
|
|
262
|
+
* `drop-fk`/`drop-check`/constraint-backed `drop-index` are checked at the
|
|
263
|
+
* CONSTRAINT grain, not just the table's: the emitter's `IF EXISTS` on the
|
|
264
|
+
* enclosing `ALTER TABLE` (the SQL half of this same #313 gap) only stops the
|
|
265
|
+
* replay from failing outright — it says nothing about whether AUTHORING the
|
|
266
|
+
* drop was ever supposed to be permission-free. A table can be fully managed
|
|
267
|
+
* while carrying a constraint another tool added directly against the live
|
|
268
|
+
* database; that constraint's name is absent from the snapshotted table's own
|
|
269
|
+
* `foreignKeys`/`checks`/`indexes`, exactly like a whole unmanaged table is
|
|
270
|
+
* absent from `snapshot.tables`.
|
|
271
|
+
*/
|
|
272
|
+
async function snapshotAbsentDrops(changes, snapPath) {
|
|
273
|
+
let snapshot;
|
|
274
|
+
try {
|
|
275
|
+
snapshot = await readSnapshot(snapPath);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
if (snapshot === null)
|
|
281
|
+
return [];
|
|
282
|
+
const managedTables = new Map();
|
|
283
|
+
for (const t of snapshot.tables)
|
|
284
|
+
managedTables.set(qualifiedDbName(t), t);
|
|
285
|
+
const managedViews = new Set();
|
|
286
|
+
for (const v of snapshot.views)
|
|
287
|
+
managedViews.add(qualifiedDbName(v));
|
|
288
|
+
const absent = [];
|
|
289
|
+
for (const c of changes) {
|
|
290
|
+
if (c.kind === "drop-table") {
|
|
291
|
+
const name = qualifiedDbName({ name: c.table, schema: c.schema });
|
|
292
|
+
if (!managedTables.has(name))
|
|
293
|
+
absent.push(name);
|
|
294
|
+
}
|
|
295
|
+
else if (c.kind === "drop-view") {
|
|
296
|
+
const name = qualifiedDbName({ name: c.view, schema: c.schema });
|
|
297
|
+
if (!managedViews.has(name))
|
|
298
|
+
absent.push(name);
|
|
299
|
+
}
|
|
300
|
+
else if (c.kind === "drop-fk" || c.kind === "drop-check" || c.kind === "drop-index") {
|
|
301
|
+
const tableName = qualifiedDbName({ name: c.table, schema: c.schema });
|
|
302
|
+
const table = managedTables.get(tableName);
|
|
303
|
+
const constraintName = c.kind === "drop-fk" ? c.fk : c.kind === "drop-check" ? c.check : c.index;
|
|
304
|
+
const recorded = c.kind === "drop-fk" ? table?.foreignKeys
|
|
305
|
+
: c.kind === "drop-check" ? table?.checks
|
|
306
|
+
: table?.indexes;
|
|
307
|
+
if (recorded === undefined || !recorded.some((d) => d.name === constraintName)) {
|
|
308
|
+
absent.push(`${tableName}.${constraintName}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return absent;
|
|
313
|
+
}
|
|
154
314
|
function allowFlagFor(kind) {
|
|
155
315
|
switch (kind) {
|
|
156
316
|
case "drop-column": return "drop-column";
|
|
@@ -204,8 +364,56 @@ wranglerRunner, fmt = "text") {
|
|
|
204
364
|
emitStructuredError(`migrate: ${msg}`, "run `meta migrate --help` for usage", fmt);
|
|
205
365
|
return 2;
|
|
206
366
|
}
|
|
207
|
-
|
|
367
|
+
// The project root is the directory whose `.metaobjects/config.json` governs
|
|
368
|
+
// this run — the same directory `resolveCollection` resolves the metadata
|
|
369
|
+
// from, found the same way (design §4.6.1: "Per-port generator config is then
|
|
370
|
+
// read from that same directory"). Everything below is relative to it: the
|
|
371
|
+
// `.metaobjects/config.json` operational block, `metaobjects.config.ts`'s
|
|
372
|
+
// `columnNamingStrategy`, the migrations `outDir`, `wrangler.toml` discovery.
|
|
373
|
+
//
|
|
374
|
+
// Read from ambient cwd instead, as this did, they DIVERGE the moment the two
|
|
375
|
+
// differ: run `meta migrate` from a subdirectory of a project whose root
|
|
376
|
+
// declares `columnNamingStrategy: "literal"` and the metadata resolves from
|
|
377
|
+
// the ancestor while the strategy silently defaults to snake_case — emitting a
|
|
378
|
+
// migration that renames every column. Newly reachable, too: before metadata
|
|
379
|
+
// sources were resolvable that invocation just failed with "no metaobjects/
|
|
380
|
+
// found".
|
|
381
|
+
//
|
|
382
|
+
// `resolveConfigDir` rather than `resolveCollection` deliberately: this must
|
|
383
|
+
// not require metadata to EXIST. `migrate apply-pending` and `--rollback`
|
|
384
|
+
// replay committed SQL and load no metadata at all, and making them fail on a
|
|
385
|
+
// project with no model would be a regression. It is the SAME walk
|
|
386
|
+
// `resolveCollection` runs (one exported definition in the sdk's
|
|
387
|
+
// `discovery.ts`, not two that agree by construction), so the directory this
|
|
388
|
+
// resolves and the directory the metadata comes from cannot diverge.
|
|
389
|
+
const metaRoot = await resolveConfigDir(cwd);
|
|
390
|
+
// `metaobjects.config.ts` is the one thing here NOT governed by that walk. It
|
|
391
|
+
// answers a different question from `.metaobjects/config.json` (design §4.6 —
|
|
392
|
+
// per-port "how is code generated here?" versus port-neutral "where does metadata
|
|
393
|
+
// come from?"), and in a Maven- or pip-rooted monorepo the two legitimately sit in
|
|
394
|
+
// different directories. Reading it from `metaRoot` there silently defaults
|
|
395
|
+
// `columnNamingStrategy` for a package that declares one — the exact
|
|
396
|
+
// rename-every-column failure the co-location was introduced to prevent, arrived
|
|
397
|
+
// at from the other side (#326). Nearest wins, so a subdirectory declaring nothing
|
|
398
|
+
// still walks up to the project root's config; identical to `metaRoot` in every
|
|
399
|
+
// `meta init` project, where the two files sit together.
|
|
400
|
+
const genRoot = resolveGenConfigDir(cwd, metaRoot);
|
|
208
401
|
const config = await resolveMigrateConfig(flags, metaRoot);
|
|
402
|
+
// `resolveFormatOutDir`, not `resolvePath(metaRoot, config.outDir)`: under
|
|
403
|
+
// `--migration-format flyway` with a default outDir the run writes to
|
|
404
|
+
// Flyway's conventional location instead, so the unredirected path names a
|
|
405
|
+
// directory this invocation will never touch.
|
|
406
|
+
//
|
|
407
|
+
// Skipped for a plain `--dialect d1` run (format !== flyway): d1 resolves
|
|
408
|
+
// its OWN directory from wrangler.toml, unknowable until its binding
|
|
409
|
+
// resolves, so it issues this warning for itself from inside
|
|
410
|
+
// `runD1Migrate` instead — `resolveFormatOutDir` here would name the
|
|
411
|
+
// Kysely-path default, a directory that run never writes to. A d1 +
|
|
412
|
+
// `--migration-format flyway` run is refused just below before either
|
|
413
|
+
// directory is ever touched, so THAT combination still wants this one.
|
|
414
|
+
if (config.dialect !== "d1" || config.format === "flyway") {
|
|
415
|
+
warnIfLedgerRelocated(cwd, resolveFormatOutDir(config, metaRoot));
|
|
416
|
+
}
|
|
209
417
|
try {
|
|
210
418
|
// #192 — Flyway owns apply + history (flyway_schema_history). We generate the
|
|
211
419
|
// migration; applying it is Flyway's job. Refuse at generation time rather than
|
|
@@ -254,11 +462,11 @@ wranglerRunner, fmt = "text") {
|
|
|
254
462
|
emitStructuredError(`migrate: --rollback is not supported for dialect 'd1'`, "use 'wrangler d1 migrations' tooling to roll back d1", fmt);
|
|
255
463
|
return 2;
|
|
256
464
|
}
|
|
257
|
-
return await runD1Migrate(config, metaRoot, wranglerRunner ?? defaultWranglerRunner, fmt);
|
|
465
|
+
return await runD1Migrate(config, metaRoot, cwd, wranglerRunner ?? defaultWranglerRunner, fmt);
|
|
258
466
|
}
|
|
259
467
|
// `migrate baseline` — seed the committed reference snapshot, emit no migration.
|
|
260
468
|
if (config.baseline) {
|
|
261
|
-
return await runBaseline(config, metaRoot, fmt);
|
|
469
|
+
return await runBaseline(config, metaRoot, fmt, genRoot);
|
|
262
470
|
}
|
|
263
471
|
// `migrate apply-pending` — replay committed migration files; no diff, no metadata load.
|
|
264
472
|
if (config.applyPending) {
|
|
@@ -268,7 +476,7 @@ wranglerRunner, fmt = "text") {
|
|
|
268
476
|
// when explicitly requested via --from-db, when --apply needs a connection, or
|
|
269
477
|
// for --rollback (which runs hand-authored down.sql against the live DB).
|
|
270
478
|
if (!config.fromDb && !config.apply && config.rollback === undefined) {
|
|
271
|
-
return await runOfflineGenerate(config, metaRoot, fmt);
|
|
479
|
+
return await runOfflineGenerate(config, metaRoot, fmt, genRoot);
|
|
272
480
|
}
|
|
273
481
|
if (config.databaseUrl === undefined) {
|
|
274
482
|
log.error(`migrate: --db <url> required (or set DATABASE_URL, or add migrate.databaseUrl to .metaobjects/config.json)`);
|
|
@@ -284,28 +492,35 @@ wranglerRunner, fmt = "text") {
|
|
|
284
492
|
// Best-effort load of metaobjects.config.ts to pick up consumer-supplied
|
|
285
493
|
// providers. migrate's postgres/sqlite path also reads the config later
|
|
286
494
|
// for columnNamingStrategy; we load it once here and reuse below.
|
|
287
|
-
let
|
|
495
|
+
let postgresLoadOptions = {};
|
|
288
496
|
try {
|
|
289
|
-
|
|
290
|
-
postgresConfigProviders = forgeConfig.providers;
|
|
497
|
+
postgresLoadOptions = loadMemoryOptionsFrom(await loadMetaobjectsConfig(genRoot));
|
|
291
498
|
}
|
|
292
499
|
catch {
|
|
293
|
-
|
|
500
|
+
postgresLoadOptions = {};
|
|
501
|
+
}
|
|
502
|
+
// Discovery and load are two separate failure modes, kept in separate try blocks
|
|
503
|
+
// (the `meta gen` pattern): a broad catch around both reports a genuine ParseError
|
|
504
|
+
// as "no metadata found", masking the real failure. `resolveCollection` raises
|
|
505
|
+
// ERR_COLLECTION_NOT_FOUND with its own message — the same exit 2 the hand-rolled
|
|
506
|
+
// ENOENT sniff used to produce.
|
|
507
|
+
let collection;
|
|
508
|
+
try {
|
|
509
|
+
collection = await resolveCollection(metaRoot);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
log.error(err.message);
|
|
513
|
+
return 2;
|
|
294
514
|
}
|
|
295
515
|
let metadata;
|
|
296
516
|
try {
|
|
297
|
-
metadata = await loadMemory(
|
|
298
|
-
|
|
517
|
+
metadata = await loadMemory(collection.configDir, {
|
|
518
|
+
files: collection.files,
|
|
519
|
+
...postgresLoadOptions,
|
|
299
520
|
});
|
|
300
521
|
}
|
|
301
522
|
catch (err) {
|
|
302
|
-
|
|
303
|
-
if (msg.includes("ENOENT") || msg.includes("no such") || msg.includes("cannot read")) {
|
|
304
|
-
log.error(`no metaobjects/ found in ${metaRoot}; run 'meta init' to scaffold`);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
log.error(`failed to load metadata: ${msg}`);
|
|
308
|
-
}
|
|
523
|
+
log.error(`failed to load metadata: ${err.message}`);
|
|
309
524
|
return 2;
|
|
310
525
|
}
|
|
311
526
|
let kysely;
|
|
@@ -328,7 +543,7 @@ wranglerRunner, fmt = "text") {
|
|
|
328
543
|
// and projection view DDL — derive it once, up front, so every view path agrees.
|
|
329
544
|
let columnNamingStrategy = "snake_case";
|
|
330
545
|
try {
|
|
331
|
-
const cfg = await loadMetaobjectsConfig(
|
|
546
|
+
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
332
547
|
if (cfg.columnNamingStrategy)
|
|
333
548
|
columnNamingStrategy = cfg.columnNamingStrategy;
|
|
334
549
|
}
|
|
@@ -340,11 +555,21 @@ wranglerRunner, fmt = "text") {
|
|
|
340
555
|
// view DDL (create/drop/replace + dependency-recreate) and emit() renders it —
|
|
341
556
|
// there is no separate view-migration emitter.
|
|
342
557
|
const expectedViews = buildProjectionViews(metadata, { dialect: kysely.dialect, columnNamingStrategy });
|
|
343
|
-
const
|
|
558
|
+
const built = buildExpectedSchemaWithProvenance(metadata, {
|
|
344
559
|
dialect: kysely.dialect,
|
|
345
560
|
columnNamingStrategy,
|
|
346
561
|
views: expectedViews,
|
|
347
562
|
});
|
|
563
|
+
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
564
|
+
if (scopeRc !== undefined)
|
|
565
|
+
return scopeRc;
|
|
566
|
+
// Per-command scope: objects outside `migrate.scope` are another owner's. They
|
|
567
|
+
// leave the expected schema here and are suppressed on the actual side below —
|
|
568
|
+
// dropping them from `expected` ALONE would propose DROP TABLE for every one of
|
|
569
|
+
// them that exists in the database.
|
|
570
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
571
|
+
const expected = scoped.snapshot;
|
|
572
|
+
logOutOfScope(scoped.outOfScope, fmt);
|
|
348
573
|
let actual;
|
|
349
574
|
try {
|
|
350
575
|
actual = await introspect(kysely.db, kysely.dialect);
|
|
@@ -359,7 +584,12 @@ wranglerRunner, fmt = "text") {
|
|
|
359
584
|
let diffResult;
|
|
360
585
|
try {
|
|
361
586
|
diffResult = await diff({
|
|
362
|
-
|
|
587
|
+
// The three scoped-diff obligations as one value (migrate-ts's scope.ts
|
|
588
|
+
// header has the mechanism): the narrowed expected side, `unmanagedNames`
|
|
589
|
+
// merging #208 §7's declared-@unmanaged set with the out-of-scope names so
|
|
590
|
+
// neither is created or dropped, and the schema scope pinned to the
|
|
591
|
+
// UNSCOPED model so narrowing can never widen the run.
|
|
592
|
+
...scopedDiffInputs(scoped, collectUnmanagedNames(metadata)),
|
|
363
593
|
actual,
|
|
364
594
|
dialect: kysely.dialect,
|
|
365
595
|
allow: tokensToAllowOptions(config.allow),
|
|
@@ -367,9 +597,6 @@ wranglerRunner, fmt = "text") {
|
|
|
367
597
|
// has no expressible migration; refuse loudly instead of emitting SQL that drops
|
|
368
598
|
// the constraint and breaks referencing FKs at apply.
|
|
369
599
|
refusePrimaryKeyChange: true,
|
|
370
|
-
// #208 §7 — declared-@unmanaged objects are external: exclude them from the
|
|
371
|
-
// actual side so migrate proposes neither create nor drop for them.
|
|
372
|
-
unmanagedNames: collectUnmanagedNames(metadata),
|
|
373
600
|
onAmbiguous: async (a) => {
|
|
374
601
|
collectedAmbiguous.push(a);
|
|
375
602
|
return onAmbiguousResolution;
|
|
@@ -409,6 +636,34 @@ wranglerRunner, fmt = "text") {
|
|
|
409
636
|
throw err;
|
|
410
637
|
}
|
|
411
638
|
changeCounts = summarizeChanges(diffResult.changes);
|
|
639
|
+
// #313 — refuse to AUTHOR a drop for an object the committed snapshot never
|
|
640
|
+
// contained. This path diffs metadata against introspection and never reads the
|
|
641
|
+
// snapshot, so an object another tool owns reads as "in the DB, not in the model"
|
|
642
|
+
// and is proposed for a drop; the migration that results cannot replay against a
|
|
643
|
+
// database where that object never existed, which is how a chain stays broken for
|
|
644
|
+
// months. `classify.ts` already states the doctrine — objects present in the DB
|
|
645
|
+
// but not the snapshot "must never be treated as actionable drift or
|
|
646
|
+
// auto-dropped" — and this is where it is finally enforced.
|
|
647
|
+
//
|
|
648
|
+
// It does not false-fire on the brownfield cases, because both of them ADD to the
|
|
649
|
+
// snapshot: a `baseline --from-db` snapshot contains the foreign table, and a
|
|
650
|
+
// scoped project carries its out-of-scope entries forward. The guard fires
|
|
651
|
+
// precisely when nothing ever claimed the object.
|
|
652
|
+
//
|
|
653
|
+
// Only on THIS path, and that is not an omission: the offline path diffs metadata
|
|
654
|
+
// against the committed snapshot, so it proposes a drop only for an object the
|
|
655
|
+
// snapshot HAS. A snapshot-absent drop is unreachable there by construction.
|
|
656
|
+
const unmanagedDrops = await snapshotAbsentDrops(diffResult.changes, snapshotPath(resolvePath(metaRoot, config.outDir), kysely.dialect));
|
|
657
|
+
if (unmanagedDrops.length > 0 && tokensToAllowOptions(config.allow).dropUnmanaged !== true) {
|
|
658
|
+
const named = unmanagedDrops.join(", ");
|
|
659
|
+
log.error(`migrate: refusing to drop ${named} — absent from the committed schema snapshot, so this ` +
|
|
660
|
+
`toolchain never managed ${unmanagedDrops.length === 1 ? "it" : "them"} and the migration ` +
|
|
661
|
+
`could not replay against a database where ${unmanagedDrops.length === 1 ? "it" : "they"} ` +
|
|
662
|
+
`never existed. Re-run with '--allow drop-unmanaged' if the drop is intended.`);
|
|
663
|
+
emitStructuredError(`migrate: refusing to drop ${named} — absent from the committed schema snapshot`, "re-run with '--allow drop-unmanaged' if the drop is intended", fmt);
|
|
664
|
+
await kysely.close();
|
|
665
|
+
return 2;
|
|
666
|
+
}
|
|
412
667
|
// All changes — tables AND views — are emitted by the one schema-diff path.
|
|
413
668
|
// View DDL (create/drop/replace) is produced by diff()'s view passes (2b body
|
|
414
669
|
// comparison, 2c dependency-recreate) and rendered by every dialect's emitter;
|
|
@@ -507,7 +762,23 @@ wranglerRunner, fmt = "text") {
|
|
|
507
762
|
// native ALTER vs recreate-and-copy on older SQLite.
|
|
508
763
|
if (!config.dryRun && exitCode === 0 && !applyFailed && writtenPaths.length > 0) {
|
|
509
764
|
try {
|
|
510
|
-
|
|
765
|
+
// The COMMITTED snapshot keeps what `migrate.scope` excluded: writing the
|
|
766
|
+
// narrowed schema would delete every out-of-scope entry, so a later widening
|
|
767
|
+
// would propose CREATE TABLE for a table that exists and fail at apply. The
|
|
768
|
+
// out-of-scope entries come from `actual` — they are in the database, which
|
|
769
|
+
// is the same thing `baseline --from-db` records. Identical object, and so a
|
|
770
|
+
// byte-identical snapshot, for an unscoped run.
|
|
771
|
+
//
|
|
772
|
+
// The trade-off, stated so it is not rediscovered: those carried entries are
|
|
773
|
+
// INTROSPECTED descriptors, not metadata-built ones, so they can differ
|
|
774
|
+
// cosmetically from what this model would have emitted for the same table
|
|
775
|
+
// (column order, a default's rendered form). Removing the scope later can
|
|
776
|
+
// therefore produce one round of alter churn. That is strictly better than
|
|
777
|
+
// the alternative it replaced — a `CREATE TABLE` for a table that exists,
|
|
778
|
+
// which fails at apply — and it is the same mixed-provenance snapshot
|
|
779
|
+
// `baseline --from-db` writes for every table it adopts.
|
|
780
|
+
const committed = carryForwardOutOfScope(expected, actual, scoped.outOfScope);
|
|
781
|
+
await writeSnapshot(snapshotPath(resolvePath(metaRoot, config.outDir), kysely.dialect), actual.meta !== undefined ? { ...committed, meta: actual.meta } : committed);
|
|
511
782
|
}
|
|
512
783
|
catch (err) {
|
|
513
784
|
// The migration itself is written (and possibly applied) — report the
|
|
@@ -568,7 +839,11 @@ wranglerRunner, fmt = "text") {
|
|
|
568
839
|
* `--from-metadata` (default) derives it from metadata; `--from-db` introspects
|
|
569
840
|
* an existing database once. Emits no migration.
|
|
570
841
|
*/
|
|
571
|
-
export async function runBaseline(config, metaRoot, fmt = "text"
|
|
842
|
+
export async function runBaseline(config, metaRoot, fmt = "text",
|
|
843
|
+
/** Directory whose `metaobjects.config.ts` governs — see `migrateCommand`'s
|
|
844
|
+
* `genRoot`. Defaults to `metaRoot`, the co-located case every `meta init`
|
|
845
|
+
* project is in and the only one the direct-call test suites construct. */
|
|
846
|
+
genRoot = metaRoot) {
|
|
572
847
|
if (config.dialect === undefined) {
|
|
573
848
|
log.error(`migrate baseline: --dialect required (or set migrate.dialect in .metaobjects/config.json)`);
|
|
574
849
|
return 2;
|
|
@@ -621,23 +896,34 @@ export async function runBaseline(config, metaRoot, fmt = "text") {
|
|
|
621
896
|
`baseline — this emits NO CREATE TABLE. Use it only if your database already matches ` +
|
|
622
897
|
`your metadata; for a new/empty database run \`${greenfieldCreateCmd(config.dialect)}\` instead.`);
|
|
623
898
|
let metadata;
|
|
624
|
-
// Load metaobjects.config.ts ONCE, up front, for BOTH the consumer providers
|
|
899
|
+
// Load metaobjects.config.ts ONCE, up front, for BOTH the consumer providers/libraries
|
|
625
900
|
// and the columnNamingStrategy — mirroring the DB path (and `meta gen`) so
|
|
626
901
|
// offline baseline resolves config-registered custom subtypes too (#157).
|
|
627
|
-
let
|
|
902
|
+
let baselineLoadOptions = {};
|
|
628
903
|
let baselineStrategy = "snake_case";
|
|
629
904
|
try {
|
|
630
|
-
const cfg = await loadMetaobjectsConfig(
|
|
631
|
-
|
|
905
|
+
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
906
|
+
baselineLoadOptions = loadMemoryOptionsFrom(cfg);
|
|
632
907
|
if (cfg.columnNamingStrategy)
|
|
633
908
|
baselineStrategy = cfg.columnNamingStrategy;
|
|
634
909
|
}
|
|
635
910
|
catch {
|
|
636
|
-
// config absent — no custom providers, default snake_case
|
|
911
|
+
// config absent — no custom providers, no libraries, default snake_case
|
|
637
912
|
}
|
|
913
|
+
// `baseline` records a STARTING POINT, so it is deliberately NOT scoped: the
|
|
914
|
+
// `--from-db` arm captures whatever the database holds (there is no provenance
|
|
915
|
+
// for an introspected table), and an offline baseline that recorded less would
|
|
916
|
+
// disagree with it. An out-of-scope table sitting in the snapshot is harmless:
|
|
917
|
+
// every later run suppresses it on both sides of the diff, and an accepted
|
|
918
|
+
// scoped run carries it FORWARD (`carryForwardOutOfScope`) rather than dropping
|
|
919
|
+
// it — which is what makes that true. Committing the narrowed schema instead
|
|
920
|
+
// would delete the entry, and removing the scope later would then propose
|
|
921
|
+
// CREATE TABLE for a table that exists.
|
|
638
922
|
try {
|
|
639
|
-
|
|
640
|
-
|
|
923
|
+
const collection = await resolveCollection(metaRoot);
|
|
924
|
+
metadata = await loadMemory(collection.configDir, {
|
|
925
|
+
files: collection.files,
|
|
926
|
+
...baselineLoadOptions,
|
|
641
927
|
});
|
|
642
928
|
}
|
|
643
929
|
catch (err) {
|
|
@@ -735,35 +1021,50 @@ export async function runApplyPending(config, metaRoot, fmt = "text") {
|
|
|
735
1021
|
* Scope: table/column/index/FK changes. Projection-view migrations stay on the
|
|
736
1022
|
* introspection path (offline-view parity is a follow-up).
|
|
737
1023
|
*/
|
|
738
|
-
export async function runOfflineGenerate(config, metaRoot, fmt = "text"
|
|
1024
|
+
export async function runOfflineGenerate(config, metaRoot, fmt = "text",
|
|
1025
|
+
/** Directory whose `metaobjects.config.ts` governs — see `migrateCommand`'s
|
|
1026
|
+
* `genRoot`. Defaults to `metaRoot`, the co-located case. */
|
|
1027
|
+
genRoot = metaRoot) {
|
|
739
1028
|
if (config.dialect === undefined) {
|
|
740
1029
|
log.error(`migrate: --dialect required for offline generation (or use --from-db)`);
|
|
741
1030
|
return 2;
|
|
742
1031
|
}
|
|
743
|
-
// Load metaobjects.config.ts ONCE, up front, for BOTH the consumer providers
|
|
1032
|
+
// Load metaobjects.config.ts ONCE, up front, for BOTH the consumer providers/libraries
|
|
744
1033
|
// and the columnNamingStrategy — mirroring the DB path (and `meta gen`) so
|
|
745
1034
|
// offline generate resolves config-registered custom subtypes too (#157).
|
|
746
|
-
let
|
|
1035
|
+
let offlineLoadOptions = {};
|
|
747
1036
|
let offlineStrategy = "snake_case";
|
|
748
1037
|
try {
|
|
749
|
-
const cfg = await loadMetaobjectsConfig(
|
|
750
|
-
|
|
1038
|
+
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
1039
|
+
offlineLoadOptions = loadMemoryOptionsFrom(cfg);
|
|
751
1040
|
if (cfg.columnNamingStrategy)
|
|
752
1041
|
offlineStrategy = cfg.columnNamingStrategy;
|
|
753
1042
|
}
|
|
754
1043
|
catch {
|
|
755
|
-
// config absent — no custom providers, default snake_case
|
|
1044
|
+
// config absent — no custom providers, no libraries, default snake_case
|
|
756
1045
|
}
|
|
757
1046
|
let metadata;
|
|
1047
|
+
let collection;
|
|
758
1048
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
1049
|
+
collection = await resolveCollection(metaRoot);
|
|
1050
|
+
metadata = await loadMemory(collection.configDir, {
|
|
1051
|
+
files: collection.files,
|
|
1052
|
+
...offlineLoadOptions,
|
|
761
1053
|
});
|
|
762
1054
|
}
|
|
763
1055
|
catch (err) {
|
|
764
1056
|
log.error(`migrate: failed to load metadata: ${err.message}`);
|
|
765
1057
|
return 2;
|
|
766
1058
|
}
|
|
1059
|
+
const offlineDialect = config.dialect;
|
|
1060
|
+
const offlineViews = buildProjectionViews(metadata, { dialect: offlineDialect, columnNamingStrategy: offlineStrategy });
|
|
1061
|
+
const scopeRc = refuseScopeMismatch(collection, () => buildExpectedSchemaWithProvenance(metadata, {
|
|
1062
|
+
dialect: offlineDialect,
|
|
1063
|
+
columnNamingStrategy: offlineStrategy,
|
|
1064
|
+
views: offlineViews,
|
|
1065
|
+
}).provenance, fmt);
|
|
1066
|
+
if (scopeRc !== undefined)
|
|
1067
|
+
return scopeRc;
|
|
767
1068
|
const outDir = resolvePath(metaRoot, config.outDir);
|
|
768
1069
|
const path = snapshotPath(outDir, config.dialect);
|
|
769
1070
|
let snapshot;
|
|
@@ -788,7 +1089,7 @@ export async function runOfflineGenerate(config, metaRoot, fmt = "text") {
|
|
|
788
1089
|
}
|
|
789
1090
|
const collectedAmbiguous = [];
|
|
790
1091
|
const onAmbiguousResolution = mapOnAmbiguous(config.onAmbiguous);
|
|
791
|
-
const
|
|
1092
|
+
const offlineScope = collection.inMigrateScope;
|
|
792
1093
|
let plan;
|
|
793
1094
|
try {
|
|
794
1095
|
plan = await planOffline({
|
|
@@ -797,6 +1098,8 @@ export async function runOfflineGenerate(config, metaRoot, fmt = "text") {
|
|
|
797
1098
|
snapshot,
|
|
798
1099
|
columnNamingStrategy: offlineStrategy,
|
|
799
1100
|
views: offlineViews,
|
|
1101
|
+
// Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
|
|
1102
|
+
...(offlineScope !== undefined ? { inScope: offlineScope } : {}),
|
|
800
1103
|
allow: tokensToAllowOptions(config.allow),
|
|
801
1104
|
onAmbiguous: async (a) => {
|
|
802
1105
|
collectedAmbiguous.push(a);
|
|
@@ -817,7 +1120,11 @@ export async function runOfflineGenerate(config, metaRoot, fmt = "text") {
|
|
|
817
1120
|
}
|
|
818
1121
|
throw err;
|
|
819
1122
|
}
|
|
820
|
-
|
|
1123
|
+
// `nextSnapshot` is what gets COMMITTED (it retains this run's out-of-scope
|
|
1124
|
+
// entries); `expected` is the governed side the emitter renders against. Equal
|
|
1125
|
+
// for an unscoped run.
|
|
1126
|
+
const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
|
|
1127
|
+
logOutOfScope(plan.outOfScope, fmt);
|
|
821
1128
|
if (diffResult.blocked.length > 0) {
|
|
822
1129
|
log.error(`migrate: ${diffResult.blocked.length} destructive change(s) blocked; re-run with --allow <tokens>`);
|
|
823
1130
|
return 1;
|
|
@@ -832,7 +1139,7 @@ export async function runOfflineGenerate(config, metaRoot, fmt = "text") {
|
|
|
832
1139
|
}
|
|
833
1140
|
const emitResult = emit(diffResult.changes, {
|
|
834
1141
|
dialect: config.dialect,
|
|
835
|
-
expectedSchema:
|
|
1142
|
+
expectedSchema: governedExpected,
|
|
836
1143
|
actualSchema: snapshot,
|
|
837
1144
|
...(snapshot.meta ? { actualMeta: snapshot.meta } : {}),
|
|
838
1145
|
});
|
|
@@ -907,7 +1214,12 @@ async function runRollback(config, metaRoot) {
|
|
|
907
1214
|
}
|
|
908
1215
|
}
|
|
909
1216
|
}
|
|
910
|
-
async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
1217
|
+
async function runD1Migrate(config, metaRoot, cwd, runner, fmt = "text") {
|
|
1218
|
+
// Same split as `migrateCommand`: `.metaobjects/` state hangs off `metaRoot`,
|
|
1219
|
+
// `metaobjects.config.ts` off its own nearest-ancestor walk (#326). Recomputed
|
|
1220
|
+
// rather than threaded — it is a pure function of the two arguments this already
|
|
1221
|
+
// takes, so the two cannot disagree.
|
|
1222
|
+
const genRoot = resolveGenConfigDir(cwd, metaRoot);
|
|
911
1223
|
// 1. Resolve wrangler.toml + binding.
|
|
912
1224
|
const wranglerConfigPath = config.d1.wranglerConfigPath
|
|
913
1225
|
? resolvePath(metaRoot, config.d1.wranglerConfigPath)
|
|
@@ -931,6 +1243,11 @@ async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
|
931
1243
|
// No wrangler config but explicit binding — let wrangler discover the DB itself.
|
|
932
1244
|
binding = { binding: config.d1.binding, database_name: "", database_id: "", migrations_dir: undefined };
|
|
933
1245
|
}
|
|
1246
|
+
// The binding — and with it wrangler.toml's `migrations_dir` — is only now
|
|
1247
|
+
// known, so this is the earliest point d1 can honestly answer "where will
|
|
1248
|
+
// this run write?" (the caller skipped its own generic check for exactly
|
|
1249
|
+
// this reason; see the guard around that call).
|
|
1250
|
+
warnIfLedgerRelocated(cwd, resolveD1OutDir(config, metaRoot, binding.migrations_dir));
|
|
934
1251
|
// 2. Build a D1Runner closure over the wrangler runner.
|
|
935
1252
|
const d1Runner = async (sql) => {
|
|
936
1253
|
const args = buildWranglerExecuteArgs({
|
|
@@ -944,34 +1261,39 @@ async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
|
944
1261
|
};
|
|
945
1262
|
// 3. Load metadata. Best-effort config read for consumer providers; falls
|
|
946
1263
|
// back to default core+forge bundle if metaobjects.config.ts is absent.
|
|
947
|
-
let
|
|
1264
|
+
let d1LoadOptions = {};
|
|
948
1265
|
try {
|
|
949
|
-
|
|
950
|
-
d1ConfigProviders = forgeConfig.providers;
|
|
1266
|
+
d1LoadOptions = loadMemoryOptionsFrom(await loadMetaobjectsConfig(genRoot));
|
|
951
1267
|
}
|
|
952
1268
|
catch {
|
|
953
|
-
|
|
1269
|
+
d1LoadOptions = {};
|
|
1270
|
+
}
|
|
1271
|
+
// Discovery and load are separate failure modes (the `meta gen` pattern);
|
|
1272
|
+
// `resolveCollection`'s own ERR_COLLECTION_NOT_FOUND replaces the hand-rolled
|
|
1273
|
+
// ENOENT sniff, with the same exit 2.
|
|
1274
|
+
let collection;
|
|
1275
|
+
try {
|
|
1276
|
+
collection = await resolveCollection(metaRoot);
|
|
1277
|
+
}
|
|
1278
|
+
catch (err) {
|
|
1279
|
+
log.error(err.message);
|
|
1280
|
+
return 2;
|
|
954
1281
|
}
|
|
955
1282
|
let metadata;
|
|
956
1283
|
try {
|
|
957
|
-
metadata = await loadMemory(
|
|
958
|
-
|
|
1284
|
+
metadata = await loadMemory(collection.configDir, {
|
|
1285
|
+
files: collection.files,
|
|
1286
|
+
...d1LoadOptions,
|
|
959
1287
|
});
|
|
960
1288
|
}
|
|
961
1289
|
catch (err) {
|
|
962
|
-
|
|
963
|
-
if (msg.includes("ENOENT") || msg.includes("no such") || msg.includes("cannot read")) {
|
|
964
|
-
log.error(`no metaobjects/ found in ${metaRoot}; run 'meta init' to scaffold`);
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
log.error(`migrate: failed to load metadata: ${msg}`);
|
|
968
|
-
}
|
|
1290
|
+
log.error(`migrate: failed to load metadata: ${err.message}`);
|
|
969
1291
|
return 2;
|
|
970
1292
|
}
|
|
971
1293
|
// 4. Build expected schema + introspect actual.
|
|
972
1294
|
let columnNamingStrategy = "snake_case";
|
|
973
1295
|
try {
|
|
974
|
-
const cfg = await loadMetaobjectsConfig(
|
|
1296
|
+
const cfg = await loadMetaobjectsConfig(genRoot);
|
|
975
1297
|
if (cfg.columnNamingStrategy)
|
|
976
1298
|
columnNamingStrategy = cfg.columnNamingStrategy;
|
|
977
1299
|
}
|
|
@@ -979,7 +1301,14 @@ async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
|
979
1301
|
// metaobjects.config.ts absent or invalid — use default snake_case
|
|
980
1302
|
}
|
|
981
1303
|
const expectedViews = buildProjectionViews(metadata, { dialect: "d1", columnNamingStrategy });
|
|
982
|
-
const
|
|
1304
|
+
const built = buildExpectedSchemaWithProvenance(metadata, { dialect: "d1", columnNamingStrategy, views: expectedViews });
|
|
1305
|
+
const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
|
|
1306
|
+
if (scopeRc !== undefined)
|
|
1307
|
+
return scopeRc;
|
|
1308
|
+
// Per-command scope — both-sided, exactly as on the Kysely path above.
|
|
1309
|
+
const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
|
|
1310
|
+
const expected = scoped.snapshot;
|
|
1311
|
+
logOutOfScope(scoped.outOfScope, fmt);
|
|
983
1312
|
let actual;
|
|
984
1313
|
try {
|
|
985
1314
|
actual = await introspectD1({
|
|
@@ -999,7 +1328,8 @@ async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
|
999
1328
|
let diffResult;
|
|
1000
1329
|
try {
|
|
1001
1330
|
diffResult = await diff({
|
|
1002
|
-
|
|
1331
|
+
// The three scoped-diff obligations, exactly as on the online path above.
|
|
1332
|
+
...scopedDiffInputs(scoped, collectUnmanagedNames(metadata)),
|
|
1003
1333
|
actual,
|
|
1004
1334
|
// D1 is SQLite at the SQL level — the dialect activates the sqlite diff
|
|
1005
1335
|
// semantics (structural FK matching: SQLite stores no FK names; CHECK
|
|
@@ -1011,8 +1341,6 @@ async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
|
1011
1341
|
// has no expressible migration; refuse loudly instead of emitting SQL that drops
|
|
1012
1342
|
// the constraint and breaks referencing FKs at apply (same failure as the online path).
|
|
1013
1343
|
refusePrimaryKeyChange: true,
|
|
1014
|
-
// #208 §7 — declared-@unmanaged objects are external (see the online path above).
|
|
1015
|
-
unmanagedNames: collectUnmanagedNames(metadata),
|
|
1016
1344
|
onAmbiguous: async (a) => {
|
|
1017
1345
|
collectedAmbiguous.push(a);
|
|
1018
1346
|
return onAmbiguousResolution;
|
|
@@ -1067,11 +1395,9 @@ async function runD1Migrate(config, metaRoot, runner, fmt = "text") {
|
|
|
1067
1395
|
}
|
|
1068
1396
|
const combinedUp = emitResult.up;
|
|
1069
1397
|
const combinedDown = emitResult.down;
|
|
1070
|
-
// Migration dir resolution
|
|
1071
|
-
//
|
|
1072
|
-
|
|
1073
|
-
const isDefaultOutDir = config.outDir === MIGRATE_DEFAULT_OUT_DIR;
|
|
1074
|
-
const migrationsDir = resolvePath(metaRoot, isDefaultOutDir ? (binding.migrations_dir ?? "migrations") : config.outDir);
|
|
1398
|
+
// Migration dir resolution — same convention `warnIfLedgerRelocated` was
|
|
1399
|
+
// just given above, so the two cannot drift apart.
|
|
1400
|
+
const migrationsDir = resolveD1OutDir(config, metaRoot, binding.migrations_dir);
|
|
1075
1401
|
if (config.dryRun) {
|
|
1076
1402
|
log.info(`-- UP --\n${combinedUp}\n\n-- DOWN --\n${combinedDown}`);
|
|
1077
1403
|
return 0;
|