Skip to content

Commit ebfe658

Browse files
docs(cli): say what a read-only boot's first connection writes to a SQLite file (#21779)
Fixes #21745 Clause-②: no. Documentation is made true about existing behaviour; nothing is published. Docs only: two files under `content/docs/`, which no package ships. No code change, no behaviour change, no changeset. ## What changed Triage graded #21745 option B: make the reference true, and leave the one-rule-for-every-connect ruling closed. `content/docs/deployment/cli.mdx` now says it once, in the read-only-boot paragraph under **Data migrations**: - the commands write no row and no schema; - on a SQLite file no ObjectStack process has opened before, the first connection converts its journal to WAL, as every ObjectStack connection does. That changes the file's header and no row or table; - after that, a read-only run leaves the file byte-identical. Each other "writes nothing" sentence about a command that opens the database now says only what is true on its own ("writes no row", "no row and no schema"). The prose sites (secret orphans, plan/apply, duplicates) link to that paragraph. `content/docs/deployment/seed-tenancy-repair.mdx` made the same claim about `os migrate duplicates`, and it gets the same qualification. The heading "Nothing is written before you confirm" is now "Nothing is applied before you confirm". No page under `content/docs` links to the old anchor. ## Measured Base: `origin/main` `d7fff21736`. Every run used the built CLI binary `packages/cli/bin/run.js`, launched as a child process. **The fixtures.** - A database that `os serve` left behind for a one-object app with an inline seed: 35 tables, WAL, auto_vacuum INCREMENTAL. Called "configured" below. - Legacy copies of it, made with raw better-sqlite3 (`VACUUM INTO`, then `PRAGMA journal_mode = DELETE`), so no ObjectStack process ever opened them: - auto_vacuum NONE, the shape another tool leaves; - auto_vacuum INCREMENTAL, the shape of a pre-WAL ObjectStack file; - auto_vacuum FULL, as a control. - Header bytes were read with `fs` only, never through a SQLite connection. **A1.** `os migrate duplicates --database-url file:X` on the NONE copy: | | journal | [18] | [19] | [27] | [95] | md5 | |---|---|---|---|---|---|---| | before | delete | 1 | 1 | 1 | 1 | `71cdd04b096d` | | after run 1 | wal | 2 | 2 | 2 | 2 | `02bf2c53ba03` | | after run 2 | wal | 2 | 2 | 2 | 2 | `02bf2c53ba03` | Run 1 changed 4 bytes in the file, all in the header (offsets 18, 19, 27 and 95). Run 2 changed none. No `-wal`, `-shm` or `-journal` file was left after either run. **A2.** The no-write mode of every `bootSchemaStack` caller ran twice, each time on a fresh copy of each fixture. The callers are the family that `schema-migrate.one-shot-family.integration.test.ts` reads off the source. - The modes: `migrate` `duplicates`, `plan`, `multi-value-columns`, `unmapped-columns`, `files-to-references`, `value-shapes`, `summary-nulls`, `meta --stored`, `recorded-by`, `resume`, `audit-metadata-bodies` and `account-issuer`; `secret orphans`; `secret rewrap`; `storage orphans`; `meta resync`. | fixture | run 1 | run 2 | |---|---|---| | legacy NONE (16 modes) | 16 of 16 converted: header offsets 18, 19, 27 and 95 only, 4 bytes each | 16 of 16 unchanged: 0 bytes, same md5 | | legacy INCREMENTAL (16 modes) | 16 of 16 converted, the same 4 header offsets | 16 of 16 unchanged: 0 bytes | | legacy FULL (`duplicates`, `plan`) | converted, and auto_vacuum went FULL to INCREMENTAL (offset 67): 5 header bytes | 0 bytes | | configured (16 modes) | 0 bytes | 0 bytes | Every run exited 0 and printed parseable JSON. **Other modes.** - `os migrate apply` answered `n` at a real `[y/N]` prompt (a pty via `script`), against a release one field and one object ahead: "Aborted — no changes made.", and the legacy file converted all the same (offsets 18, 19, 27 and 95). The configured file did not change. - Re-runs of write modes: - `secret rewrap --apply` and `summary-nulls --apply` changed 0 bytes on their second run (legacy and configured alike). - `value-shapes --apply` rewrites only its flag row, `sys_migration` `adr-0104-value-shapes`. A row diff of both runs shows nothing else. ### The sites, one row each (line numbers are `d7fff21736`'s) | site | command | converts? | evidence | edit | |---|---|---|---|---| | `cli.mdx` `skip-seed-data` gloss (old :308) | a `bootSchemaStack` boot (`skipSeedData: true` on every one) | yes | A2 | "writes nothing" to "runs no seed" | | :544, :547, :555 | `secret orphans` report | yes | A2 | "writes no row"; pointer to Data migrations; code comment "writes no rows" | | :601, :606 | `secret rewrap` dry run | yes | A2 | "writes no row"; code comment | | :631 "a finished run writes nothing" | `secret rewrap --apply` re-run | no: the file was already opened by the first run | 0 bytes on the second `--apply` run | kept | | :853, :859 "(no changes applied)" | `plan` | yes, but these sentences are about drift changes | A2 | kept | | :868 heading, :870, :888 | `plan` / `apply` boot, answered `n` | yes | A2 plus the pty `n` run | heading reworded; "writes no row and no schema"; "every table and row exactly as it was" plus pointer | | :933 occupancy table | `plan` | yes | A2 | "writes no row and no schema either way" | | :1005 | `multi-value-columns` dry run | yes | A2 | "writes no row and no schema at all" | | :1078–:1081 | `unmapped-columns` | yes | A2 | "writes no row and no schema" | | :1114 | `duplicates` (table row) | yes | A1 | "writes no row and no schema" | | :1116–:1135 | the shared read-only-boot paragraph | yes | A2 | lead sentence "no row and no schema"; the one qualification paragraph added after it | | :1150, :1203, :1255, :1349 | dry run / scan / preview code comments | yes | A2 | "writes no rows" | | :1185, :1239 | `files-to-references` / `value-shapes` dry runs | yes | A2 | "writes **no row**" | | :1215 "`--apply`'s only write is the flag row" | `value-shapes --apply` | yes, plus the flag row | the write-mode legs above | "the only row `--apply` writes is the flag itself" | | :1290 "a second run … writes nothing" | `summary-nulls --apply` re-run | no: the file was already opened by the first run | 0 bytes on the second run | kept | | :1438 SDK `migrateStored()` | runs inside the serving process, on its existing connection | no new connection | route doc: it uses the server's live engine | kept | | :1451, :1473–:1476 | `duplicates` | yes | A1 | "writes no row and no schema under any flag"; "changes no data in production" plus the WAL sentence and pointer | | :1539 "boots read-only and repairs nothing" | `duplicates` | the term the qualified paragraph now defines | — | kept | | :739 `os validate`, :1595/:1616/:1711/:1662/:1729 `os generate`, :1806/:1820 `os lint --fix` | open no SQLite file | no | `git grep` of each command for `bootSchemaStack`, `SqlDriver` and `better-sqlite3`: 0 imports | kept | | `seed-tenancy-repair.mdx:311` | `duplicates` | yes | A1 | "writes no row"; "its own queries are `SELECT`s only"; WAL sentence plus link | **A4, other pages.** I ran `git grep` over `content/docs` (excluding `releases/` and `references/`) for "writes nothing", "read-only boot", "byte-identical", "changes nothing", and for every family command name. Hits with no edit: - `seed-tenancy-repair.mdx:123` is about the log, and `:251` is about a later serving boot's repair rows. - `ui/translations.mdx:335`: `os i18n extract --check`. `commands/i18n/*.ts` has 0 hits for SQLite or boot imports. - `automation/flows.mdx:1445`: "writes nothing for it" refers to the decision nodes' rows. - `protocol/kernel/http-protocol.mdx:1241`, `ui/actions.mdx:261` and `concepts/metadata-lifecycle.mdx:112` are unrelated surfaces. ## Acceptance notes - **Kept out on purpose: the journal opt-out.** `OS_DATABASE_SQLITE_JOURNAL_MODE=delete` makes the first run on a rollback-journal file change 0 bytes (measured on the NONE and INCREMENTAL copies). The reference does not offer it for read-only runs, because on a file already in WAL the same setting converts the file back to `delete`. That is a write, to a served deployment's file. - **Same phenomenon in runtime text.** The CLI's decline line "Aborted — no changes made." prints after the conversion on a never-opened file. That text speaks of the migration's changes. This card rules out any code change. Noted, not filed; no follow-up PR is planned. - **"After that" holds beyond the legacy files the card named.** On an auto_vacuum FULL legacy file, the first connection also flips auto_vacuum to INCREMENTAL. That is a header change too, and the second run changes 0 bytes. The new paragraph says "the file's header changes" so that this case stays true. ## Verification (head `fcc13878fa`) - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 41 families at `fcc13878fa`. All 41 ran: 41 exited 0. - The first `check:skill-examples` run exited 3 (PREREQUISITE NOT MET: `client-react` was not built). It exited 0 after `pnpm --filter @objectstack/client build && pnpm --filter @objectstack/client-react build`. - `--ran` reconciliation: "41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED". - `check:doc-anchors`: "428 internal #fragment link(s) across 414 source file(s) all resolve". Two one-time ablations through `scripts/ablation-replace.mjs`: - the new cross-page link `/docs/deployment/cli#data-migrations` mutated: red, naming `seed-tenancy-repair.mdx:316`; - a new same-page `#data-migrations` link mutated: red, naming `cli.mdx:549`. - Both were restored, with the blob equal to HEAD (`24ba2ab3876e`, `385558ab3097`) and `git diff HEAD` empty. - `pnpm lint`: eslint's population is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` (`eslint.config.mjs:971`). `eslint --format json` on the 2 changed files reports 2 files, each "File ignored because no matching configuration was supplied". So no file in the lint population changed, and an `.mdx` edit cannot move a linted file's verdict. - No package changed, so no package build or test is owed. - NOT MEASURED: the docs site build and the lychee link check (`Check Documentation Links`). Reason: CI runs them over the whole site, and lychee is not installed in this container. --- _Generated by [Claude Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent d7fff21 commit ebfe658

