Repository navigation
Commit 6990b0d
fix(driver-sql,cli): os migrate plan previews a
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DQK6nb1EwEVz5iwboqt6xrS)_
Fixes #30636
Clause-②: yes (widening)
Metadata-protocol: none
## 维护者速读
- 改了什么:`os migrate plan` 遇到用 `_id` 当主键、没有 `id` 列的已有表,不再预报「补列」,而是照 apply
的真实做法预报:有数据就列为「拒绝」(带表名、`_id`、行数和拒绝原文),空表就列为「重建」。`os migrate apply`
遇到这种被拒的表,不再整批停下:其余表照常建表补列,被拒的表单独点名,原样不动,最后 PARTIAL、退出码 1。
- 为什么改:实测 main 上 plan 对这两种表都说「补列」(无害),apply 却在第一张有数据的 `_id` 表上抛错退出,另外 80
张该建的表一张没建、该补的列一列没补,已完成清单也随异常丢了。
- 风险与回滚:只动 driver-sql 的预览与延迟 DDL 冲刷、CLI 的 plan/apply 渲染;正常表(有
`id`)的预览与执行一字未变。另加一条保护:被拒表上的 drift(其 `_id` 列看起来是「孤儿列」)不再下发,即使带
`--allow-destructive` 也不会删它。回滚即撤本 PR。
-
先看:`sql-driver-deferred-ddl.ts`(按预览把被拒表排除出冲刷);`sql-driver-schema-sync.ts`
的 `previewDeferredSchemaWork` 新分支;`apply.ts` 冲刷之后那段(拆分被拒、扣下被拒表的
drift、PARTIAL)。
- 两分钟验证:`pnpm --filter @objectstack/driver-sql exec vitest run
src/sql-driver-schema.test.ts`,预期 sqlite 格 7 个 `schema sync never drops
a table holding rows` 用例全绿(其中 4 个是本 PR 新增)。
- 需要你做的:无,席位落地。
## What changes
An existing table keyed by `_id` with no `id` column is the one shape
the schema sync does not adopt by adding columns: `initObjects` rebuilds
it while it is empty and, since PR #30632, refuses it
(`ExistingTableAdoptionRefusal`) while it holds rows.
- **Plan** (`previewDeferredSchemaWork`, `sql-driver-schema-sync.ts`):
that shape is now planned as the sync treats it, with the same two
probes and the same row count. Holding rows: a `refuse_table` entry,
`columns: ['_id']`, `rows: N`, and `refusal`, the text rendered by
`renderExistingTableAdoptionRefusal`, the renderer the sync throws
through. Empty: a `rebuild_table` entry (`rows: 0`, the declared
columns). Never `add_columns`. A rotation-declared object never reaches
that branch of the sync, so it does not reach this one.
- **Kinds** (`schema-drift.ts`): `PendingSchemaWorkKind` gains
`rebuild_table` and `refuse_table`; `PendingSchemaWork` gains the
optional `refusal`; `isRefusedSchemaWork(kind)` is exported beside
`isInPlaceSchemaWork`, which now answers `true` for `rebuild_table` (it
is not additive) and `false` for `refuse_table` (it is no work at all).
- **Apply, driver side** (`flushDeferredSchemaDdl`,
`sql-driver-deferred-ddl.ts`): the flush already captures the preview
before running the DDL; a table the preview refused is now left out of
the one `initObjects(pending)` re-entry, so it stops nothing. Every
other deferred object gets its DDL and the refusal comes back as its
entry in the returned list instead of a throw that took that list with
it. The sync's own refusal still guards the rows if the table fills
between preview and DDL.
- **Apply, CLI side** (`apply.ts`, `schema-migrate.ts`):
`splitRefusedSchemaWork` splits what the flush returned; `created` lists
performed work only, each refused table is printed with its refusal
text, and the run ends `PARTIAL` with exit 1 (`--json`: `refused` beside
`created`, `message: "partial"`). Drift entries on a refused table are
withheld from the driver and reported in `skipped`: the table's `_id`
column reads as an orphaned column, and with `--allow-destructive` the
driver would otherwise drop it from the table the sync just left alone
(on `main` the throw stopped the run before that call, so this path is
new with "carry on").
- **Rendering**: `renderPendingSchemaWork` lists a refused table under
its own heading `Refused (left exactly as it is when you apply)` with
the refusal text, and a rebuild under the in-place heading;
`summarizePendingSchemaWork` adds `N empty table(s) to rebuild` / `N
table(s) REFUSED` when there are some and no longer counts them as
temporal columns. `addRefusedTableLimits` gives each drift entry on a
refused table that refusal as its apply limit, so neither `plan` nor the
listing `apply` prints before its prompt offers `--allow-destructive`
for it.
Changed lines (`git diff origin/main --stat`): 12 files, +553 / −18
(driver-sql src +65/−4, CLI src +131/−12, tests +344/−2, changeset +13).
## Tests
- `packages/drivers/driver-sql/src/sql-driver-schema.test.ts`, the
`schema sync never drops a table holding rows` matrix (every dialect
cell): the plan pin on both arms (rows: exactly one `refuse_table` entry
whose `refusal` equals the renderer's output AND the message
`syncSchema` throws for the same table; empty: `rebuild_table`, then the
flush performs it), the CONTROL (`id`-keyed table: `add_columns
['qty']`), and the scope addition (a refused table between two good ones
in deferred order: both get their DDL, the returned list carries both
`create_table` entries and the `refuse_table` entry, the refused table
is byte-for-byte as before, the deferral is disarmed).
- `packages/cli/src/utils/schema-migrate.pending-render.test.ts`: the
Refused heading and text, the rebuild line, the summary, the split.
- `packages/cli/test/migrate-plan-refused-table.e2e.test.ts` (nightly
e2e tier): the CLI measurement below as a pin, `plan --json` then `apply
--yes --allow-destructive --json` on a seeded SQLite file.
<details><summary>Measured through the CLI (fixture: `legacy_full` _id +
3 rows, `legacy_empty` _id empty, `plain_ok` id-keyed missing `note`,
`zz_fresh` absent)</summary>
`origin/main` (driver-sql dist and CLI src from 89dbc74):
- `os migrate plan --json`: `legacy_full` and `legacy_empty` both `kind:
"add_columns"` (8 columns each), exit 0.
- `os migrate apply --yes`: lists `legacy_full [add_columns: …]`, then
`✗ Schema sync REFUSED for the existing table 'legacy_full' …`, exit 1.
Afterwards: `legacy_full` 3 rows untouched, but `legacy_empty` not
rebuilt, `plain_ok` without `note`, `zz_fresh` absent: none of the 80
creates / 25 columns listed was performed.
This branch:
- `os migrate plan`: `+ plain_ok [add_columns: …, note]`, `~
legacy_empty [rebuild_table: empty table dropped and recreated, 9
column(s)]`, `✗ legacy_full [refuse_table: _id — 3 row(s)]` + the
refusal text; summary `80 table(s) to create, 9 column(s) to add, 1
empty table(s) to rebuild, 1 table(s) REFUSED`; `legacy_full._id
[drop_column]` says `"os migrate apply" cannot apply it on this
database: the schema sync refuses the table 'legacy_full' …`.
- `os migrate apply --yes`: `✓ Created/extended 82 table(s)`, `✗ REFUSED
legacy_full [refuse_table: _id — 3 row(s)]` + text, `✗ PARTIAL apply:
schema sync REFUSED 1 existing table(s) …`, exit 1. Afterwards
`legacy_full` `["_id","title"]` 3 rows; `legacy_empty` rebuilt with
`id`; `plain_ok` has `note`; `zz_fresh` created.
- `os migrate apply --yes --json`: `created` 82 entries, `refused`
[legacy_full, 3 rows, refusal text], `notApplied: []`, `message:
"partial"`, exit 1.
- `os migrate apply --yes --allow-destructive`: same, and `legacy_full`
still `["_id","title"]` with 3 rows.
</details>
<details><summary>Ablation (commit aa54f87 on the fixed tree, driver
tests read src/)</summary>
- Flush leg: `initObjects(refused.size === 0 ? pending :
pending.filter(…))` → `initObjects(pending)` via
`scripts/ablation-replace.mjs` (anchor 1 → 0, blob aa5c540 → 5d90f111):
`Tests 1 failed | 26 passed` (the refused-between-two test). Restored,
`git diff HEAD` empty.
- Plan leg: the preview branch condition prefixed with `false &&` (blob
2de56b0 → c89e4d34): `Tests 3 failed | 24 passed` (both plan arms and
the flush test). Restored: blob == HEAD, `git diff HEAD` empty.
</details>
## Acceptance notes
- **Published type widened.** `PendingSchemaWorkKind` is exported by
`@objectstack/driver-sql` (and re-exported as `PendingSchemaWork` from
the CLI's `schema-migrate.ts`); it gains two members,
`PendingSchemaWork` gains the optional `refusal`, and
`isRefusedSchemaWork` is a new export. The `os migrate plan --json`
`pending` array and the `os migrate apply --json` document (new
`refused` key) carry them. The seat re-derived Clause-② for this as `yes
(widening)` (claim amendment 6673377065); the changeset carries the same
two lines, both packages stay `patch` (`Metadata-protocol: none`), and
its body names the widening for a driver-sql consumer: a caller of
`flushDeferredSchemaDdl()` must treat a returned `refuse_table` entry as
refused, not performed.
- **Behaviour change on apply:** a run that met a refused `_id` table
used to stop with the refusal and do nothing else; it now performs every
other table's DDL and the drift, and still ends exit 1. Nothing in the
tree depended on the stop (the only caller of `flushDeferredSchemaDdl`
is `SchemaStack.flushSchemaDdl`).
- Not changed: the drift listing still shows `legacy_empty._id
[drop_column]` for an empty `_id` table the flush rebuilds; once rebuilt
the column is gone, and `--allow-destructive` reports the entry as
applied. Pre-existing on `main` for an empty-only database; not this
card's surface.
## Verification
Head `77b3053a` adds one changeset-only commit over `87ec2b98` (the
Clause-② line and one sentence); the changeset gates were re-run on it
(see the end of this section). Everything else below is on head
`87ec2b98` (this branch merged with `origin/main` 4564d6f), after `pnpm
install --frozen-lockfile` and a whole-tree `turbo run build` (lock
VERDICT command-exit 0).
- `pnpm --filter @objectstack/driver-sql typecheck` exit 0; `pnpm
--filter @objectstack/cli typecheck` exit 0 (on aa54f87, before the
merge).
- `pnpm --filter @objectstack/driver-sql test`: `Test Files 238 passed |
12 skipped (250)`, `Tests 3866 passed | 218 skipped`, exit 0 (aa54f87).
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: `Test
Files 389 passed (389)`, `Tests 5435 passed (5435)`, exit 0 (aa54f87).
The integration tier is declared to CI; the new e2e file ran by hand:
`OS_TEST_TIERS=nightly … --project integration
test/migrate-plan-refused-table.e2e.test.ts` → `Tests 5 passed (5)`.
- `sql-driver-schema.test.ts` against a local PostgreSQL 16
(`OS_TEST_POSTGRES_URL`): `Tests 34 passed | 1 skipped` (the live
postgres cell of the matrix, all 7 cases green). MySQL not provisioned
here; the `Temporal Conformance (live PG + MySQL)` job runs the cell.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 66 families, each run and exit 0; `--ran` →
`66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED`.
<details><summary>The 66 gate families (each `:: exit 0`)</summary>
```
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:cli-test-child-env :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-conformance :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:i18n-coverage :: exit 0
pnpm check:i18n-walk-parity :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:object-def-param-keys :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:tenant-chokepoint :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
```
</details>
On head `77b3053a` (the changeset-only commit), with this body as the
`pull_request` event payload: each `:: exit 0`.
```
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:issue-citations :: exit 0
```
`check-changeset-no-major` LEVEL AXIS line: "this PR declares clause-②
yes (widening) and metadata-protocol none, and it introduces no
changeset entry graded minor or above" (pass).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cy3CHSvvtQP754FaLB
<!-- signed: domain:server#4 · os-elon-musk ·
session_016WV4cy3CHSvvtQP754FaLB -->
---------
Co-authored-by: Claude <noreply@anthropic.com>_id-keyed table as apply treats it; one refused table no longer stops apply (#30750)1 parent e4e041f commit 6990b0d
12 files changed
Lines changed: 553 additions & 18 deletions
File tree
- .changeset
- packages
- cli
- src
- commands/migrate
- utils
- test
- drivers/driver-sql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
29 | 32 | | |
30 | 33 | | |
31 | 34 | | |
| |||
347 | 350 | | |
348 | 351 | | |
349 | 352 | | |
| 353 | + | |
| 354 | + | |
350 | 355 | | |
351 | 356 | | |
352 | 357 | | |
| |||
680 | 685 | | |
681 | 686 | | |
682 | 687 | | |
683 | | - | |
684 | | - | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
685 | 704 | | |
686 | 705 | | |
687 | 706 | | |
| |||
701 | 720 | | |
702 | 721 | | |
703 | 722 | | |
704 | | - | |
| 723 | + | |
705 | 724 | | |
706 | | - | |
| 725 | + | |
707 | 726 | | |
708 | 727 | | |
709 | 728 | | |
| |||
712 | 731 | | |
713 | 732 | | |
714 | 733 | | |
| 734 | + | |
715 | 735 | | |
716 | 736 | | |
717 | 737 | | |
| |||
729 | 749 | | |
730 | 750 | | |
731 | 751 | | |
732 | | - | |
| 752 | + | |
733 | 753 | | |
734 | 754 | | |
735 | 755 | | |
| |||
752 | 772 | | |
753 | 773 | | |
754 | 774 | | |
755 | | - | |
756 | | - | |
757 | | - | |
758 | | - | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
759 | 784 | | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
760 | 799 | | |
761 | 800 | | |
762 | 801 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
| |||
469 | 470 | | |
470 | 471 | | |
471 | 472 | | |
| 473 | + | |
| 474 | + | |
472 | 475 | | |
473 | 476 | | |
474 | 477 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| |||
113 | 114 | | |
114 | 115 | | |
115 | 116 | | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1698 | 1698 | | |
1699 | 1699 | | |
1700 | 1700 | | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
| 1705 | + | |
| 1706 | + | |
| 1707 | + | |
| 1708 | + | |
| 1709 | + | |
| 1710 | + | |
| 1711 | + | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
| 1716 | + | |
| 1717 | + | |
| 1718 | + | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
1701 | 1728 | | |
1702 | 1729 | | |
1703 | 1730 | | |
| |||
1764 | 1791 | | |
1765 | 1792 | | |
1766 | 1793 | | |
| 1794 | + | |
| 1795 | + | |
| 1796 | + | |
| 1797 | + | |
| 1798 | + | |
| 1799 | + | |
| 1800 | + | |
| 1801 | + | |
| 1802 | + | |
| 1803 | + | |
| 1804 | + | |
| 1805 | + | |
| 1806 | + | |
| 1807 | + | |
| 1808 | + | |
| 1809 | + | |
| 1810 | + | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
| 1816 | + | |
| 1817 | + | |
1767 | 1818 | | |
1768 | 1819 | | |
1769 | 1820 | | |
| |||
1808 | 1859 | | |
1809 | 1860 | | |
1810 | 1861 | | |
1811 | | - | |
1812 | | - | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
1813 | 1865 | | |
1814 | 1866 | | |
1815 | 1867 | | |
| |||
1827 | 1879 | | |
1828 | 1880 | | |
1829 | 1881 | | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
1830 | 1891 | | |
1831 | 1892 | | |
1832 | 1893 | | |
| |||
1837 | 1898 | | |
1838 | 1899 | | |
1839 | 1900 | | |
| 1901 | + | |
| 1902 | + | |
| 1903 | + | |
| 1904 | + | |
| 1905 | + | |
| 1906 | + | |
| 1907 | + | |
| 1908 | + | |
| 1909 | + | |
| 1910 | + | |
| 1911 | + | |
1840 | 1912 | | |
1841 | 1913 | | |
1842 | 1914 | | |
| |||
1854 | 1926 | | |
1855 | 1927 | | |
1856 | 1928 | | |
1857 | | - | |
| 1929 | + | |
1858 | 1930 | | |
1859 | 1931 | | |
1860 | 1932 | | |
1861 | 1933 | | |
1862 | 1934 | | |
| 1935 | + | |
| 1936 | + | |
| 1937 | + | |
| 1938 | + | |
| 1939 | + | |
1863 | 1940 | | |
1864 | 1941 | | |
0 commit comments