Skip to content

Commit 93f3526

Browse files
fix(cli): the read-only migrate arms say they write no row or table, and that opening a SQLite file can still switch its journal mode (#23608)
Fixes #23562 Clause-②: no ## What changed The ownership ceremony's plan arm (`os migrate organization-ownership` without `--apply`) printed "The plan below writes nothing, but --apply will refuse until the database is free." beside a holder, and closed with "Plan written to FILE — nothing in the database was changed." Its boot is read-only, but it still opens the file through the SQL driver, and the driver's connect switches a SQLite file still on a rollback journal to WAL. Measured below: header bytes 18 and 19 go from 1,1 to 2,2 and the change counter moves; no row or table does. Both sentences now claim no row or table, followed by one shared note, `JOURNAL_MODE_SWITCH_NOTE`, exported beside `describeOccupancy` in `packages/cli/src/utils/sqlite-occupancy.ts`: > Opening a SQLite file can still switch its journal mode (to WAL by default), which changes its header only. The closing line carries the note only on a SQLite target: the occupancy probe answers `not_applicable` for PostgreSQL, MySQL and `:memory:`. The same sentence is corrected in `os migrate plan` (triage's fold-in) and in six sibling read-only arms (the four-condition fold-ins below). The driver's first-connect rule does not change: no `packages/drivers/driver-sql` path is touched. No exit code, `--json` document or refusal changes. ## Measurements (real CLI through `bin/run-dev.js`, at `9474bc6ea9`) Each run used a fresh SQLite file on a rollback journal, holding one `sys_organization` row. An idle better-sqlite3 connection in ANOTHER process held it open, which is the case the fd probe sees. Each command ran once. | command | header bytes 18,19 | change counter | exit | busy warning printed | |---|---|---|---|---| | `os migrate organization-ownership` (plan) | 1,1 to 2,2 | 2 to 3 | 0 | yes, the new text | | `os migrate plan` | 1,1 to 2,2 | 2 to 3 | 0 | yes, the new text | | `os migrate summary-nulls` (dry run) | 1,1 to 2,2 | 2 to 3 | 1 (the scratch project has no compiled artifact; the boot had already run) | yes, the new text | Measured with better-sqlite3 alone: - A switch to WAL and back moves only header bytes 18, 19, 24-27 and 92-95. After a clean close, no sidecar remains. - The `auto_vacuum` setter the driver sends to a legacy NONE file that holds tables moves no byte (md5 unchanged, under both WAL and a rollback journal). So the journal-mode switch is the only header change. - **(a) The call path:** plan arm, then `bootSchemaStack` (`utils/schema-migrate.ts`), `createStandaloneStack` (`@objectstack/runtime`), `DefaultDatasourcePlugin`, `SqlDriver.connect()`, `applyAutoVacuumIncremental()`, `applySqliteJournalMode()`. - **(b) `readOnlyProbe` acts only on an ABSENT file.** It sets `sqliteAbsentFile: 'empty-in-memory'` behind an `existsSync` guard in `standalone-stack.ts`, and `SqlDriver.knexConfigFor` redirects only a missing file. An existing file opens as it is, and `connect()` runs both pragmas. So `plan.ts:134`'s "nothing is written" was the same false sentence. It is folded in, and the table above measures it. - **(e) Where the driver reads `OS_DATABASE_SQLITE_JOURNAL_MODE`:** from `process.env` at connect. `bootSchemaStack` first loads the project's `.env*` into `process.env` through dotenv-flow, which never overrides an exported variable. So the arm reads its own shell and the project `.env`, and never the serving process's shell. - Measured: with `=delete` exported in the arm's shell, a WAL file is switched to a rollback journal (2,2 to 1,1). - Measured: with `=delete` only in the project `.env`, a rollback file is left alone (1,1 to 1,1). - This changes which sentence is true: a note saying only "switches to WAL" is false under the opt-out. So the note says "switch its journal mode (to WAL by default)". ## Fold-ins: the four conditions, one row each Every row is the warning a read-only arm prints beside a holder. Each arm boots `bootSchemaStack` with `readOnlyProbe` on that arm, which reaches the same `connect()` as (a). | site | ① same class | ② mechanical | ③ no other claim on the file | ④ same gate family | folded | |---|---|---|---|---|---| | `plan.ts:134` | yes, measured | one sentence + import | none open (the claim's read at `19c554ddc1`) | `packages/cli`, one derived list | yes, triage's fold-in | | `audit-metadata-bodies.ts:118` | yes, read | one sentence + import | none open | same | yes | | `comment-reactions.ts:139` | yes, read | one sentence + import | none open | same | yes | | `files-to-references.ts:195` | yes, read | one sentence + import | none open | same | yes | | `meta.ts:1442` | yes, read | one sentence + import | none open | same | yes | | `multi-value-columns.ts:397` | yes, read | one sentence + import | none open | same | yes | | `summary-nulls.ts:159` | yes, measured (table above) | one sentence + import | none open | same | yes | | `security-catalog-overlays.ts:198` | yes, read | yes | **no**: PR #23469 (open, draft) edits this file at `:290` | same | **no**, see Acceptance notes | ## Tests, round 0 (all on `86ce1100c3`, the head this PR opened with) - `pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 --reporter=verbose` over three files: `organization-ownership.journal-mode.integration.test.ts`, `organization-ownership.occupancy.integration.test.ts` and `utils/sqlite-occupancy.test.ts`. Result: `Test Files 3 passed (3)`, `Tests 21 passed (21)`, `VERDICT command-exit 0`. The new real-boot case took 5442 ms (shared box). - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: `Test Files 290 passed (290)`, `Tests 4324 passed (4324)`, `VERDICT command-exit 0`. The integration layer is declared to CI. The two integration files this PR touches ran by path, above. - `pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck`: exit 0. `check:test-typecheck` held its debt at 3 files, 28 errors, 6 signatures. `tsc --noEmit --listFiles -p tsconfig.json` puts both touched test files and `sqlite-occupancy.ts` in its 1564-file program. - **New pin, `organization-ownership.journal-mode.integration.test.ts`:** the real boot runs on a rollback-journal file. It asserts: - header bytes 1,1 before and 2,2 after; - rows unchanged; - the closing line says "no row or table in the database was changed." and carries `JOURNAL_MODE_SWITCH_NOTE`; - no line says "nothing in the database was changed". If a read-only boot ever stops switching the file, the header assertion goes red, and the note and this pin can both go. - **Extended pin, `organization-ownership.occupancy.integration.test.ts` (plan case):** the busy warning says "The plan below writes no row or table" and carries the note, and no line says "writes nothing". - **Not pinned:** the six siblings and `plan.ts`. No existing test asserted their old text. ## Ablation (one-shot, run through `scripts/ablation-replace.mjs` with HEAD restores, at `6a65958a88`) The fix was committed first. Each leg puts one old sentence back into `organization-ownership.ts` and runs both pin files: - **Leg 1, the closing line:** anchor hit once, blob `fa72f5c40052` to `b853ff316a93`. The new pin went RED: `Received: " ✓ Plan written to …/plan.json — nothing in the database was changed."`. The header assertion before it passed, so the file was switched under the old text too. `1 failed | 5 passed`. - **Leg 2, the busy warning:** blob `fa72f5c40052` to `9996f6490f4f`. The occupancy plan case went RED: `expected ' ⚠ /tmp/os-ownership-occupancy-…' to contain 'The plan below writes no row or table'`. `1 failed | 5 passed`. - **Restore, both legs:** blob equals HEAD (`fa72f5c40052`), and `git diff HEAD` is empty. The wrapping script's own trap proved it again. - The direction was red, as expected. ## Gates, round 0 (on `86ce1100c3`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 64 families from the merge base `19c554ddc`, over 12 paths and 235 changed lines. All 64 ran, each exit code captured before any pipe. - `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3 (PREREQUISITE NOT MET: missing `dist/`). After `turbo run build --filter=@objectstack/connector-slack`, both re-ran and exited 0. `check:i18n-coverage` reported `OK (14 config(s), 621 baselined untranslated string(s), none new)`. - `--ran` then printed `64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN`. - **Lint, by the narrowing:** `pnpm exec eslint --no-inline-config` over the 11 changed `.ts` files exited 0, with no "ignored" notice. `--print-config` shows 5 and 6 active rules on two of them. The repo-wide `pnpm lint` is CI's. - Every family CI runs is read from this PR's check-runs, not re-run here. ## Patch round 1 (head `cc578600b0`; the seat's rulings are PR comment 6184246424) The seat answered the three open questions: Q1 A (`security-catalog-overlays.ts` stays out; the seat files the remainder as its own card), Q2 A (fixed in this round), Q3 A (keep the text). This round makes four changes, and nothing else changes. 1. **`.changeset/23400-ownership-occupancy-gate.md:15`** (unreleased): "The plan (no `--apply`) writes nothing, so it still runs." now reads "The plan (no `--apply`) writes no row or table, so it still runs." The rest of that file is byte-identical: the committed file equals the HEAD copy with that one replacement, 3143 to 3151 bytes. 2. **`multi-value-columns.ts:334`, the help description**: "… and the database they would run against, and writes nothing." now reads "… and writes no row or table." This PR's changeset gains a FROM and TO line for it. The help text carries no note, so the changeset's note sentence now says "each warning". No test asserted the old description (`git grep` over the repo: only the source line). 3. **Developer comments in files this PR already touches**, one phrase each: - `organization-ownership.ts` header (:75): "The one thing it writes is the plan FILE" now reads "It writes no row or table; the one thing it writes is the plan FILE" (re-wrapped over the same three lines). - `files-to-references.ts:98`: "writes NOTHING" now reads "writes NO ROW". `:191`: "writes nothing" now reads "writes no row". - `plan.ts:55` and `:143`: "writes nothing" now reads "writes no row". - `audit-metadata-bodies.ts:57`: "(writes nothing)" now reads "(writes no row)". - `summary-nulls.ts:54`: "(writes nothing at all)" now reads "(writes no row at all)". - `meta.ts:1489`: the quoted report text "writes nothing" now reads "writes no row". - Left as written, because they are true: `plan.ts:65` ("nothing is written to disk", a never-started project opened in memory), and the `organization-ownership.ts` header's "no database file brought into existence". - `organization-ownership.ts:25-29` and `:579` (PR #23469's hunks) are untouched. 4. **Read only, no edit: `recorded-by.ts:158` and `resume.ts:172`.** Both print "Nothing was changed." after the same read-only boot through the same connect, so on a SQLite file still on a rollback journal both sentences are false the same way. The answer is in the Acceptance notes. **Tests on `cc578600b0`:** - `pnpm --filter @objectstack/cli build && pnpm --filter @objectstack/cli typecheck && vitest run` by path over the three pin files: `Test Files 3 passed (3)`, `Tests 21 passed (21)`, `VERDICT command-exit 0`. `check:test-typecheck` held its debt at 3 files, 28 errors, 6 signatures. The real-boot case took 5421 ms. - `vitest run --project unit --maxWorkers=2`: `Test Files 290 passed (290)`, `Tests 4324 passed (4324)`, `VERDICT command-exit 0`. Narrowed `eslint --no-inline-config` over the 11 changed `.ts` files exited 0. - No pin changed structure, so there is no ablation this round. **Gates on `cc578600b0`:** - `dispatch-gates --commands --repo objectstack-ai/objectstack` derived the same 64 families as round 0, from 13 paths and 260 changed lines. All 64 ran, each exit captured before any pipe. 61 exited 0. `check:dual-build-cjs-loads` and `check:i18n-coverage` first exited 3 (missing `dist/` in the fresh worktree); both re-ran after the later gates had built it and exited 0. `--ran` printed `64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN`. - **One gate is red, by design: `node scripts/check-empty-changeset.mjs --base origin/main` exits 1.** It reports: "This PR changes a changeset it did not add: .changeset/23400-ownership-occupancy-gate.md". This is the gate's DELIBERATE CORRECTION class, not a filename collision. The PR's own changeset keeps its issue-scoped name, and the 23400 note is corrected because its plan sentence was false. The gate's own remedy for this class is to say so on the PR and get it confirmed. The confirmation is the seat's ruling, PR comment 6184246424 (Q2: A). The gate stays red until a person takes that decision, and no `skip-changeset` label is applied. - The tool printed a STALE TREE notice: `scripts/measure-test-shard-timings.mjs` changed on `origin/main` (`fc1a55a212`) after the base. - `git merge-tree --write-tree HEAD origin/main` is clean (exit 0, tree `84fc586f90`), so `origin/main` was not merged in, by the order. Since the base, main has touched nothing in `packages/cli`, `packages/runtime`, `packages/drivers` or the 23400 changeset. ## Acceptance notes - **`os migrate recorded-by` and `os migrate resume` (read, not measured, not edited).** Both boot the same read-only stack and print "Nothing was changed.", so on a SQLite file still on a rollback journal both sentences are false in the same way: - `recorded-by.ts:105-110`: the dry run calls `bootSchemaStack({ …, deferSchemaDdl: true, readOnlyProbe: true })` (`:109`, when no `--apply` is given). - `resume.ts:114-119`: the list mode calls the same (`:118`, when no `--run` is given). - That boot takes the call path measured above: `createStandaloneStack`, `SqlDriver.connect()`, `applySqliteJournalMode()`. - Each then prints `printInfo("Nothing was changed. …")`: `recorded-by.ts:158` (pending sentinel rows exist) and `resume.ts:172` (interrupted runs exist). - Neither command probes occupancy, so neither knows whether its target is a SQLite file, and the sentence prints on every dialect. - The seat files these with the `security-catalog-overlays.ts:198` remainder. - **`security-catalog-overlays.ts:198`** ("The preview below writes nothing.") stays out by the seat's Q1 ruling: PR #23469 holds the file. The seat files it as its own card, ordered after PR #23469, using the exported `JOURNAL_MODE_SWITCH_NOTE`. - **Not built (seat's Q3 ruling: keep the text):** a read-only boot that keeps the file's journal mode by declaring the driver's `sqliteJournalMode` from the header it finds. - **`organization-ownership.ts` imports the constant on its own line** (24), beside the `describeOccupancy` import, because the claim holds line 23 off-limits. This body was updated for patch round 1 by session `session_01Q9PZ5Pzqcus4woVvTZwWNU` through the fleet relay; the original was written by the same session at PR creation. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent af25552 commit 93f3526

13 files changed

Lines changed: 227 additions & 33 deletions

‎.changeset/23400-ownership-occupancy-gate.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Clause-②: no (narrowing)
1212

1313
**FROM.** `os migrate organization-ownership --apply` opened a SQLite database that another process held open (a running `os dev` or `os serve`, or a runtime's pooled connection) without checking for it, and ran the ceremony beside that process. `os migrate apply` and the other data migrations probe the database first; this command did not. One measured run of the plan and then `--apply` beside a runtime holding a connection left that runtime writing to a deleted `-wal` file, and a row it wrote afterwards never reached the database.
1414

15-
**TO.** Before it boots the stack, the command probes the database the way its siblings do. Beside a holder, `--apply` is refused with exit 1: the ceremony boots nothing and writes nothing (the probe itself opens the file only briefly, to check). The check runs first, so a busy `--apply` that also lacks its plan file or its backup acknowledgement answers reason `database-busy` rather than `plan-file-required` or `backup-unacknowledged`; it is refused either way. The text output names the file and, when the probe can tell, the process holding it, then the remedy: stop the process using it and re-run. With `--json` the refusal uses this command's own envelope: `{ "error": "apply_refused", "reason": "database-busy", "database", "signal", "detail", "hint" }`. There is no `--force`: the ceremony has no flag that skips its reviewed plan or its backup either, and its refusal never names one. The plan (no `--apply`) writes nothing, so it still runs. Its text output warns that `--apply` will refuse until the database is free, and its `--json` document is unchanged.
15+
**TO.** Before it boots the stack, the command probes the database the way its siblings do. Beside a holder, `--apply` is refused with exit 1: the ceremony boots nothing and writes nothing (the probe itself opens the file only briefly, to check). The check runs first, so a busy `--apply` that also lacks its plan file or its backup acknowledgement answers reason `database-busy` rather than `plan-file-required` or `backup-unacknowledged`; it is refused either way. The text output names the file and, when the probe can tell, the process holding it, then the remedy: stop the process using it and re-run. With `--json` the refusal uses this command's own envelope: `{ "error": "apply_refused", "reason": "database-busy", "database", "signal", "detail", "hint" }`. There is no `--force`: the ceremony has no flag that skips its reviewed plan or its backup either, and its refusal never names one. The plan (no `--apply`) writes no row or table, so it still runs. Its text output warns that `--apply` will refuse until the database is free, and its `--json` document is unchanged.
1616

1717
**Unchanged.** A PostgreSQL, MySQL or remote libSQL target is not probed. A probe that cannot tell (for example, the database file cannot be read) warns in the text output and proceeds; under `--json` it is silent. The probe reads the project's `.env` files, as the run itself does.
1818

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): the read-only `os migrate` arms say they write no row or table, and that opening a SQLite file can still switch its journal mode, instead of saying they write nothing
6+
7+
**What was wrong.** These arms boot read-only: no DDL, no seed, SELECT statements only. But the boot still opens the database through the SQL driver, and the driver's connect switches a SQLite file to the journal mode it asks for: WAL, unless `OS_DATABASE_SQLITE_JOURNAL_MODE=delete` is set in the command's own environment or the project's `.env` (then a WAL file is switched back to a rollback journal). On a file still on a rollback journal, a plan or a dry run therefore rewrote the file's header (the journal-mode bytes and the change counter) while saying "writes nothing" or "nothing in the database was changed". No row or table changed, and nothing was lost.
8+
9+
**What changed.** Each sentence below now claims no row or table, and each warning is followed by one shared note: "Opening a SQLite file can still switch its journal mode (to WAL by default), which changes its header only."
10+
11+
- `os migrate organization-ownership` (the plan), beside a process holding the file. FROM: "The plan below writes nothing, but --apply will refuse until the database is free." This sentence has not been published: it arrives in this same release, with the occupancy gate the ceremony's `--apply` now takes. TO: "The plan below writes no row or table, but --apply will refuse until the database is free." and the note.
12+
- Its closing line. FROM: "Plan written to FILE — nothing in the database was changed." TO: "Plan written to FILE — no row or table in the database was changed." On a SQLite target the note follows; on PostgreSQL or MySQL the line ends there.
13+
- `os migrate plan`, beside a holder. FROM: "The plan below is still accurate — nothing is written — but "os migrate apply" will refuse until it is free (or you pass --force)." TO: "The plan below is still accurate — it writes no row or table — but …", the rest unchanged, and the note.
14+
- The dry runs of `os migrate audit-metadata-bodies`, `comment-reactions`, `files-to-references` and `summary-nulls`, beside a holder. FROM: "The dry run below writes nothing, but its counts may shift while that process is running." TO: "The dry run below writes no row or table, but its counts may shift while that process is running." and the note.
15+
- The dry run of `os migrate multi-value-columns`, beside a holder. FROM: "The dry run below writes nothing." TO: "The dry run below writes no row or table." and the note.
16+
- The help description of `os migrate multi-value-columns` (`os migrate multi-value-columns --help`). FROM: "… Dry-run by default: prints the exact statements and the database they would run against, and writes nothing." TO: "… and writes no row or table." The help text carries no note.
17+
- The preview of `os migrate meta --stored`, beside a holder. FROM: "The preview below writes nothing, but a live process may be saving metadata while it runs." TO: "The preview below writes no row or table, but a live process may be saving metadata while it runs." and the note.
18+
19+
**Unchanged.** No exit code, `--json` document or refusal changes; the warnings are still printed in the text output only. What the driver does on connect is unchanged: once a file is in the mode the driver asks for, a read-only run leaves it byte-identical.

‎packages/cli/src/commands/migrate/audit-metadata-bodies.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '../../utils/format.js';
1919
import { bootSchemaStack } from '../../utils/schema-migrate.js';
2020
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
21-
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
21+
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
2222
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2323
import { absentTableReads } from '../../utils/absent-table-reads.js';
2424

@@ -54,7 +54,7 @@ async function confirm(question: string): Promise<boolean> {
5454
* withholds the hashes in those notes, in the same pass. Operators run it once
5555
* after upgrading, dry run first.
5656
*
57-
* Dry run by default (writes nothing), `--apply` to rewrite. Idempotent: a
57+
* Dry run by default (writes no row), `--apply` to rewrite. Idempotent: a
5858
* second run finds nothing — a redacted copy has no credential and no hash
5959
* left — so re-running and reading a clean report is the verification. No
6060
* `sys_migration` flag is recorded: nothing gates irreversible behaviour on this
@@ -115,7 +115,7 @@ export default class MigrateAuditMetadataBodies extends Command {
115115
if (occupancy.status === 'busy' && !flags.json) {
116116
printWarning(apply
117117
? `--force: ${describeOccupancy(occupancy)} Rewriting anyway — the live process may write rows mid-walk.`
118-
: `${describeOccupancy(occupancy)} The dry run below writes nothing, but its counts may shift while that process is running.`);
118+
: `${describeOccupancy(occupancy)} The dry run below writes no row or table, but its counts may shift while that process is running. ${JOURNAL_MODE_SWITCH_NOTE}`);
119119
}
120120
if (occupancy.status === 'unknown' && !flags.json) {
121121
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);

‎packages/cli/src/commands/migrate/comment-reactions.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '../../utils/format.js';
1818
import { bootSchemaStack } from '../../utils/schema-migrate.js';
1919
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
20-
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
20+
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
2121
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2222
import { absentTableReads } from '../../utils/absent-table-reads.js';
2323
import type { MigrationFlagEngine } from '@objectstack/platform-objects/system';
@@ -136,7 +136,7 @@ export default class MigrateCommentReactions extends Command {
136136
if (occupancy.status === 'busy' && !flags.json) {
137137
printWarning(apply
138138
? `--force: ${describeOccupancy(occupancy)} Converting anyway — the live process may write rows mid-walk.`
139-
: `${describeOccupancy(occupancy)} The dry run below writes nothing, but its counts may shift while that process is running.`);
139+
: `${describeOccupancy(occupancy)} The dry run below writes no row or table, but its counts may shift while that process is running. ${JOURNAL_MODE_SWITCH_NOTE}`);
140140
}
141141
if (occupancy.status === 'unknown' && !flags.json) {
142142
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);

‎packages/cli/src/commands/migrate/files-to-references.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import { StandardErrorCode } from '@objectstack/spec/api';
2020
import { bootSchemaStack } from '../../utils/schema-migrate.js';
2121
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
22-
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
22+
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
2323
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
2424
import { isNarrowedRun, narrowedFlagNote, refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
2525
import {
@@ -95,7 +95,7 @@ async function confirm(question: string): Promise<boolean> {
9595
* platform version) is what later opens released-file collection (#3459) and
9696
* strict media value-shape enforcement (#3438) on this deployment.
9797
*
98-
* Dry run by default, and a dry run writes NOTHING — not conversions, not the
98+
* Dry run by default, and a dry run writes NO ROW — not conversions, not the
9999
* flag. Not run / not passed → files keep being retained forever: storage
100100
* cost, zero data loss.
101101
*
@@ -188,11 +188,11 @@ export default class MigrateFilesToReferences extends Command {
188188
return;
189189
}
190190
if (occupancy.status === 'busy' && !flags.json) {
191-
// A dry run writes nothing, so it only ever warns — but it warns, because
191+
// A dry run writes no row, so it only ever warns — but it warns, because
192192
// the numbers it reports are a moving target while another process writes.
193193
printWarning(apply
194194
? `--force: ${describeOccupancy(occupancy)} Converting anyway — the live process may write records mid-scan.`
195-
: `${describeOccupancy(occupancy)} The dry run below writes nothing, but its counts may shift while that process is running.`);
195+
: `${describeOccupancy(occupancy)} The dry run below writes no row or table, but its counts may shift while that process is running. ${JOURNAL_MODE_SWITCH_NOTE}`);
196196
}
197197
if (occupancy.status === 'unknown' && !flags.json) {
198198
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js
4040
import { absentTableReads } from '../../utils/absent-table-reads.js';
4141
import type { StoredMigrationReport } from '@objectstack/metadata-protocol';
4242
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
43-
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
43+
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
4444
import { checkProtocolCompat, type ProtocolHandshakeManifest } from '@objectstack/metadata-core';
4545
import {
4646
planAuthoredSourceWrite,
@@ -1439,8 +1439,8 @@ export default class MigrateMeta extends Command {
14391439
if (occupancy.status === 'busy' && !flags.json) {
14401440
printWarning(apply
14411441
? `--force: ${describeOccupancy(occupancy)} Rewriting anyway — the live process may save metadata mid-run.`
1442-
: `${describeOccupancy(occupancy)} The preview below writes nothing, but a live process may `
1443-
+ 'be saving metadata while it runs.');
1442+
: `${describeOccupancy(occupancy)} The preview below writes no row or table, but a live process may `
1443+
+ `be saving metadata while it runs. ${JOURNAL_MODE_SWITCH_NOTE}`);
14441444
}
14451445
if (occupancy.status === 'unknown' && !flags.json) {
14461446
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);
@@ -1486,7 +1486,7 @@ export default class MigrateMeta extends Command {
14861486
// takes. A plain boot runs schema sync and the artifact's inline seed
14871487
// loader, and the seed upserts every seeded row of the app's tables
14881488
// (`updated_at` bumped, `organization_id` stamped, relative-date values
1489-
// re-evaluated) before the report says "writes nothing". `deferSchemaDdl`
1489+
// re-evaluated) before the report says "writes no row". `deferSchemaDdl`
14901490
// holds the DDL back and suppresses the seed; `readOnlyProbe` keeps a
14911491
// missing sqlite file from being created. `--apply` keeps the plain boot:
14921492
// it is the writing mode, and its behaviour is unchanged.

‎packages/cli/src/commands/migrate/multi-value-columns.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '../../utils/format.js';
1818
import { bootSchemaStack } from '../../utils/schema-migrate.js';
1919
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
20-
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
20+
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
2121
import type { ManagedDriftEntry } from '@objectstack/driver-sql';
2222
import type { IObjectQLEngine } from '@objectstack/spec/contracts';
2323

@@ -331,7 +331,7 @@ export default class MigrateMultiValueColumns extends Command {
331331
static override description =
332332
'Migrate a stale varchar/text column to json where the field declares multiple: true; boot and ' +
333333
'os migrate plan only report such a column and never alter it unattended. ' +
334-
'Dry-run by default: prints the exact statements and the database they would run against, and writes nothing.';
334+
'Dry-run by default: prints the exact statements and the database they would run against, and writes no row or table.';
335335

336336
static override examples = [
337337
'$ os migrate multi-value-columns',
@@ -394,7 +394,7 @@ export default class MigrateMultiValueColumns extends Command {
394394
if (occupancy.status === 'busy' && !flags.json) {
395395
printWarning(apply
396396
? `--force: ${describeOccupancy(occupancy)} Altering the column anyway — the live process may write rows mid-migration.`
397-
: `${describeOccupancy(occupancy)} The dry run below writes nothing.`);
397+
: `${describeOccupancy(occupancy)} The dry run below writes no row or table. ${JOURNAL_MODE_SWITCH_NOTE}`);
398398
}
399399

400400
if (apply && !flags.yes) {

0 commit comments

Comments
 (0)