Skip to content

Commit f963d8e

Browse files
fix(cli): os migrate plan/apply examine the datasource a datasourceMapping default routes objects to, as os dev does (#24199)
Fixes #24120 Clause-②: no ## What was wrong Take a project whose primary store is a declared datasource that `datasourceMapping` routes as the default: `datasources: [{ name: 'qa_main', driver: 'sqlite', … }]` with `datasourceMapping: [{ default: true, datasource: 'qa_main' }]`. On it, `os migrate plan` resolved `qa_main`'s database as its primary ("the project config's default datasource qa_main"), but it registered no driver named `qa_main`. Every object the rule routes there, platform objects included, was bound to no driver. The plan printed `Examined 0 managed table(s)` and a PARTIAL plan, and logged `[ObjectQL] Datasource 'qa_main' mapped for object 'sys_metadata' is not registered`. `os dev` on the same config reports drift in that database and names `os migrate` as the remedy. ## Mechanism, measured (dispatch hypotheses H1–H3) - **H1 holds.** Reproduced at `cb8756b00` on a minimal scratch stack (`qa_main` sqlite plus the default mapping, one app object, provisioned by a boot, then staged). The plan reported `Examined 0 managed table(s)` and coverage `0 of 71 declared object(s) … (71 bound to no driver)`. `os dev` on the same database printed `[schema-drift] os24120_thing.extra: column exists in the database but not in metadata (orphaned)`. - **H2, located one level above `schema-migrate.ts`'s driver lookup.** `AppPlugin` connects declared datasources only through the `datasource-connection` service (`honourDatasourceMappingBeforeSchemaSync` and `start()`, then `connectDeclared`, the ADR-0062 D1 path). `os dev` / `os serve` publish that service from `DatasourceAdminServicePlugin` (`packages/cli/src/commands/serve.ts`, the Runtime Datasource Admin block). The migrate boot (`bootSchemaStack`, `packages/cli/src/utils/schema-migrate.ts`) published nothing, so `AppPlugin` left `qa_main` metadata-only. Even with a driver, the planned set was the primary and the telemetry sibling only. - **H3 is false, as the dispatch allowed.** The definition is already in hand: `AppPlugin` connects the declared datasource from the artifact or config it holds. `packages/runtime` (`resolve-project-database.ts`) is untouched. ## The fix (`packages/cli` only) - `bootSchemaStack` gets `composeDeclaredDatasources`. It is set by `os migrate plan`, by `apply`, and by `unmapped-columns` (see below). It composes `DeclaredDatasourceConnectionPlugin`, which publishes `datasource-connection` as a `DatasourceConnectionService` over the shared driver factory. That is the same path `os dev` connects `qa_main` by, under its declared name. It is not composed when the composition already brings a publisher. - Its connect policy admits **managed** datasources only. An `external` / `validate-only` datasource holds no table a migration plans, and its objects are federated. - A read-only probe's factory opens an absent SQLite file as an empty in-memory database, as the primary's does. Measured: a fresh project's plan leaves no `data/` directory behind. - The planned drivers are the primary and the sibling, plus each SQL driver that meets two conditions: it is registered under a declared datasource name the service reports `available`, and a registered non-federated object resolves to it (the engine's routing decides). `SchemaStack.databaseOf` names each planned driver's database. A composition note names each declared datasource and its database, in both faces. - `distinctDatabaseDrivers`: the unmanaged-table sweep reads each database once. The primary resolved from a declared default and the declared connection are two connections to one file, and without this `physicalTables` doubled (measured 144 against 72). - **`os migrate unmapped-columns` (outside the claim's file surface).** It filters the plan's own findings. Once the plan composed `qa_main`, its refusal on this config, `sys_user is not in the set "os migrate plan" diffs: it is bound to no driver`, became false. It now composes the same datasources and names the database through `databaseOf`. Edited under the standing rule that a published defect this change makes false is fixed in the same change. The claim's file surface needs that path added. **Scope.** The mechanism connects every managed declared datasource `os serve` connects: one a mapping routes objects to, one an object binds, or one with `autoConnect`. It is not limited to the default. Limiting it would have needed a filter that keeps the same defect on a non-default mapping rule. **Behaviour change:** a routed managed datasource that cannot connect now fails `plan` / `apply` / `unmapped-columns` with the connect error `os serve` gives. Measured with an unreachable mapped postgres: exit 1 and `datasource 'qa_pg': connect failed — connect ECONNREFUSED … fail-fast per ADR-0062 D5`. Under `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` the plan exits 0 and is PARTIAL (`1 bound to no driver`). ## Evidence The pin is `packages/cli/src/commands/migrate/plan.declared-default-datasource.integration.test.ts` (integration tier, `// test-budget: 80`). It provisions with `os dev --compile` and `OS_MIGRATE_AND_EXIT=1`, then stages two drifts in `qa_main`'s file: a column the app object stopped declaring, and a column on `sys_user`. Then it asserts four things: - the plan's drift set equals the `[schema-drift]` set `os dev` prints, and holds both staged drifts; - `unexaminedObjects: 0`, and `databaseSource` is `config-datasource` `qa_main`; - `physicalTables` equals the file's table count; - `os migrate unmapped-columns --object sys_user` reads `fx24120_legacy`. | run | tree | result | |---|---|---| | pin, after | `0b5ed65b4` | 1 passed, 56.1 s | | pin, before (`schema-migrate.ts`, `plan.ts`, `apply.ts` restored to `cb8756b00`) | `be919df4a` | **red**: `AssertionError: expected [] to deeply equal [ 'fx24120_thing.extra', …(1) ]` | | unmapped leg, before (only `unmapped-columns.ts` restored to `cb8756b00`) | `0b5ed65b4` | **red**: `os migrate unmapped-columns --object sys_user failed` (exit 1, `Datasource 'qa_main' … is not registered`) | | policy ablation (`schemaMode` check replaced by `true`, `scripts/ablation-replace.mjs`) | `3df5f9f1f` | **red**: the `external` and `validate-only` cases of `schema-migrate.declared-datasources.test.ts` | Each before or ablation leg restored with `git checkout HEAD --`, with `git diff HEAD` empty and blob hashes equal to HEAD. Manual readings on the scratch stack, after the fix: the plan examined 71 tables and listed `os24120_thing.extra [drop_column]`. `apply --allow-destructive --yes` applied it, and the next plan showed 0 changes and 0 unexamined. `--database-url file:` pointing at the same file gave `Examined 71`, the same drift, and `physicalTables: 72`. Local checks at `0b5ed65b4`: - `pnpm --filter @objectstack/cli typecheck && vitest run --project unit`: exit 0, 310 files / 4599 tests. - `vitest run --project integration` over the pin and `unmapped-columns.integration.test.ts`: 2 files / 11 tests passed. - `pnpm exec eslint --no-inline-config` over the 6 changed `.ts` files: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands`: 65 families derived; all 65 run, each exit 0. `--ran`: `65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN`. CI owns the rest: the farm, `Test Core`, the type-check lanes, dogfood. ## Acceptance notes - **Residue, stated:** runtime-origin datasources (created in Setup) are restored and connected by `DatasourceAdminServicePlugin.start()` on `os serve`, which this boot does not run. Objects bound to them stay "bound to no driver", and the plan says so (PARTIAL). - `os migrate organization-ownership` (held off on this seat) and `security-catalog-overlays` are not opted in. On a `datasourceMapping`-default deployment the former's drops "through that set's drift" still examine none of the routed tables. Not measured here. - `docs/qa/platform-checklist/areas/cli.json`, item `cli.dev-automigrate-policy`: step 2's note that `os migrate plan` "composes no driver for it and prints a PARTIAL plan" is stale once this lands, and the plan can serve as that step's oracle again. - Neighbour #24059 (`sys_packages` listed as unmanaged) is not touched: the sweep listed `sys_packages` alone, once, before and after. - A missing declared index (staged by dropping `idx_sys_user_created_at`) is a `create_index` drift in the plan, while `os dev`'s boot creates it silently. The two agree on the remedy. This predates this change; the pin stages no such drift. - `origin/main` moved 5 commits since `cb8756b00` (to `2bcaefba9`). None touches a path here, and the branch was not merged. CI's merge ref and the queue's rebuilt generation carry the joint check. --- _Generated by [Claude Code](https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b1845ed commit f963d8e

7 files changed

Lines changed: 569 additions & 14 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os migrate plan` and `os migrate apply` examine the objects a `datasourceMapping` rule routes to a declared datasource, the declared default included, in that datasource, as `os serve` connects it (#24120)
6+
7+
Clause-②: no
8+
9+
A project whose primary store is a declared datasource routed as the default (`datasources: [{ name: 'qa_main', … }]` with `datasourceMapping: [{ default: true, datasource: 'qa_main' }]`) got a plan that examined nothing. The plan resolved `qa_main`'s database as its primary, but it registered no driver under the name `qa_main`, so every object the rule routes there, platform objects included, was bound to no driver. The plan printed `Examined 0 managed table(s)` and a PARTIAL plan. Meanwhile `os dev` on the same project reported drift in that database and named `os migrate` as the remedy.
10+
11+
- **Now:** `plan` and `apply` connect the project's declared **managed** datasources the way `os serve` connects them: through the same `datasource-connection` service, under the declared name, for the datasources a mapping rule routes objects to, an object binds to, or that set `autoConnect`. Each datasource that objects live in is diffed and applied beside the primary. A composition note names each one and its database, in both the human output and the `--json` output. On a `datasourceMapping`-default project, the plan's drift is the drift `os dev` reports there.
12+
- **Not connected:** a datasource whose schema the remote owns (`schemaMode: 'external'` or `'validate-only'`). Its objects are federated, and a plan has no table on it.
13+
- **Refused now:** a routed managed datasource that cannot connect fails `plan`, `apply` and `unmapped-columns` with the connect error `os serve` gives for it. It is no longer skipped with a PARTIAL plan. `OS_ALLOW_DRIVER_CONNECT_FAILURE` degrades it here as it does there.
14+
- **`os migrate unmapped-columns` follows:** it filters the plan's own findings, so it composes the same datasources. On such a project it reads an object's unmapped columns from the declared datasource. Before, it refused with "not in the set os migrate plan diffs: it is bound to no driver".
15+
- **Unchanged:** a project that declares no datasource gets the plan it got before. `os migrate security-catalog-overlays` and `organization-ownership` boot as before. The unmanaged-table sweep's `physicalTables` counts each database once, although the primary and a declared default resolved from it are two connections to one file.

‎packages/cli/src/commands/migrate/apply.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export default class MigrateApply extends Command {
219219
deferredDdlUnsupported: 'skip-schema-sync',
220220
composeHostStack: true,
221221
composeServedPlatform: true,
222+
// [#24120] The plan's datasources too: each declared datasource a
223+
// `datasourceMapping` rule routes objects to, connected as `os serve`
224+
// connects it, reconciled where they live.
225+
composeDeclaredDatasources: true,
222226
});
223227
} catch (error: any) {
224228
if (flags.json) { await emitJson({ error: error.message, ...errorCodeFields(error) }, 0, { compact: true }); this.exit(1); }
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
// test-budget: 80 — spawns the real producer once per single-cause fixture: four spawns of the source CLI over one staged fixture (os dev to provision the declared datasource, os dev over the staged drift, os migrate plan --json, os migrate unmapped-columns --json); 56.1 s, the slower of two local runs on a shared container, x 1.25, no CI reading yet
3+
4+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
5+
import { spawnSync } from 'node:child_process';
6+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
7+
import { createRequire } from 'node:module';
8+
import { createServer } from 'node:net';
9+
import { tmpdir } from 'node:os';
10+
import { dirname, join, resolve } from 'node:path';
11+
import { fileURLToPath } from 'node:url';
12+
import Database from 'better-sqlite3';
13+
14+
/**
15+
* [#24120] On a project whose primary store is a DECLARED datasource that
16+
* `datasourceMapping` routes as the default, `os migrate plan` examines the
17+
* drift `os dev` reports — the same set, in the same database.
18+
*
19+
* ## The defect
20+
*
21+
* `datasources: [{ name: 'qa_main', … }]` with
22+
* `datasourceMapping: [{ default: true, datasource: 'qa_main' }]`. `os dev`
23+
* connects `qa_main` under its declared name (through the
24+
* `datasource-connection` service `AppPlugin` connects declared datasources
25+
* by), so every object — platform objects included — lives there, and its boot
26+
* prints a `[schema-drift]` line per divergence, each naming `os migrate` as
27+
* the remedy. `os migrate plan` resolved the same database FILE as its primary
28+
* ("the project config's default datasource qa_main") but published no such
29+
* service, so `qa_main` stayed metadata-only, every object routed to it was
30+
* bound to no driver, and the plan printed `Examined 0 managed table(s)` and a
31+
* PARTIAL plan with no drift at all.
32+
*
33+
* ## What is pinned
34+
*
35+
* Two drifts are staged in the declared datasource's database, one per kind of
36+
* object the default rule routes: a column the app object no longer declares,
37+
* and a column no platform object declares on `sys_user`. Then:
38+
*
39+
* - the plan's drift set (`table.column` of each `changes` entry) EQUALS the
40+
* set of `[schema-drift]` lines `os dev` prints over the same database — and
41+
* holds both staged drifts, so the equality is not two empty sets;
42+
* - the plan examined every declared object (`unexaminedObjects: 0`) and
43+
* names who chose its database (`config-datasource`, `qa_main`);
44+
* - its unmanaged-table sweep counts that database's tables once, although
45+
* the primary and `qa_main` are two connections to the one file;
46+
* - `os migrate unmapped-columns`, a filter over the plan's own findings,
47+
* reads the platform object's unmapped column from the same datasource.
48+
*
49+
* `os dev` runs as the operator runs it, compiling the config, with
50+
* `OS_MIGRATE_AND_EXIT=1` (its `serve --dev` child boots, syncs and exits) and
51+
* `--no-restart` (so the parent exits with it). The datasource's filename is
52+
* RELATIVE, as an author writes it: the primary resolves it against the project
53+
* root, the declared connection opens it from the project directory.
54+
*/
55+
56+
const HERE = dirname(fileURLToPath(import.meta.url));
57+
const CLI_ROOT = resolve(HERE, '..', '..', '..');
58+
const RUN_DEV = resolve(CLI_ROOT, 'bin', 'run-dev.js');
59+
const TSX = resolve(CLI_ROOT, '..', '..', 'node_modules', '.bin', 'tsx');
60+
const CASE_TIMEOUT_MS = 600_000;
61+
const SPAWN_TIMEOUT_MS = 240_000;
62+
63+
const APP_OBJECT = 'fx24120_thing';
64+
const DATABASE = join('data', 'main.db');
65+
/** The `[schema-drift]` line the SQL driver prints at boot: `[schema-drift] TABLE.COLUMN: …`. */
66+
const DRIFT_LINE = /\[schema-drift\] ([a-z0-9_]+\.[a-z0-9_]+):/g;
67+
68+
let dir: string;
69+
let port: number;
70+
71+
/**
72+
* The config, in the one shape `os build` accepts (`defineStack`). The project
73+
* resolves `@objectstack/spec` through a link to the workspace package
74+
* (`test/helpers/define-stack-fixture.ts` does the same, out of reach of this
75+
* `src` program's `rootDir`).
76+
*/
77+
function writeConfig(fields: Record<string, unknown>): void {
78+
const stack = {
79+
manifest: { id: 'com.example.fx24120', name: 'fx24120', version: '0.0.0', type: 'app', engines: { protocol: '^18' } },
80+
datasources: [{ name: 'qa_main', driver: 'sqlite', config: { filename: `./${DATABASE}` } }],
81+
datasourceMapping: [{ default: true, datasource: 'qa_main' }],
82+
objects: [{ name: APP_OBJECT, sharingModel: 'private', fields }],
83+
};
84+
writeFileSync(
85+
join(dir, 'objectstack.config.ts'),
86+
`import { defineStack } from '@objectstack/spec';\n\nexport default defineStack(${JSON.stringify(stack, null, 2)});\n`,
87+
);
88+
}
89+
90+
/**
91+
* The child's environment: this process's, minus the families
92+
* `test/helpers/serve-process.ts` `childEnv()` strips (its header says why),
93+
* and nothing that names a database, a home, an artifact or a telemetry sibling.
94+
*/
95+
function env(extra: Record<string, string> = {}): Record<string, string | undefined> {
96+
const inherited: Record<string, string | undefined> = {};
97+
for (const [key, value] of Object.entries(process.env)) {
98+
if (key === 'TEST' || key === 'VITEST' || key.startsWith('VITEST_') || key === 'NODE_PATH') continue;
99+
inherited[key] = value;
100+
}
101+
return {
102+
...inherited,
103+
OS_DATABASE_URL: undefined,
104+
DATABASE_URL: undefined,
105+
TURSO_DATABASE_URL: undefined,
106+
OS_DATABASE_DRIVER: undefined,
107+
OS_HOME: undefined,
108+
OS_ARTIFACT_PATH: undefined,
109+
OS_TELEMETRY_DB: '0',
110+
OS_CLOUD_URL: 'off',
111+
OS_TELEMETRY_DISABLED: '1',
112+
OS_AUTH_SECRET: 'os24120-declared-default-secret-at-least-32-characters',
113+
NO_COLOR: '1',
114+
...extra,
115+
};
116+
}
117+
118+
/** A port nothing listens on: the boot exits before it would listen, but `os dev` takes one. */
119+
async function freePort(): Promise<number> {
120+
return new Promise((done, fail) => {
121+
const server = createServer();
122+
server.once('error', fail);
123+
server.listen(0, '127.0.0.1', () => {
124+
const address = server.address();
125+
server.close(() => (address && typeof address === 'object' ? done(address.port) : fail(new Error('no port'))));
126+
});
127+
});
128+
}
129+
130+
/**
131+
* `os dev`, booted once and exited: its output. `--compile` because a config
132+
* newer than its artifact is otherwise served STALE (with a warning), and the
133+
* staged edit below is exactly such a config.
134+
*/
135+
function devBootOnce(): string {
136+
const r = spawnSync(
137+
TSX,
138+
[RUN_DEV, 'dev', '--port', String(port), '--compile', '--no-watch', '--no-restart', '--no-seed-admin'],
139+
{ cwd: dir, env: env({ OS_MIGRATE_AND_EXIT: '1' }), encoding: 'utf8', timeout: SPAWN_TIMEOUT_MS },
140+
);
141+
const output = `${r.stdout}\n${r.stderr}`;
142+
expect(r.status, `os dev did not boot and exit:\n${output.slice(-1500)}`).toBe(0);
143+
return output;
144+
}
145+
146+
/** One read-only `os migrate` door's `--json` document. */
147+
function migrateJson(args: string[]): any {
148+
const r = spawnSync(TSX, [RUN_DEV, 'migrate', ...args, '--json'], {
149+
cwd: dir, env: env(), encoding: 'utf8', timeout: SPAWN_TIMEOUT_MS,
150+
});
151+
expect(r.status, `os migrate ${args.join(' ')} failed:\n${`${r.stdout}\n${r.stderr}`.slice(-1500)}`).toBe(0);
152+
return JSON.parse(r.stdout);
153+
}
154+
155+
function tableCount(file: string): number {
156+
const db = new Database(file, { readonly: true });
157+
try {
158+
return (db.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").get() as { n: number }).n;
159+
} finally {
160+
db.close();
161+
}
162+
}
163+
164+
beforeAll(async () => {
165+
dir = mkdtempSync(join(tmpdir(), 'os-24120-declared-default-'));
166+
const specRoot = dirname(createRequire(import.meta.url).resolve('@objectstack/spec/package.json'));
167+
mkdirSync(join(dir, 'node_modules', '@objectstack'), { recursive: true });
168+
symlinkSync(specRoot, join(dir, 'node_modules', '@objectstack', 'spec'), 'dir');
169+
port = await freePort();
170+
});
171+
172+
afterAll(() => {
173+
if (dir) rmSync(dir, { recursive: true, force: true });
174+
});
175+
176+
describe('os migrate plan on a datasourceMapping-default declared datasource (#24120)', () => {
177+
it('examines the drift os dev reports there — the same set', () => {
178+
// Provision: the first boot creates every table in the declared datasource.
179+
writeConfig({ title: { type: 'text' }, extra: { type: 'text' } });
180+
devBootOnce();
181+
182+
// Stage: the app object stops declaring `extra`, and `sys_user` (a platform
183+
// object the same rule routes) gains a column no object declares.
184+
writeConfig({ title: { type: 'text' } });
185+
const db = new Database(join(dir, DATABASE));
186+
try { db.exec('ALTER TABLE sys_user ADD COLUMN fx24120_legacy TEXT'); } finally { db.close(); }
187+
188+
const reported = new Set([...devBootOnce().matchAll(DRIFT_LINE)].map((m) => m[1]));
189+
const plan = migrateJson(['plan']);
190+
const planned = new Set((plan.changes as Array<{ table: string; column?: string }>).map((c) => `${c.table}.${c.column}`));
191+
192+
expect([...reported].sort()).toEqual(expect.arrayContaining([`${APP_OBJECT}.extra`, 'sys_user.fx24120_legacy']));
193+
expect([...planned].sort()).toEqual([...reported].sort());
194+
expect(plan.databaseSource).toEqual({ kind: 'config-datasource', datasource: 'qa_main' });
195+
expect(plan.composition?.coverage?.unexaminedObjects, JSON.stringify(plan.composition?.coverage)).toBe(0);
196+
expect(plan.managedTables).toBeGreaterThan(0);
197+
expect(plan.unmanagedTables?.status, JSON.stringify(plan.unmanagedTables)).toBe('read');
198+
expect(plan.unmanagedTables.physicalTables).toBe(tableCount(join(dir, DATABASE)));
199+
200+
// The door an unmapped column's finding leads to filters the plan's own
201+
// findings over the same set: the platform object's column is read there,
202+
// not refused as "not in the set os migrate plan diffs".
203+
const unmapped = migrateJson(['unmapped-columns', '--object', 'sys_user']);
204+
expect((unmapped.columns as Array<{ column: string }>).map((c) => c.column)).toEqual(['fx24120_legacy']);
205+
}, CASE_TIMEOUT_MS);
206+
});

‎packages/cli/src/commands/migrate/plan.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
describeWithApplyLimits,
2020
describeDatabaseSource,
2121
describeTelemetryDatabase,
22+
distinctDatabaseDrivers,
2223
renderPlan,
2324
renderPendingSchemaWork,
2425
summarize,
@@ -185,6 +186,11 @@ export default class MigratePlan extends Command {
185186
// resolves — or an app declaring `requires: ['auth']` plans without
186187
// `sys_account`, and its retired `issuer` column is never a drop.
187188
composeServedPlatform: true,
189+
// [#24120] …and the datasources the deployment declares, connected as
190+
// `os serve` connects them: a `datasourceMapping` rule's objects are
191+
// planned in the datasource it routes them to, the declared default's
192+
// included. `os migrate apply` passes the same.
193+
composeDeclaredDatasources: true,
188194
});
189195
} catch (error: any) {
190196
if (flags.json) { await emitJson({ error: error.message, ...errorCodeFields(error) }, 0, { compact: true }); this.exit(1); }
@@ -221,8 +227,10 @@ export default class MigratePlan extends Command {
221227
// indistinguishable from "never looked at". See `../../utils/unmanaged-tables.ts`.
222228
const { normalizeRows } = await import('@objectstack/metadata-protocol');
223229
const unmanagedTables: UnmanagedTablesReport = await collectUnmanagedTables({
224-
// [#22580] Every database the drift above covers, the sibling included.
225-
drivers: stack.drivers,
230+
// [#22580] Every database the drift above covers, the sibling included —
231+
// [#24120] each read once, though a declared default and the primary
232+
// resolved from it are two connections to one file.
233+
drivers: distinctDatabaseDrivers(stack.drivers),
226234
declaredObjects: stack.allObjects(),
227235
composition: stack.composition,
228236
normalize: normalizeRows,

‎packages/cli/src/commands/migrate/unmapped-columns.ts‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ export default class MigrateUnmappedColumns extends Command {
297297
readOnlyProbe: true,
298298
composeHostStack: true,
299299
composeServedPlatform: true,
300+
// [#24120] …and the declared datasources the plan connects, so an
301+
// object a `datasourceMapping` rule routes to one — the declared
302+
// default's platform objects included — is in this set as in the plan's.
303+
composeDeclaredDatasources: true,
300304
});
301305
} catch (error: any) {
302306
if (flags.json) {
@@ -340,9 +344,10 @@ export default class MigrateUnmappedColumns extends Command {
340344
'objectql',
341345
) as { getDriverForObject?: (name: string) => unknown } | undefined;
342346
const bound = engine?.getDriverForObject?.(object);
343-
// [#22579] Any driver the plan diffs: the primary, or the `telemetry`
347+
// [#22579] Any driver the plan diffs: the primary, the `telemetry`
344348
// sibling a lifecycle-classed object lives in when the serving boot
345-
// keeps one. The read goes to that driver, and the document names it.
349+
// keeps one, or [#24120] the declared datasource a mapping rule routes
350+
// it to. The read goes to that driver, and the document names it.
346351
const objectDriver = stack.drivers.find((d) => d === bound) ?? null;
347352
const outside =
348353
declared.external != null
@@ -358,9 +363,9 @@ export default class MigrateUnmappedColumns extends Command {
358363
'for it, and that is unmeasured, not clean. Refusing rather than answering empty work.',
359364
);
360365
}
361-
// The sibling is the one other planned driver, and it is planned only
362-
// with its file: a driver that is not the primary has a database named.
363-
const database = objectDriver === stack.driver ? stack.dbLabel : stack.telemetryDatabase!;
366+
// Every planned driver has a database named (`SchemaStack.databaseOf`),
367+
// and `objectDriver` is one of them by the lookup above.
368+
const database = stack.databaseOf(objectDriver)!;
364369

365370
const reader = objectDriver as unknown as Partial<UnmappedColumnReader>;
366371
if (typeof reader.find !== 'function') {

0 commit comments

Comments
 (0)