Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/23400-ownership-occupancy-gate.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/cli': minor
---
Expand All @@ -12,7 +12,7 @@

**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.

**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.
**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.

**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.

Expand Down
19 changes: 19 additions & 0 deletions .changeset/23562-read-only-arms-journal-mode-note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@objectstack/cli': patch
---

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

**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.

**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."

- `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.
- 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.
- `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.
- 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.
- 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.
- 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.
- 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.

**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.
6 changes: 3 additions & 3 deletions packages/cli/src/commands/migrate/audit-metadata-bodies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from '../../utils/format.js';
import { bootSchemaStack } from '../../utils/schema-migrate.js';
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
import { absentTableReads } from '../../utils/absent-table-reads.js';

Expand Down Expand Up @@ -54,7 +54,7 @@ async function confirm(question: string): Promise<boolean> {
* withholds the hashes in those notes, in the same pass. Operators run it once
* after upgrading, dry run first.
*
* Dry run by default (writes nothing), `--apply` to rewrite. Idempotent: a
* Dry run by default (writes no row), `--apply` to rewrite. Idempotent: a
* second run finds nothing — a redacted copy has no credential and no hash
* left — so re-running and reading a clean report is the verification. No
* `sys_migration` flag is recorded: nothing gates irreversible behaviour on this
Expand Down Expand Up @@ -115,7 +115,7 @@ export default class MigrateAuditMetadataBodies extends Command {
if (occupancy.status === 'busy' && !flags.json) {
printWarning(apply
? `--force: ${describeOccupancy(occupancy)} Rewriting anyway — the live process may write rows mid-walk.`
: `${describeOccupancy(occupancy)} The dry run below writes nothing, but its counts may shift while that process is running.`);
: `${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}`);
}
if (occupancy.status === 'unknown' && !flags.json) {
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/migrate/comment-reactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../utils/format.js';
import { bootSchemaStack } from '../../utils/schema-migrate.js';
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
import { absentTableReads } from '../../utils/absent-table-reads.js';
import type { MigrationFlagEngine } from '@objectstack/platform-objects/system';
Expand Down Expand Up @@ -136,7 +136,7 @@ export default class MigrateCommentReactions extends Command {
if (occupancy.status === 'busy' && !flags.json) {
printWarning(apply
? `--force: ${describeOccupancy(occupancy)} Converting anyway — the live process may write rows mid-walk.`
: `${describeOccupancy(occupancy)} The dry run below writes nothing, but its counts may shift while that process is running.`);
: `${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}`);
}
if (occupancy.status === 'unknown' && !flags.json) {
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/commands/migrate/files-to-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { StandardErrorCode } from '@objectstack/spec/api';
import { bootSchemaStack } from '../../utils/schema-migrate.js';
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
import { isNarrowedRun, narrowedFlagNote, refuseUndeclaredObjects } from '../../utils/migrate-object-scope.js';
import {
Expand Down Expand Up @@ -95,7 +95,7 @@ async function confirm(question: string): Promise<boolean> {
* platform version) is what later opens released-file collection (#3459) and
* strict media value-shape enforcement (#3438) on this deployment.
*
* Dry run by default, and a dry run writes NOTHING — not conversions, not the
* Dry run by default, and a dry run writes NO ROW — not conversions, not the
* flag. Not run / not passed → files keep being retained forever: storage
* cost, zero data loss.
*
Expand Down Expand Up @@ -188,11 +188,11 @@ export default class MigrateFilesToReferences extends Command {
return;
}
if (occupancy.status === 'busy' && !flags.json) {
// A dry run writes nothing, so it only ever warns — but it warns, because
// A dry run writes no row, so it only ever warns — but it warns, because
// the numbers it reports are a moving target while another process writes.
printWarning(apply
? `--force: ${describeOccupancy(occupancy)} Converting anyway — the live process may write records mid-scan.`
: `${describeOccupancy(occupancy)} The dry run below writes nothing, but its counts may shift while that process is running.`);
: `${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}`);
}
if (occupancy.status === 'unknown' && !flags.json) {
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/commands/migrate/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js
import { absentTableReads } from '../../utils/absent-table-reads.js';
import type { StoredMigrationReport } from '@objectstack/metadata-protocol';
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
import { checkProtocolCompat, type ProtocolHandshakeManifest } from '@objectstack/metadata-core';
import {
planAuthoredSourceWrite,
Expand Down Expand Up @@ -1439,8 +1439,8 @@ export default class MigrateMeta extends Command {
if (occupancy.status === 'busy' && !flags.json) {
printWarning(apply
? `--force: ${describeOccupancy(occupancy)} Rewriting anyway — the live process may save metadata mid-run.`
: `${describeOccupancy(occupancy)} The preview below writes nothing, but a live process may `
+ 'be saving metadata while it runs.');
: `${describeOccupancy(occupancy)} The preview below writes no row or table, but a live process may `
+ `be saving metadata while it runs. ${JOURNAL_MODE_SWITCH_NOTE}`);
}
if (occupancy.status === 'unknown' && !flags.json) {
printWarning(`Could not check whether the database is in use — ${occupancy.detail}`);
Expand Down Expand Up @@ -1486,7 +1486,7 @@ export default class MigrateMeta extends Command {
// takes. A plain boot runs schema sync and the artifact's inline seed
// loader, and the seed upserts every seeded row of the app's tables
// (`updated_at` bumped, `organization_id` stamped, relative-date values
// re-evaluated) before the report says "writes nothing". `deferSchemaDdl`
// re-evaluated) before the report says "writes no row". `deferSchemaDdl`
// holds the DDL back and suppresses the seed; `readOnlyProbe` keeps a
// missing sqlite file from being created. `--apply` keeps the plain boot:
// it is the writing mode, and its behaviour is unchanged.
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/commands/migrate/multi-value-columns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '../../utils/format.js';
import { bootSchemaStack } from '../../utils/schema-migrate.js';
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
import { describeOccupancy, JOURNAL_MODE_SWITCH_NOTE } from '../../utils/sqlite-occupancy.js';
import type { ManagedDriftEntry } from '@objectstack/driver-sql';
import type { IObjectQLEngine } from '@objectstack/spec/contracts';

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

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

if (apply && !flags.yes) {
Expand Down
Loading
Loading