2 files changed

Lines changed: 54 additions & 35 deletions

File tree

‎content/docs/deployment/cli.mdx‎

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ and a source that finished by throwing may record no counts at all.
305305
`suppressed` is non-empty when this boot registered a seed source and
306306
deliberately never ran it — `multi-tenant-replay` (rows are written per
307307
organization on `sys_organization` insert) or `skip-seed-data` (a planning boot
308-
that writes nothing). Those sources never settle and no further signal is
308+
that runs no seed). Those sources never settle and no further signal is
309309
coming for them, which is exactly why the message is sent anyway with the reason
310310
attached: a consumer that waited for *every* source to finish would wait
311311
forever.
@@ -541,18 +541,19 @@ shape they accept. See [Source vs Artifact](#source-vs-artifact) below.
541541
542542
Reports the `sys_secret` rows no producer references any more, and — only behind
543543
`--delete` — removes the ones it can prove are the settings subsystem's to remove.
544-
**Report-only by default: without `--delete` it writes nothing and deletes nothing.**
544+
**Report-only by default: without `--delete` it writes no row and deletes nothing.**
545545
It is never run for you; nothing on any boot or upgrade path invokes it.
546546
547547
The report boots your app read-only: no schema change, no seed rows, and a SQLite file
548-
that does not exist is not created. Pointed at a database that lacks `sys_secret` (one
549-
that was never booted, or the wrong `--database-url`), it does not read the table, since
550-
a table that does not exist holds no row: it reports nothing to act on, names the
551-
tables it did not read, and exits 0. Any other read it cannot make still refuses and
552-
exits 1 (under `--json`: `"error": "scan_failed"`).
548+
that does not exist is not created. Its first connection can still change a SQLite
549+
file's header; [Data migrations](#data-migrations) says when. Pointed at a database
550+
that lacks `sys_secret` (one that was never booted, or the wrong `--database-url`), it
551+
does not read the table, since a table that does not exist holds no row: it reports
552+
nothing to act on, names the tables it did not read, and exits 0. Any other read it
553+
cannot make still refuses and exits 1 (under `--json`: `"error": "scan_failed"`).
553554
554555
```bash
555-
os secret orphans # report (writes nothing)
556+
os secret orphans # report (writes no rows)
556557
os secret orphans --json # the same report, machine-readable
557558
os secret orphans --no-declared-datasources # state that this host declares none
558559
os secret orphans --delete --export ./secrets-backup.json --no-declared-datasources
@@ -599,11 +600,11 @@ Re-wraps the `sys_secret` ciphertext sealed before secrets were bound to the pro
599600
that wrote them, so it carries the current binding (ADR-0128). Each row is re-sealed
600601
under the scope of the producer whose holder references it: a setting, an object's
601602
`secret` field, or a datasource credential. **A dry run by default: without `--apply`
602-
it writes nothing.** It is never run for you; nothing on any boot or upgrade path
603+
it writes no row.** It is never run for you; nothing on any boot or upgrade path
603604
invokes it.
604605
605606
```bash
606-
os secret rewrap --no-declared-datasources # dry run (writes nothing)
607+
os secret rewrap --no-declared-datasources # dry run (writes no rows)
607608
os secret rewrap --json --no-declared-datasources # the same, machine-readable
608609
os secret rewrap --declared-datasources ./datasources.json
609610
os secret rewrap --apply --no-declared-datasources # write the re-wrapped rows
@@ -865,10 +866,10 @@ os migrate plan --json # Machine-readable output
865866
os migrate unmapped-columns --object contact --json # A retired field's stored values, keyed by record id (read-only)
866867
```
867868
868-
#### Nothing is written before you confirm
869+
#### Nothing is applied before you confirm
869870
870-
Both commands boot your app to read its metadata. That boot no longer touches
871-
the target database: the additive schema sync (create missing tables, add
871+
Both commands boot your app to read its metadata. That boot writes no row and no
872+
schema to the target database: the additive schema sync (create missing tables, add
872873
missing columns) and the artifact's inline seed data are **deferred**, not
873874
performed. So `plan` really is a dry run, and everything `apply` is about to do
874875
— additive work included — is on screen before the `[y/N]` prompt:
@@ -885,7 +886,9 @@ performed. So `plan` really is a dry run, and everything `apply` is about to do
885886
✓ crm_contact.email [relax_not_null]
886887
```
887888
888-
Answering `n` leaves the database exactly as it was.
889+
Answering `n` leaves every table and row exactly as it was. The boot's first
890+
connection can still change a SQLite file's header; [Data migrations](#data-migrations)
891+
says when.
889892
890893
The two upper sections differ in a way worth reading carefully. **New** is
891894
purely additive — it creates tables and columns and never touches a row. **In
@@ -930,7 +933,7 @@ written.
930933
931934
| Command | If the database is in use |
932935
|---------|---------------------------|
933-
| `os migrate plan` | Warns and continues — a plan writes nothing either way |
936+
| `os migrate plan` | Warns and continues — a plan writes no row and no schema either way |
934937
| `os migrate apply` | **Refuses** (exit 1, `error: database_busy` under `--json`). Stop the other process, or pass `--force` |
935938
| `os migrate files-to-references --apply` | **Refuses** likewise — it rewrites rows, so a concurrent writer is at least as dangerous |
936939
| `os migrate meta --stored --apply` | **Refuses** likewise — it rewrites `sys_metadata` rows, and a live process saving metadata is exactly the collision |
@@ -1002,8 +1005,8 @@ os migrate multi-value-columns --table crm_case # Restrict to one physical
10021005
os migrate multi-value-columns --database-url postgres://…
10031006
```
10041007
1005-
**Take a backup first.** The dry run is the default and writes nothing at all —
1006-
not a probe, not a temporary table — so run it, read the statements it prints,
1008+
**Take a backup first.** The dry run is the default and writes no row and no schema
1009+
at all — not a probe, not a temporary table — so run it, read the statements it prints,
10071010
and only then re-run with `--apply`.
10081011
10091012
The statement is the one the drift finding itself prints, per dialect, and the
@@ -1077,8 +1080,8 @@ them into the declared fields with your own script, then run
10771080
own `id`, `created_at` and `updated_at`.
10781081
- **Operator-only and read-only.** It runs under the database credentials you
10791082
pass, and covers every organization's rows. There is no REST route or API
1080-
flag behind it. It boots the way `plan` does, so it writes nothing, and it
1081-
drops nothing.
1083+
flag behind it. It boots the way `plan` does, so it writes no row and no schema,
1084+
and it drops nothing.
10821085
- **Values as stored.** An unmapped column has no declared type, so each value is
10831086
emitted as the database client returns it, with no field-type decoding: a
10841087
retired `json` field on SQLite reads as its stored text, a retired `boolean`
@@ -1111,9 +1114,9 @@ where the data lives.
11111114
| `os migrate value-shapes` | Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean |
11121115
| `os migrate summary-nulls` | Backfill roll-up `count` / `sum` columns still stored as `NULL` on parent rows created before the insert-time seed. Repairs values; no flag, nothing depends on it having run |
11131116
| `os migrate meta --stored` | Replay the metadata conversion chain over this deployment's `sys_metadata` rows and rewrite the ones still carrying a pre-protocol shape. Hygiene, not a gate — nothing depends on it having run |
1114-
| `os migrate duplicates` | Report business identifiers already minted twice across the organization partitions, and the rows blocking the boot-time NULL-safe index tightenings — a read-only inventory as JSON on stdout. Renumbers nothing and writes nothing at all; run it before the boot-time tenancy repair, which overwrites part of the evidence |
1117+
| `os migrate duplicates` | Report business identifiers already minted twice across the organization partitions, and the rows blocking the boot-time NULL-safe index tightenings — a read-only inventory as JSON on stdout. Renumbers nothing and writes no row and no schema; run it before the boot-time tenancy repair, which overwrites part of the evidence |
11151118
1116-
**The boot itself writes nothing you did not ask for.** Each of these commands boots
1119+
**The boot itself writes no row and no schema you did not ask for.** Each of these commands boots
11171120
your app to read its metadata. Without `--apply`, that boot is read-only, the same boot
11181121
`os migrate plan` takes: the schema sync is held back, the app's inline seed data is not
11191122
loaded, and a SQLite file that does not exist is not created. With `--apply`, the boot
@@ -1132,6 +1135,16 @@ report no secret and no file. A read the command cannot avoid and that fails for
11321135
other reason still refuses and exits 1. Point `--database-url` at the deployment's
11331136
database, or boot the deployment once first, to see what it holds.
11341137
1138+
**The first connection to a SQLite file can change its header.** SQLite keeps a file's
1139+
journal mode in the file itself, and every ObjectStack connection switches a file still
1140+
on a rollback journal to
1141+
[WAL](/docs/data-modeling/drivers#journal-mode-wal-and-cross-process-access), whether
1142+
the boot behind it is read-only or not. That covers every command on this page that
1143+
boots your app. On a SQLite file no ObjectStack process has opened before (one made by
1144+
another tool, or before ObjectStack defaulted to WAL), the first connection converts
1145+
its journal to WAL, whichever command makes it: the file's header changes, and no row
1146+
or table does. After that, a read-only run leaves the file byte-identical.
1147+
11351148
**`--object` narrows a run, and only a run over every object records a flag.**
11361149
`files-to-references`, `value-shapes`, `summary-nulls` and `duplicates` take
11371150
`--object` to restrict the run to the objects you name. `duplicates` takes one name,
@@ -1147,7 +1160,7 @@ was, and `--json` carries `filter: { objects }`. Any `--object` narrows, even a
11471160
that names every object, so run the command without `--object` to record the flag.
11481161
11491162
```bash
1150-
os migrate files-to-references # Dry run: full report, writes nothing
1163+
os migrate files-to-references # Dry run: full report, writes no rows
11511164
os migrate files-to-references --apply # Convert, verify, record the flag (prompts)
11521165
os migrate files-to-references --apply --yes --json # CI / scripts
11531166
os migrate files-to-references --object product # Restrict to one object (repeatable); records no flag
@@ -1182,7 +1195,7 @@ evidence, because this migration is evidence about *file* values and says
11821195
nothing about theirs.
11831196
11841197
<Callout type="warn">
1185-
A dry run writes **nothing** — not the conversions, and not the flag either,
1198+
A dry run writes **no row** — not the conversions, and not the flag either,
11861199
even when the self-check would pass. `--apply` is the only writing mode. A
11871200
later run that *fails* its self-check clears the flag's verified state, so a
11881201
database that has drifted closes its own gate.
@@ -1200,7 +1213,7 @@ The same gate for the **non-media** value classes — references (`lookup`,
12001213
`composite`, `repeater`, `record`, `vector`).
12011214
12021215
```bash
1203-
os migrate value-shapes # Scan: full report, writes nothing
1216+
os migrate value-shapes # Scan: full report, writes no rows
12041217
os migrate value-shapes --apply # Scan, then record the flag if clean (prompts)
12051218
os migrate value-shapes --apply --yes --json # CI / scripts
12061219
os migrate value-shapes --object contact # Restrict to one object (repeatable); records no flag
@@ -1212,8 +1225,8 @@ the platform narrowed that storage form and therefore owes the conversion; a
12121225
*application* data whose correct value only its author knows. So the run reports
12131226
— object, field, type, how many records, sample record ids, and the parse issue
12141227
— and you fix the values (or the code writing them) and re-run until it is
1215-
green. Because there is nothing to convert, `--apply`'s only write is the flag
1216-
row itself.
1228+
green. Because there is nothing to convert, the only row `--apply` writes is the
1229+
flag itself.
12171230
12181231
A scan that is **truncated** (by `--max-records`) or that cannot read an object
12191232
fails the gate even with zero violations found: "none in the part we read" is
@@ -1236,7 +1249,7 @@ know my data" lever, not the route to strictness — the route is running the
12361249
migration that produces the evidence.
12371250
</Callout>
12381251
1239-
Same writing rules as its sibling: a dry run writes **nothing**, `--apply` is
1252+
Same writing rules as its sibling: a dry run writes **no row**, `--apply` is
12401253
the only writing mode, a later failing run clears the verified state, and a
12411254
running server reads the flag once — **restart it** after a successful apply.
12421255
@@ -1252,7 +1265,7 @@ then silently omits it, and so do sorting, `GROUP BY` and any formula reading
12521265
the column (null propagation).
12531266
12541267
```bash
1255-
os migrate summary-nulls # Dry run: full report, writes nothing
1268+
os migrate summary-nulls # Dry run: full report, writes no rows
12561269
os migrate summary-nulls --apply # Recompute and write (prompts)
12571270
os migrate summary-nulls --apply --yes --json # CI / scripts
12581271
os migrate summary-nulls --object project # Restrict to one object (repeatable)
@@ -1346,7 +1359,7 @@ them on every load, and each one logs a conversion notice once per boot. This
13461359
command ends that for the deployment that runs it.
13471360
13481361
```bash
1349-
os migrate meta --stored # Preview: per-row report, writes nothing
1362+
os migrate meta --stored # Preview: per-row report, writes no rows
13501363
os migrate meta --stored --apply # Rewrite the rows (prompts)
13511364
os migrate meta --stored --apply --yes --json # CI / scripts
13521365
os migrate meta --stored --type view --type object # Restrict to a type (repeatable)
@@ -1448,8 +1461,8 @@ executor registry the conflict guard needs from the process it is running in.
14481461
14491462
#### `os migrate duplicates`
14501463
1451-
The one command in this family that is **not** a migration: it writes nothing
1452-
under any flag, and there is nothing to apply. It inventories business
1464+
The one command in this family that is **not** a migration: it writes no row and
1465+
no schema under any flag, and there is nothing to apply. It inventories business
14531466
identifiers the platform already handed out twice — one value held by rows in
14541467
more than one of the organization partitions a
14551468
[`unique: 'organization'`](/docs/data-modeling/indexing) index separates.
@@ -1473,7 +1486,10 @@ human-rendered mode — the report is the deliverable, you archive it, and a
14731486
second renderer would be a second contract to keep true. The boot behind it is
14741487
read-only: no DDL, no seed, and a missing SQLite file is not brought into
14751488
existence. A full run leaves the rows and the counters byte-identical, so
1476-
pointing it at production changes nothing about production.
1489+
pointing it at production changes no data in production. A SQLite file
1490+
ObjectStack has already switched to WAL comes out byte-identical too; on a file
1491+
still on a rollback journal, the first connection changes the file's header, as
1492+
[Data migrations](#data-migrations) describes.
14771493
14781494
**Run it before the repair reaches this deployment.** On its first boot after
14791495
the upgrade, a single-organization deployment adopts those untenanted seed rows

‎content/docs/deployment/seed-tenancy-repair.mdx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,9 +308,12 @@ The live condition is the only forward-looking line in the report, and it is the
308308
perishable one. On a deployment you have not yet restarted since discovering the
309309
problem, run the report first.
310310

311-
`os migrate duplicates` writes nothing: it boots read-only, declares itself out
312-
of the boot repair, issues `SELECT`s only, and emits JSON to stdout for you to
313-
archive.
311+
`os migrate duplicates` writes no row: it boots read-only, declares itself out
312+
of the boot repair, its own queries are `SELECT`s only, and it emits JSON to
313+
stdout for you to archive. Its first connection to a SQLite file still on a
314+
rollback journal does convert that journal to WAL, a change to the file's header
315+
and to no row, as the
316+
[CLI reference](/docs/deployment/cli#data-migrations) describes.
314317

315318
<Callout type="warn" title="This is easy to lose by accident">
316319
The repair fires at `kernel:ready` on a serving boot. Restarting the server to

0 commit comments

Comments
 (0)