Skip to content

Commit 1777a9b

Browse files
fix(cli): the rest of the read-only data doors answer a project with no database yet with empty work (#21552) (#21570)
Fixes #21552 Clause-②: no On a project whose database does not exist yet, `os migrate account-issuer`, `os migrate audit-metadata-bodies`, `os migrate meta --stored`, `os secret orphans`, `os secret rewrap` and `os storage orphans` now answer with empty work and exit 0. Before, each exited 1 from its own first read of a table its read-only boot had deferred. This completes the family that PR #21550 started for `resume`, `recorded-by` and `value-shapes`, under the same ruling (`5965283666`, applied to these six doors by triage's `5966537984`): prefer "not asked"; where a read cannot be avoided, recognise its refusal only with `isMissingTableError` and only for the command's own table. ## Measured at the public door Fixture: one artifact with two app objects, `--database-url file:` a path that does not exist, run as `node packages/cli/bin/run-dev.js ... --json`. Base `f83d0669d7` (origin/main when the branch was cut), head `491087ee0f` (the `src` of the six doors is the same at the current head `69a1689f00`; the patch round touched two test files only). `files-to-references`, `summary-nulls`, `multi-value-columns` and `duplicates` are the controls; `duplicates` has no `--json` flag and always prints its one document. | command (absent database) | base | head | |:--|:--|:--| | `migrate account-issuer` | exit 1, `RESOURCE_CONFLICT`, "Cannot enumerate sys_account" | exit 0, `scanned: 0, ok: true` | | `migrate audit-metadata-bodies` | exit 1, `failures: 3` (`sys_audit_log`, `sys_activity`, `sys_metadata_audit`) | exit 0, `failures: 0` | | `migrate meta --stored` | exit 1, `DATABASE_ERROR` for `sys_metadata` | exit 0, `scanned: 0, clean: true` | | `secret orphans` | exit 1, `scan_failed` for `sys_secret` | exit 0, `counts.total: 0`, all three families `enumerated` | | `secret rewrap` | exit 1, `scan_failed` for `sys_secret` | exit 0, `counts.total: 0`, all three families `enumerated` | | `storage orphans` | exit 1, `DATABASE_ERROR` for `sys_file` | exit 0, `filesScanned: 0, stranded: 0` | | the four controls | exit 0 | exit 0 | | database file left behind | none | none | Each of the six names, on stderr under `--json` and on stdout in human mode, the tables it read as no rows. ## The boot path of each door (what the seam can and cannot say) All six boot through `bootSchemaStack` with `deferSchemaDdl` and `readOnlyProbe`, so `SchemaStack.tableAbsent` is there to ask. Measured per door: - **Five ask it** (`audit-metadata-bodies`, `meta --stored`, `secret orphans`, `secret rewrap`, `storage orphans`) before the first read. A shared helper, `packages/cli/src/utils/absent-table-reads.ts`, is the one place a door asks. It adds no second mechanism: it wraps `tableAbsent`, and a refused read of an ordinary table is still issued and still refused. - **`account-issuer` cannot ask.** Its boot composes no auth plugin, so `sys_account` is not a registered object and the held-back sync never lists it: `tableAbsent('sys_account')` is false on every database. It is the `isMissingTableError` case the ruling names: the probe's read is not avoidable, and the door reads that one refusal, for `sys_account` only, as no rows. Registering the object would not be a fix: the probe selects the retired `issuer` column, which a registered `sys_account` no longer declares. - **`meta --stored`** hands its read to the metadata protocol, which holds a private engine this command cannot wrap. The preview answers an absent `sys_metadata` itself with the report the protocol returns for zero rows, typed as `StoredMigrationReport` so a new field is a compile error here. - **`secret orphans` and `secret rewrap`** read at driver level and through the reference union. The union is given a read-only view of the engine (`secretUnionReadView`): only `find` is carried onto the driver, so `--delete` and `--apply` take their write verbs from the unwrapped driver, and `listDatasourceDefs` stays absent when the engine has none (its absence is a declared gap). ## The table the boot cannot measure: `sys_activity` The control for `audit-metadata-bodies` showed `tableAbsent('sys_activity')` true on a booted database. `sys_activity` is rotation-managed: its rows live in `sys_activity__rYYYYMMDD` shard tables and its base name is a view. The deferred sync asks the driver `hasTable` for the base name, a view is not a table, so on SQLite it lists the base as `create_table` over a database that serves it. A door that believed it would have answered "Nothing to rewrite" over the cleartext credential copies the command exists to reach. So `absentTableReads` takes a schema lookup, and for a rotation-managed object it does not consult the measurement: the read is issued, and only its missing-table refusal (`isMissingTableError`, for that object) reads as no rows. The control pins it: a cleartext copy seeded into the rotation shard is found and counted (`sys_activity.rewritten: 1`). The seam itself is untouched here (see Out of scope). ## Documented exit (A3) - `migrate account-issuer`: its own description, "exits non-zero when the drop must not proceed". A database with no account table has no collision, and a booted database holding two clean accounts answers `ok: true` with exit 0. - `migrate audit-metadata-bodies`, `migrate meta --stored`: the platform checklist's migrate item (`docs/qa/platform-checklist/areas/cli.json`: "EVERY migrate subcommand with --json exits 0 on success"); `meta --stored` also documents "A second pass reporting every row canonical exits 0". - `secret orphans`, `secret rewrap`, `storage orphans`: report-only by their own documentation ("Report-only by default: without `--delete` it writes nothing", "A dry run by default", "Writes nothing"); their report mode exits 0 whenever the read succeeded. - Each is also the exit the same door gives the booted control (below). `content/docs/deployment/cli.mdx` said the opposite in two places: the Data migrations edge paragraph ("Another dry run that reads a missing table can still fail and exit 1") and `os secret orphans` ("it refuses and exits 1 ... `scan_failed`"). Both now describe the empty-work answer, and keep the refusal for any other read that fails. ## Tests - **Integration pin, extended:** `packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts` gains a second block. For each of the six, on the absent database: `--json` exit 0 with the empty-work document, no refused read of its own tables, the table named on stderr; human mode exit 0 on the empty-work sentence; no database file created. The control is a booted database holding one row of work per door (two legacy `sys_account` rows, a cleartext audit copy in `sys_audit_log` and in the `sys_activity` shard, a stored metadata row, an unreferenced `sys_secret` row, a stranded `sys_file`): each door READS it, reports the row, and says nothing about absent tables. The four doors that already exited 0 are pinned to still do. Every spawn runs in `beforeAll`. - **Two refusal pins flipped:** `preview-read-only.integration.test.ts` pinned exit 1 for `meta --stored` and `audit-metadata-bodies` on a missing database (the refusal #21391 declared). That is the behaviour the ruling reverses for these doors; both cases now assert exit 0 and the empty-work document, with the file header updated. - **Unit:** `packages/cli/src/utils/absent-table-reads.test.ts`, 17 cases: the helper's answers, the stderr/stdout split, and the rotation rule. - **Patch round 1: two stale test doubles.** CI Test Core (1/6) was red at `491087ee0f` in `rewrap.guards.test.ts`. That suite, and `orphans.guards.test.ts`, replace `bootSchemaStack` and hand the command a stack of only `kernel` and `shutdown`. The doors' first ask is `stack.tableAbsent`, a member of `SchemaStack` since PR #21550, so every case fell into the `scan_failed` catch. Root cause read off the case's own output (a throwaway copy of the suite that printed the payload): `{"error":"scan_failed","message":"stack.tableAbsent is not a function"}`, in all five cases of `rewrap.guards.test.ts`. Run locally against the unfixed head, the two suites had 7 red cases (5 in `rewrap.guards`, 2 in `orphans.guards`); CI listed four. The fix is on the double's side: both doubles now carry `tableAbsent` as the real boot returns it (`false` for every table on the plain boot of a writing run; `rewrap.guards` takes the measured-absent set as an option). No consumer-side tolerance was added. `rewrap.guards.test.ts` also gains one case: a dry run over tables the boot measured absent issues no read, reports `counts.total: 0` with every family `enumerated`, and a present-table control still reads. The stack double in `test/exit-signal.pin.test.ts` (`stackWith`) has no `tableAbsent` either, and no path that pin drives reaches it (`recorded-by --apply` and `resume --run` short-circuit before the ask, and `account-issuer` does not use the seam), so it is left as it is. - **Test files that mock `bootSchemaStack` or `schema-migrate.js`** (`git grep` for `vi.mock` of the module over `packages/cli`): `rewrap.guards`, `orphans.guards`, `test/exit-signal.pin`, `files-to-references.column-step-refusal` and `summary-nulls`. The last two reach other commands, not these doors. The test files that reach a door through any other seam (a real boot, the command module, or a source scan) were found by `git grep` for the door names and modules: `data-commands.absent-database`, `preview-read-only`, `meta.stored-flow-resolution`, `meta.report-order`, `meta.stored-flags`, `orphans.driver-contract`, `rewrap.driver-contract`, `platform-migrations-arming`, `schema-migrate.one-shot-family`, `resume.recorded-by`, `sys-secret-rewrap`, `one-shot-settings.pin`, `migrate-meta-engine-guidance`, `migrate-meta-strict-factories`. - At `69a1689f00` (this branch merged with origin/main at `491087ee0f`, which brought PR #21560's refusal pins; no later merge, the gate derivation does not call the tree stale): - The 13 files above that run in the package's two projects (the five mockers and the door-reaching files, `summary-nulls` and `files-to-references.column-step-refusal` among them), `vitest run --maxWorkers=2`, both projects: 13 files, 202 tests passed (it was 7 failed, 194 passed before the double fix). 7 of the 13 are integration-tier. - The round-1 integration files (`data-commands.absent-database`, `preview-read-only`, `meta.stored-flow-resolution`, `platform-migrations-arming`, `schema-migrate.one-shot-family`, `resume.recorded-by`), `--project integration`: 6 files, 137 passed, 1 skipped (the live PostgreSQL cell, not provisioned here). - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: 253 files, 3702 passed (after `pnpm --filter @objectstack/cli build`, which the two `published-subpath-*` pins need). - `pnpm --filter @objectstack/cli build` and `pnpm --filter @objectstack/cli typecheck` (test layer included): exit 0. - **NOT MEASURED: the other 69 files of the cli integration tier** (82 files listed by `vitest list --project integration`; 13 run here: those 7 and the six round-1 files). Reason: the tier runs past the 10-minute foreground cap here, so the rest is declared to CI. The narrowing is the `git grep` above: none of the 69 names a door, mocks `schema-migrate.js`, or imports the new helper. And the two `*.e2e.test.ts` files that spawn the doors (`test/migrate-meta.e2e.test.ts`, `test/json-stdout-purity.e2e.test.ts`) belong to neither of the package's two vitest projects, so no local run selects them (the runner printed "matches no test file in this package"). ## Reverse verification Each leg mutated the committed tree through `scripts/ablation-replace.mjs`: anchor 1 to 0, blob changed, restored to `HEAD` with an empty `git diff HEAD` and a clean status. The CLI spawns run `packages/cli/src` through tsx, so there is no dist hop. Two first attempts were refused by the tool itself because the replacement text contained the anchor or already occurred in the file; nothing ran, and each leg was redone with a distinct replacement. - **Leg A, `tableAbsent` forced false** (`schema-migrate.ts`). Predicted: every fresh-project pin of the five seam doors red, the booted controls green, `account-issuer` green (it does not use the seam). Observed: 19 failed, 25 passed, 1 skipped. The 19 are the 7 pins PR #21550 added, the 10 new fresh-project cases (five doors, `--json` and human) and the two flipped `preview-read-only` cases. Every booted control and the four exit-0 controls stayed green, and `account-issuer` stayed green. - **Leg B, the `isMissingTableError` branch of `account-issuer` disabled.** Predicted: the two `account-issuer` fresh-project cases red, nothing else. Observed: 2 failed, 34 passed. - **Leg C, the rotation rule disabled** (`absent-table-reads.ts`). Predicted: the audit control red because `sys_activity` is skipped. Observed: integration 1 failed, 35 passed, with `sys_activity.scanned` 0 where a cleartext copy is stored; the unit file 2 failed, 15 passed. - **Leg D, patch round: the not-asked answer disabled** (`absent-table-reads.ts`, `absent()` always false), over `rewrap.guards.test.ts`. Predicted: only the new absent-tables case red. Observed: 1 failed, 6 passed. Restored to `HEAD`, empty `git diff HEAD`. Legs A to C ran at `491087ee0f`; `src` of the doors and the helper is unchanged since (the round touched two test files). ## Gates At `69a1689f00`, after the final commit: - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 94 commands (the same list as at `491087ee0f`); `--ran`, with an exit code per command, reconciled as "94 run, 0 NOT-MEASURED (a DERIVED zero)". All 94 read exit 0 on this pass, with every package built first (`turbo run build --filter='!@objectstack/docs'`). The derivation says no commit it can see touched what it derives from, so no merge was made. - **`pnpm lint`, as a proven narrowing.** `eslint --no-inline-config --format json` on the 12 changed TS files: 12 files, 0 errors, 0 warnings, none reported as ignored. The population comes from `eslint.config.mjs` (`files: **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`); the config enables no type-aware linting (no `parserOptions.project`, and every block's `parserOptions` is `{ ecmaVersion, sourceType }`), so this diff cannot move the verdict on a file it does not touch. The `.md` and `.mdx` files are outside it. ## Acceptance notes - **Out of the card, kept:** every read-only data-command boot of an absent database still prints `[ObjectQLPlugin] sys_metadata_activation is registered but could not be read`. Triage ruled it out of this card. - **A new file outside the claim's list:** `packages/cli/src/utils/absent-table-reads.ts` (and its unit test). Five doors need the same view, the same notice line and the same rotation rule; five inline copies would drift. `schema-migrate.ts` is untouched. - **A new `this.exit` or `this.error`:** none. The exit-signal pin and PR #21560's refusal pin both ran green over the whole command table. - **Round 1 of the patch:** `rewrap.guards.test.ts` and `orphans.guards.test.ts` are the two files added to the diff (see Tests). - **`value-shapes` (PR #21550)** reads through the same seam without the rotation rule. It composes no audit plugin, so none of its scanned objects is rotation-managed today; noted, not changed. ## Out of scope (reported to the seat, not filed here) - `SchemaStack.tableAbsent` answers true for a rotation-managed object's base name on a booted SQLite database, because `previewDeferredSchemaWork` asks `hasTable` and a view is not a table. Measured through `os migrate audit-metadata-bodies` against a booted database whose `sys_activity` is the view over `sys_activity__rYYYYMMDD`. The same list is what `os migrate plan` prints for a host that composes the audit plugin (not measured at `plan`). The fix belongs in the driver's preview, not in a consumer. --- _Generated by [Claude Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 74281a8 commit 1777a9b

14 files changed

Lines changed: 897 additions & 52 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os migrate account-issuer`, `os migrate audit-metadata-bodies`, `os migrate meta --stored`, `os secret orphans`, `os secret rewrap` and `os storage orphans` answer a project whose database does not exist yet with empty work and exit 0, instead of exiting 1 on a refused read (#21552)
6+
7+
Clause-②: no
8+
9+
Each of these commands boots read-only by default: the schema sync is held back, and a missing SQLite file is opened as an empty in-memory stand-in. That boot already measures which tables the database lacks, because the held-back sync lists each one as a table to create. Each command then read the very tables it had just found missing, and the database refused the read. On a never-booted database (or a `--database-url` that points at one) every default run exited 1:
10+
11+
- `os migrate account-issuer` refused, naming `sys_account`;
12+
- `os migrate audit-metadata-bodies` counted `failures: 3` for `sys_audit_log`, `sys_activity` and `sys_metadata_audit`;
13+
- `os migrate meta --stored` refused, naming `sys_metadata`;
14+
- `os secret orphans` and `os secret rewrap` answered `"error": "scan_failed"`, naming `sys_secret`;
15+
- `os storage orphans` refused, naming `sys_file`.
16+
17+
Each command now reads only the tables its boot found present. A table that does not exist holds nothing, so:
18+
19+
- `os migrate account-issuer` reports no account and no collision (`ok: true`), exit 0;
20+
- `os migrate audit-metadata-bodies` reports nothing to rewrite, with `failures: 0`, exit 0;
21+
- `os migrate meta --stored` reports no stored metadata to examine (`scanned: 0`, `clean: true`), exit 0;
22+
- `os secret orphans` and `os secret rewrap` report no secret to act on, with every holder family enumerated rather than a gap, exit 0;
23+
- `os storage orphans` reports no stranded file, exit 0.
24+
25+
Each names the tables it did not read: on stdout in human mode, on stderr under `--json`, where stdout stays one document. `os migrate account-issuer` is the one that recognises the refusal instead of asking the boot: its boot composes no auth plugin, so `sys_account` is never listed as a table to create. It recognises only the missing-table refusal for `sys_account`, with the shared `isMissingTableError` predicate.
26+
27+
A table that exists but lacks a column, and any other read that is refused, is still read and still refuses with exit 1. The write modes (`--apply`, `--delete`) are unchanged: they boot with the schema sync, so their tables exist before they read.
28+
29+
There is nothing to migrate.

‎content/docs/deployment/cli.mdx‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,10 @@ It is never run for you; nothing on any boot or upgrade path invokes it.
532532
533533
The report boots your app read-only: no schema change, no seed rows, and a SQLite file
534534
that does not exist is not created. Pointed at a database that lacks `sys_secret` (one
535-
that was never booted, or the wrong `--database-url`), it refuses and exits 1 (under
536-
`--json`: `"error": "scan_failed"`) instead of creating the table and reporting nothing.
535+
that was never booted, or the wrong `--database-url`), it does not read the table, since
536+
a table that does not exist holds no row: it reports nothing to act on, names the
537+
tables it did not read, and exits 0. Any other read it cannot make still refuses and
538+
exits 1 (under `--json`: `"error": "scan_failed"`).
537539
538540
```bash
539541
os secret orphans # report (writes nothing)
@@ -1055,12 +1057,16 @@ creates missing tables and columns so the migration has somewhere to write, but
10551057
still loads no seed data: the only rows that change are the migration's own.
10561058
One edge follows from the read-only boot: it finds out which tables the database lacks
10571059
(a never-booted database, or the wrong `--database-url`) instead of creating them.
1058-
`os migrate value-shapes` does not read a table it found missing, since that table
1059-
holds nothing: the scan is clean over zero records, exits 0, and names the objects it
1060-
did not read. `os migrate recorded-by` and `os migrate resume` answer the same way
1061-
(nothing to convert, no interrupted runs). Another dry run that reads a missing table
1062-
can still fail and exit 1, naming the table. Point `--database-url` at the
1063-
deployment's database, or boot the deployment once first.
1060+
A read-only data command does not read a table it found missing, since that table
1061+
holds nothing, and answers with empty work and exit 0. `os migrate value-shapes` is
1062+
clean over zero records and names the objects it did not read. `os migrate
1063+
recorded-by` has nothing to convert and `os migrate resume` no interrupted runs.
1064+
`os migrate meta --stored` has no stored metadata to examine, `os migrate
1065+
audit-metadata-bodies` no audit copy to rewrite, and `os migrate account-issuer` no
1066+
account to collide. `os secret orphans`, `os secret rewrap` and `os storage orphans`
1067+
report no secret and no file. A read the command cannot avoid and that fails for any
1068+
other reason still refuses and exits 1. Point `--database-url` at the deployment's
1069+
database, or boot the deployment once first, to see what it holds.
10641070
10651071
```bash
10661072
os migrate files-to-references # Dry run: full report, writes nothing

‎packages/cli/src/commands/migrate/account-issuer.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Command, Flags } from '@oclif/core';
44
import chalk from 'chalk';
5+
import { isMissingTableError } from '@objectstack/types';
56
import {
67
printHeader,
78
printSuccess,
@@ -128,17 +129,44 @@ export default class MigrateAccountIssuer extends Command {
128129
);
129130

130131
if (!flags.json) printStep('Scanning sys_account…');
131-
const report = await probeAccountIdentityCollisions(engine as never, {
132+
133+
// [#21552] A database with no `sys_account` table holds no account, so no
134+
// two rows collide: the probe reads it as no rows. The read is not
135+
// avoided, measured: this boot composes no `AuthPlugin`, so `sys_account`
136+
// is not a registered object and the held-back sync never lists it, and
137+
// `stack.tableAbsent('sys_account')` answers false on every database.
138+
// The refusal is therefore recognised, with the shared predicate and for
139+
// this command's own table only. ⛔ No other refused read is softened: it
140+
// still throws the probe's refusal below and is never read as clean.
141+
let noAccountTable = false;
142+
const readEngine = engine as Parameters<typeof probeAccountIdentityCollisions>[0];
143+
const readView: typeof readEngine = {
144+
find: async (object, query, options) => {
145+
try {
146+
return await readEngine.find(object, query, options);
147+
} catch (error) {
148+
if (object !== 'sys_account' || !isMissingTableError(error, object)) throw error;
149+
noAccountTable = true;
150+
return [];
151+
}
152+
},
153+
};
154+
const report = await probeAccountIdentityCollisions(readView, {
132155
...(flags['max-records'] != null ? { max: flags['max-records'] } : {}),
133156
});
157+
const noAccountTableLine = noAccountTable
158+
? 'sys_account has no table in this database yet, so no account is stored in it and it was read as no rows.'
159+
: null;
134160

135161
if (flags.json) {
162+
if (noAccountTableLine) console.error(noAccountTableLine);
136163
await emitJson({ database: stack.dbLabel, ...report, duration: timer.elapsed() });
137164
if (!report.ok) this.exit(1);
138165
return;
139166
}
140167

141168
printInfo(`Database: ${chalk.white(stack.dbLabel)}`);
169+
if (noAccountTableLine) printInfo(noAccountTableLine);
142170
console.log('');
143171
console.log(formatAccountIdentityPreflightReport(report));
144172
console.log('');

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { bootSchemaStack } from '../../utils/schema-migrate.js';
2020
import { OCCUPANCY_HINT, probeMigrationTarget } from '../../utils/migrate-occupancy-gate.js';
2121
import { describeOccupancy } from '../../utils/sqlite-occupancy.js';
2222
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
23+
import { absentTableReads } from '../../utils/absent-table-reads.js';
2324

2425
async function confirm(question: string): Promise<boolean> {
2526
if (!process.stdin.isTTY) return false; // non-interactive → require --yes
@@ -179,15 +180,41 @@ export default class MigrateAuditMetadataBodies extends Command {
179180
? { info: (m: string) => console.error(m), warn: (m: string) => console.error(m) }
180181
: { info: (m: string) => printInfo(m), warn: (m: string) => printWarning(m) };
181182

182-
const report = await migrateStoredMetadataBodyCopies(engine, logger, { apply });
183+
// [#21552] Not asked: the dry run's read-only boot measured which tables
184+
// exist, and a table that does not exist holds no copy to rewrite. The
185+
// rewrite reads through this view, which answers such a table with its
186+
// true contents (no rows) without issuing the read. Read anyway, each
187+
// audited table of a project whose database does not exist yet counted as
188+
// a failed read and the dry run exited 1 over rows that do not exist.
189+
// `--apply` booted plain, so there every table exists and every read is
190+
// real; its writes go to the engine itself.
191+
// ⚠️ `sys_activity` is the exception the boot cannot measure: it is
192+
// rotation-managed, its base name a view over time-sharded tables, and the
193+
// deferred sync lists a view as a table to create. It is read, and only
194+
// the refusal of a table that is not there reads as no rows
195+
// (`absentTableReads`). Believing the measurement would skip the very rows
196+
// this command exists to reach.
197+
// ⛔ Only a table that is MEASURED absent: any other refused read is
198+
// still counted in `failures` and still exits non-zero.
199+
const reads = absentTableReads(stack, (object) => engine.getObject(object));
200+
const readView: Pick<IObjectQLEngine, 'find' | 'findOne' | 'update'> = {
201+
find: (object, query, options) => reads.rows(object, () => engine.find(object, query, options)),
202+
findOne: (object, query, options) =>
203+
reads.absent(object) ? Promise.resolve(null) : engine.findOne(object, query, options),
204+
update: (object, data, options) => engine.update(object, data, options),
205+
};
206+
207+
const report = await migrateStoredMetadataBodyCopies(readView, logger, { apply });
183208

184209
if (flags.json) {
210+
reads.notice(true);
185211
await emitJson({ database: stack.dbLabel, apply, report, duration: timer.elapsed() });
186212
if (report.failures > 0) this.exit(1);
187213
return;
188214
}
189215

190216
printInfo(`Database: ${chalk.white(stack.dbLabel)}`);
217+
reads.notice(false);
191218
console.log('');
192219
for (const [object, stats] of Object.entries(report.byObject)) {
193220
console.log(` ${chalk.white(object)}: ${stats.scanned} scanned, ${stats.rewritten} ${apply ? 'rewritten' : 'to rewrite'}`);

0 commit comments

Comments
 (0)