Skip to content

Commit fcc1387

Browse files
committed
docs(cli): say what a read-only boot's first connection writes to a SQLite file
The CLI reference said `os migrate duplicates` "writes nothing at all", and the same wording stood for every read-only boot: the data-migration dry runs, `os migrate plan`, `os secret orphans` / `rewrap`, and the shared read-only-boot paragraph. Measured through the CLI binary on a rollback-journal SQLite file no ObjectStack process had opened, each of those no-write modes moves header offsets 18, 19, 27 and 95 on its first run (the journal goes to WAL, as every ObjectStack connection does) and nothing on a second run; on a file ObjectStack already switched to WAL, both runs move nothing. The shared read-only-boot paragraph under Data migrations now states that once: the first connection converts the journal (a header change, no row or table), and after that a read-only run leaves the file byte-identical. Each per-command "writes nothing" now says what is true on its own (no row, no schema) and the prose sites point to that paragraph. seed-tenancy-repair.mdx carried the same claim about `os migrate duplicates` and gets the same qualification. Documentation only: no code or behaviour changes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
1 parent d7fff21 commit fcc1387

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)