Skip to content

Commit 117d34d

Browse files
feat(metadata-core,metadata-protocol,metadata,platform-objects,spec)!: sys_view_definition retires as inert (ADR-0131 D13, C5 stage S1) (#22374)
Refs #15206 (S1) Clause-②: no ADR-0131 C5, stage S1: `sys_view_definition` retires as inert (D13, card item 1b). This PR closes no card; the card stays open for S2 to S6. ## What this PR does `sys_view_definition` was declared for runtime-authored views (ADR-0017) and never used for them. No framework code writes or reads its rows. The console's view doors write the ADR-0005 `view` overlay in `sys_metadata` through the metadata API. This PR removes the object and everything that existed only to serve it. - **Deleted.** The object and its test (`metadata-core`), the `kernel:ready` active-row index migration and its test (`metadata-protocol/src/migrations/view-definition-active-index.ts`), and the runbook `scripts/migrate/overlay-views-to-sys-view-definition.md`. - **Both registrations.** Registration A (`assembleMetadataProtocol`'s `registerApp`) and registration B (`MetadataPlugin`'s `queryableMetadataObjects`) each provision the four metadata-storage objects. - **The repair hook.** The `kernel:ready` hook in `assembleMetadataProtocol` arms only the `sys_setting` row-identity index and the seed/API tenancy backfill. - **The pre-flight.** `runtime-index-preflight.ts` loses its `idx_sys_view_def_active` row, so `os migrate duplicates` reports three runtime-index entries. - **Exports removed.** - `SysViewDefinitionObject`, from `metadata-core`, the `platform-objects` root and `platform-objects/metadata`. - From `metadata-protocol`: `ensureViewDefinitionActiveIndex`, `resolveIndexExec`, `buildActiveIndexSql`, `VIEW_DEFINITION_TABLE`, `VIEW_ACTIVE_INDEX_NAME`, `VIEW_ACTIVE_PROBE_INDEX_NAME`, `VIEW_ACTIVE_INDEX_COLUMNS`, and the three `EnsureViewIndex*` types. - `classifyIndexFailure` and `IndexExec` stay exported, now from `partial-index-probe.js` (see Deviations). - **spec.** The name leaves `PLATFORM_OBJECTS_BY_PACKAGE['metadata-core']`. A new ADR-0087 D3 entry, `18.sys-view-definition-retired.ts`, lands with one step-18 rationale fragment, and `registry.ts` is regenerated by `gen:migration-registry`. - **Generated.** - The platform-objects translation bundles, regenerated by `pnpm i18n:extract`. Seven files lose 295 lines, one contiguous `sys_view_definition` block each, and nothing else moves. - The tenancy census, regenerated by its own `--write`. - **Docs.** `drivers.mdx`, `cli.mdx`, `packages.mdx` and the checklist item `cli.migrate-duplicates-inventory` (revision 6) now describe two migrations and three indexes. ## Measured first **Positive control** (`git grep -l sys_view_definition`). The control word `sys_metadata` runs over the same pathspec. | tree | `-- 'packages/**/src'` (as written on the card) | `-- ':(glob)packages/**/src/**'` | control `sys_metadata`, same glob | repo-wide | | :-- | --: | --: | --: | --: | | before, `origin/main` `41d0d4038c` | 0 (vacuous: the control reads 0 too) | **41** | 769 | **65** | | after, this branch `26a4702abf` | — | **31** | 769 | **52** | Before equals stage 0's census (41 under `src`, 65 repo-wide), measured at `3599fef123`, so nothing drifted. The census missed one importer, because it imports the camelCase symbol and has no snake-case hit: `packages/metadata/src/migrations/migrate-project-id-to-environment-id.test.ts` imports `SysViewDefinitionObject`. It is edited here. The 31 files that still mention the name after the change, by class: - **Retirement pins (6).** - `metadata-protocol` `plugin.metadata-objects-retired.test.ts` (new) and `runtime-index-preflight.test.ts`. - `metadata` `plugin.test.ts`. - `spec` `platform-object-names.test.ts`. - `cli` `duplicates.contract.test.ts` and `duplicates.integration.test.ts`. - **The ADR-0087 record (2).** The spec entry and `registry.ts`. - **Retirement notes in comments (14).** - `metadata-core` `objects/index.ts`. - `metadata-protocol` `index.ts`, `plugin.ts`, `overlay-index.ts`, `partial-index-probe.ts`, `runtime-index-preflight.ts` and `sys-setting-identity-index.ts`. - `metadata` `plugin.ts`. - `platform-objects` `bundle-ownership.test.ts`, and `objects-es-es-echo-decisions.test.ts`, `objects-ja-jp-echo-decisions.test.ts` and `objects-zh-cn-echo-decisions.test.ts` (a note beside each corrected size pin, added in the final commit). - `cli` `schema-migrate.host-composition.integration.test.ts` and `platform-migrations-arming.integration.test.ts`. - **Historical, left as written (9).** - `cli` `schema-migrate.ts`, `schema-migration-plugins.ts` and `schema-migrate.teardown.integration.test.ts`: past-tense measurements. - `objectql` `dangling-reference-audit.ts` and its test: the pre-#4747 symptom. - Two `platform-objects` echo-decision reasons: decision records that name the twin they copied. - The `spec` `view-item-owner-hidden-retirement.test.ts` fixture comment. - `driver-sql` `declared-index-retired-keys.test.ts`: an index-shape case that cites where the shape came from. Repo-wide, the remainder is the changeset, the checklist history row, ADR-0017, ADR-0029, ADR-0120 and ADR-0131, the v17 release notes, the CHANGELOGs, and one historical comment in `cli/test/migrate-apply-refuses-before-ddl.e2e.test.ts`. **No writer or reader of the rows.** Every non-test, non-generated source hit on `origin/main` is a comment, a declaration, a registration, or the migration's own SQL. The only statements that touched rows were the active-row migration's presence and duplicate probes and the pre-flight's copy of the duplicate probe. One non-test comment claimed a reader that does not exist (`metadata-manager.ts`: "merged in by the REST layer"; `packages/rest` has 0 hits). It is corrected here. **No importer of the removed exports.** - **This repo:** 0 importers outside `metadata-protocol` and `metadata-core`/`platform-objects`, other than the test edited here. The CLI tests only matched the migration's name string in the pre-flight output. - **objectui:** 0 hits for every removed name at `origin/main` `b1f0dcc00` and at the pinned `.objectui-sha` `a58626c88d`, against 93 for the control `sys_metadata` at both. Its only `metadata-protocol` import is `ObjectStackProtocolImplementation`. **Entry export sets.** - `metadata-protocol` `src/index.ts`: 0 names added; the ten listed above removed. - `metadata-core` `objects/index.ts` and `platform-objects` `metadata/index.ts`: 0 added; `SysViewDefinitionObject` removed. - In the built `metadata-protocol/dist/index.d.ts`, every removed name reads 0 hits, and `classifyIndexFailure` and `IndexExec` read 2 and 6. - `check:api-surface`: "public API surface + factory signatures unchanged". Nothing widens, so `Clause-②: no` stands. **What an existing database needs.** Measured with the built CLI on a scratch SQLite project. The database carried a leftover `sys_view_definition` table with one row. A control orphan column sat on `sys_metadata_audit`. - A fresh `os migrate apply` creates no `sys_view_definition`. It creates eight platform tables plus the app table. - `os migrate plan --json` reports the orphan column as `unmapped_column` and says nothing about the table in `changes`. With a host config present, the plan's informational `unmanagedTables` sweep lists `sys_view_definition`. - `os migrate apply --allow-destructive --yes` dropped the orphan column. It left the table and its row in place. So no platform path drops the table. The ADR-0087 entry and the changeset say that, and they leave the drop to the operator, by hand. Stage 0's F4 ("the physical drop is the operator's `os migrate apply --allow-destructive`") holds for a column, not for a whole table. ## Pins - **Absence of the object at registration A.** `plugin.metadata-objects-retired.test.ts` checks that `registerApp` carries exactly the four objects. The `kernel:ready` hook, run against a recording seam, issues statements, and none of them names the table. The anti-vacuity check is that the hook did issue statements. - **Absence at registration B.** `plugin.test.ts` checks `MetadataPlugin` with the same exact-set assertion. - **The registry.** `platform-object-names.test.ts` checks that `PLATFORM_OBJECTS_BY_PACKAGE['metadata-core']` equals the four names, and that `isPlatformProvidedObjectName('sys_view_definition')` is false. The existing bidirectional conformance test still scans every `*.object.ts`. - **`runtimeIndexProbes()`.** On every client (none, sqlite, pg, mysql2), it answers exactly the three `migration:table:index` triples that remain. The pre-flight fixture keeps a damaged `sys_view_definition` table, and the report never names it. - **The census.** `registered` goes 84 → 83 and `inReach` goes 49 → 48, measured by the generator and held by `check:platform-object-tenancy-census`. **Reverse verification.** This ran from the committed tree at `26a4702abf`, as a one-shot script with a `trap` restore. No file of it is committed. - **The mutation re-adds `SysViewDefinitionObject`.** - Its module comes back into the index from `41d0d4038c`. - Its export comes back into `metadata-core` through `scripts/ablation-replace.mjs`. - It goes back into `queryableMetadataObjects` (import and array) through `ablation-replace.mjs`. - On disk, the anchors fell 1 → 0 and the blobs changed. - **Proof it reached the artifact.** `metadata-core` was rebuilt, and `ablation-dist-preflight.mjs @objectstack/metadata-core SysViewDefinitionObject` found the marker in 4 built files. - **Red under the mutation:** - `metadata` `plugin.test.ts`: 1 failed, the new absence pin (`expected [ 'sys_metadata', …(4) ] to not include 'sys_view_definition'`); - `check-platform-object-tenancy-census.mjs`: exit 1 (`totals.registered: committed 83 -> tree 84`, `+ sys_view_definition (in) is new to the population`); - `spec` `platform-object-names.test.ts`, project `repo`: 1 failed (`registry group "metadata-core" is out of date`). - **The restore.** `ablation-replace.mjs --restore` brought both files back: blob == HEAD, `git diff HEAD` empty, `git status` empty. The module was removed from the index. `metadata-core` was rebuilt, and `--absent` found the marker in none of the 12 built files, with a clean tree. - **Green after the restore.** 18/18, census OK at 83/48, 11/11. - **Direction.** It was the ordinary red. The census reads the tracked object modules, not the registrations, so it went red on the restored module. The absence pin went red on the registration. - **A voided first attempt.** The first run of this script was a no-op on the export line, because the replacement re-contained the anchor and the tool refused it. Its spec leg also named the wrong vitest project, so it selected nothing. Its readings were discarded, and the run above is the second one. ## Deviations from stage 0's S1 plan 1. **`classifyIndexFailure` and `IndexExec` stay exported.** They rode out on the retired module's export block, but neither is specific to it. Both live in `partial-index-probe.ts`, and `IndexExec` is the parameter type of the still-exported `ensureSysSettingIdentityIndex` and `resolveSysSettingIndexExec`. They are re-pointed with byte-identical declarations, so nothing about them narrows. 2. **F4 is corrected for a table, as measured above.** The entry does not promise a destructive apply that never comes. 3. **Out of the census, edited:** - the camelCase importer named above; - the three `objects-*-echo-decisions` ledger counts: es-ES 46 → 45, ja-JP 36 → 35 and zh-CN 35 → 34 echoes, because each lost the object's bare `ID` leaf; - `content/docs/plugins/packages.mdx`, which also counted the MySQL-unbuildable indexes. 4. **In the census, left:** the `driver-sql` index-shape case, the two echo-decision reasons, and the `spec` fixture comment, for the reasons in the table above. 5. **The duplicates e2e changed its runtime half.** It is re-premised on two ACTIVE package-less `sys_metadata` overlays with a NON-NULL organization. While building it, I found that a NULL-organization pair is reported `blocked` by the overlay pre-flight, but the index CREATE accepts it. That is out of scope here (see Acceptance notes). ## Changeset and ADR-0087 `.changeset/15206-sys-view-definition-retired.md` declares `minor` on `metadata-core`, `metadata-protocol`, `metadata`, `platform-objects` and `spec`, with the BREAKING banner and a FROM → TO mapping. It carries `Clause-②: no (narrowing)` and the disposition `registered sys-view-definition-retired`. Changesets is in pre mode (`.changeset/pre.json`, tag `next`), and the fixed group is already majored by the v18 opening marker, so this ships as an `18.0.0-next.N`. `runtime` changes one doc comment and `cli` changes tests only, so neither carries a changeset. ## Tests and gates The final head is `26a4702abf`. Each reading names the commit it ran at. **Suites.** - **metadata-core:** test 17 files, 421 passed; typecheck exit 0 (`41dbe7962f`; the later merge did not touch the package). - **metadata-protocol:** test 223 files passed, 3 skipped, 28313 tests passed, 19 skipped (`442e585e93`, after the merge); typecheck exit 0 (`41dbe7962f`). The pin files ran verbose: 2 files, 10 passed. - **metadata:** test 58 files, 871 passed; typecheck exit 0 (`41dbe7962f`). `plugin.test.ts` ran verbose: 18 passed (`442e585e93`). - **platform-objects:** test 67 files, 1076 passed (`26a4702abf`); typecheck exit 0 (`442e585e93`). - **spec:** - project `local`: 626 files, 18740 passed, 1 todo (`442e585e93`); - project `repo`: 54 files, 915 passed (`26a4702abf`); - typecheck: exit 0 (`442e585e93`); - `check:generated`: all 15 artifacts up to date after `spec` build (`41dbe7962f`). - **cli** (all at `26a4702abf`): - `--project unit`: 268 files, 3951 passed; - `--project integration` over the five edited integration files: 5 files, 63 passed; - typecheck: exit 0. - **runtime:** typecheck exit 0 (one comment edited). - **Dogfood** (`OS_TEST_SHARD=i/3`, `26a4702abf`, after a full `turbo run build --filter=!@objectstack/docs`, 72/72 tasks): - 1/3: 76 files, 567 passed; - 2/3: 76 files, 541 passed, 1 skipped; - 3/3: 74 files passed, 1 skipped, 662 passed, 8 skipped. **Gates.** - `node scripts/pm/dispatch-gates.mjs --commands` (no paths) at `26a4702abf` derived 128 commands. All 128 ran, all exited 0. `--ran` with the exit codes recorded answers "128 derived, 128 run, 0 NOT-MEASURED, 0 UNRUN". - Three of them needed the full build first. On the pre-merge head `41dbe7962f` they exited 3 (prerequisite not met) and are not counted; at `26a4702abf` they ran green. They are `check:skill-examples`, `check:dual-build-cjs-loads` and `check:i18n-coverage` ("13 config(s), … none new"). - Verdict lines: - `check:adr-0087-registration`: `[BREAKING+bang+clause-②-narrowing] registered sys-view-definition-retired (new here: sys-view-definition-retired)`; - `check:changeset-no-major`: "This diff introduces no `major` bump". The level axis is NOT APPLICABLE locally, because there is no `pull_request` payload; - `check:empty-changeset`: OK; - `check:platform-object-tenancy-census`: "83 platform-namespace objects, 48 in the machinery's reach, 35 outside it"; - `check:i18n`: all 9 packages in sync; - `check:platform-checklist`: OK; - `check:nul-bytes`: OK; - `spec check:api-surface`: unchanged; - `check:doc-authoring`: clean. **Lint.** This is a measured narrowing, not the CI-owned repo sweep. - **Population.** `eslint.config.mjs` matches `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. - **What ran.** The 38 added or modified `.ts`/`.mjs` files in this diff, `eslint --no-inline-config --format json`: 38 files, 0 errors, 0 warnings, exit 0. - **Why the rest cannot move.** The config never enables type-aware linting (`eslint.config.mjs:328`), so this diff cannot change the verdict on any untouched file. `tsc` is green across the packages that imported the deleted modules. ## Cross-lane paths These are declared on #6017 (`domain:spec`), #6024 (`domain:cli`) and #6023 (`domain:devx`). - **spec:** - `packages/spec/src/system/constants/platform-object-names.ts` and its test; - `packages/spec/src/migrations/entries/semantic/18.sys-view-definition-retired.ts`; - `packages/spec/src/migrations/registry.ts`. - **cli** (tests and comments): - `packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts`, `duplicates.contract.test.ts` and `duplicates.integration.test.ts`; - `packages/cli/src/utils/platform-migrations-arming.integration.test.ts` and `schema-migrate.host-composition.integration.test.ts`. - **runtime** (one comment): `packages/runtime/src/standalone-stack.ts`. - **devx:** - `content/docs/data-modeling/drivers.mdx`, `content/docs/deployment/cli.mdx` and `content/docs/plugins/packages.mdx`; - `docs/qa/platform-checklist/areas/cli.json`; - `scripts/platform-object-tenancy-census.json`; - `scripts/migrate/overlay-views-to-sys-view-definition.md` (deleted); - `.changeset/15206-sys-view-definition-retired.md`. ## Serial constraints `registry.ts` and the census were regenerated after this branch's final merge of `main` (`442e585e93`). Both came out byte-identical, with no text merge. After that merge, `main` gained four commits. None of them touches `registry.ts`, the census or any file in this diff (measured with `git diff --name-only`). This PR does not touch #22307's `objectql` `plugin.ts` or `registry.ts`. ## Acceptance notes - **Out of scope, reproducible:** the overlay pre-flight reports `blocked` for rows its index accepts. - **Setup.** On a SQLite database, insert two ACTIVE package-less `sys_metadata` overlays with the same type and name, both with `organization_id` NULL. - **What `os migrate duplicates` says.** `idx_sys_metadata_overlay_active` reads `status: blocked` with `organization_id: null` and `rowCount: 2`, and the summary reads `runtimeIndexesBlocked: 1`. - **What the index does.** The migration's own probe-name `CREATE UNIQUE INDEX … (type, name, organization_id, COALESCE(package_id, '')) WHERE state = 'active'` succeeds on the same file, because the organization key part is bare and NULL-distinct (#6418). The control pair, with organization `org_x`, is refused, `UNIQUE constraint failed`. - **Where the two disagree.** The duplicate probe's `GROUP BY` folds NULLs that the index keeps distinct. The pre-existing unit fixture in `runtime-index-preflight.test.ts` pins the NULL-organization group as `blocked`. - This goes to the seat to file. The dedupe words are in the report. - **Out of tree:** cloud references `sys_view_definition` in `control-plane-permissions.ts` and in its platform-global list (stage 0's census; ADR-0131 section 7). Cloud carries that as C10. There is no cloud checkout in this container, so its symbol imports are NOT MEASURED here. - **Left as historical:** the CHANGELOGs, ADR-0017, ADR-0029, ADR-0120 and ADR-0131, and the v17 release notes. ADR-0017's runbook pointer, written in backticks and not as a link, now names a deleted file. The ADR already carries the amendment note, and editing it is Tier H. - **Not run:** `cli/test/migrate-apply-refuses-before-ddl.e2e.test.ts`, which is unedited and carries one historical comment. Neither vitest project selects it as named (`FILTER SELECTED NOTHING`). --- _Generated by [Claude Code](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bb4f5cc commit 117d34d

49 files changed

Lines changed: 518 additions & 1964 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/metadata-core': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/metadata': minor
5+
'@objectstack/platform-objects': minor
6+
'@objectstack/spec': minor
7+
---
8+
9+
feat(metadata-core,metadata-protocol,metadata,platform-objects,spec)!: `sys_view_definition` retires as inert (ADR-0131 D13)
10+
11+
Clause-②: no (narrowing)
12+
13+
<!-- adr-0087: registered sys-view-definition-retired -->
14+
15+
**BREAKING**, graded `minor` on the v18 prerelease line: Changesets is in pre mode with the tag `next`, and the fixed group is already majored by the line's opening marker, so this ships in an `18.0.0-next.N` like every other stage.
16+
17+
`sys_view_definition` was declared for runtime-authored shared and personal views (ADR-0017) and never used for them: no framework code writes or reads its rows, and the Studio console's view doors write the ADR-0005 `view` overlay in `sys_metadata` through the metadata API. ADR-0131 D13 retires it. A runtime-authored view is a `view` metadata item, written through `PUT /api/v1/meta/view/<name>` (the client's `meta.saveItem` for type `view`); nothing about that path changes.
18+
19+
**What moves for consumers.**
20+
21+
- **The object.** FROM `import { SysViewDefinitionObject } from '@objectstack/metadata-core'` (or from `@objectstack/platform-objects`, or its `/metadata` subpath) TO nothing: delete the import. Neither `MetadataPlugin` nor the metadata protocol assembly registers the object any more, so the generic data door no longer serves it.
22+
- **The migration exports** of `@objectstack/metadata-protocol`. FROM `ensureViewDefinitionActiveIndex`, `resolveIndexExec`, `buildActiveIndexSql`, `VIEW_DEFINITION_TABLE`, `VIEW_ACTIVE_INDEX_NAME`, `VIEW_ACTIVE_PROBE_INDEX_NAME`, `VIEW_ACTIVE_INDEX_COLUMNS`, `EnsureViewIndexLogger`, `EnsureViewIndexStatus` and `EnsureViewIndexResult` TO nothing: delete the import. `classifyIndexFailure` and the `IndexExec` type are still exported, unchanged, from the shared index-migration module.
23+
- **The name.** `PLATFORM_OBJECTS_BY_PACKAGE['metadata-core']` (`@objectstack/spec/system`) lists four objects, and `isPlatformProvidedObjectName` answers `false` for `sys_view_definition`. A stack that names it (a lookup target, a flow trigger, a permission entry, a platform-global declaration) is now flagged as naming an unknown object: remove the reference.
24+
- **`os migrate duplicates`.** `runtimeIndexPreflight` carries three entries (the two `sys_metadata` overlay indexes and `sys_setting`'s row identity), with no `idx_sys_view_def_active` entry. A serving boot no longer runs the active-row index migration for the table, and no longer logs its `sys_view_definition active-row index` lines.
25+
- **Translations.** The platform translation bundles no longer carry `sys_view_definition` keys.
26+
27+
**Existing databases.** Schema sync is additive and never drops a table, so a database an earlier release provisioned keeps `sys_view_definition`, with any rows a caller wrote through the generic data door; nothing reads them. `os migrate apply --allow-destructive` does not drop it either, because it reconciles declared objects only (measured on one database: it dropped an orphaned column of a declared table and left this table and its row in place). On a project with a host config, `os migrate plan` lists it among the platform-prefixed tables nothing declares. Export any row worth keeping; dropping the table is the operator's call, by hand: `DROP TABLE sys_view_definition`.

‎content/docs/data-modeling/drivers.mdx‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ log because they are worth knowing *before* you choose MySQL, not after.
376376
| Caveat | What MySQL does not give you | Way out |
377377
| :--- | :--- | :--- |
378378
| [`upsert` cannot honour a conflict target](#upsert-conflict-targets-the-target-mysql-cannot-honour) | `ON DUPLICATE KEY UPDATE` carries no target, so a merge can land on a UNIQUE key you never named. The driver refuses the call rather than let it. | Keep one UNIQUE key per table, or run the object on SQLite/PostgreSQL. |
379-
| [Three runtime uniqueness indexes cannot be built](#uniqueness-indexes-mysql-cannot-build) | Database enforcement of three platform integrity guarantees. The weaker index stays in force and every boot logs an `error`. | None in-dialect. Run the platform on SQLite/PostgreSQL for the guarantee. |
379+
| [Two platform tables' runtime uniqueness indexes cannot be built](#uniqueness-indexes-mysql-cannot-build) | Database enforcement of two platform integrity guarantees. The weaker index stays in force and every boot logs an `error`. | None in-dialect. Run the platform on SQLite/PostgreSQL for the guarantee. |
380380
| [A unique violation does not name the column](#the-conflicting-column-is-not-named) | The conflicting **field** in import errors and form-field conflict messages. The `409 UNIQUE_VIOLATION` itself is unaffected. | None. The message falls back to generic copy. |
381381

382382
### `upsert` conflict targets: the target MySQL cannot honour
@@ -471,24 +471,23 @@ object on SQLite/PostgreSQL.
471471

472472
### Uniqueness indexes MySQL cannot build
473473

474-
Three platform tables get their uniqueness from an index issued as raw SQL at
474+
Two platform tables get their uniqueness from an index issued as raw SQL at
475475
boot by a `metadata-protocol` runtime migration, because the shape each one needs
476-
cannot be expressed through the declaration surface. Two of the three are
477-
**partial** indexes (`CREATE UNIQUE INDEX … WHERE state = '…'`), and all three
476+
cannot be expressed through the declaration surface. `sys_metadata`'s are
477+
**partial** indexes (`CREATE UNIQUE INDEX … WHERE state = '…'`), and both tables'
478478
use **functional key parts** (`COALESCE(…)`) to fold a nullable column's NULLs
479479
into one bucket that is unique among itself.
480480

481481
MySQL/MariaDB has no partial indexes at any version, and rejects the functional
482-
key parts as these statements spell them. So on MySQL none of the three is built,
483-
and the guarantee each one backs is not enforced by the database:
482+
key parts as these statements spell them. So on MySQL neither is built, and the
483+
guarantee each one backs is not enforced by the database:
484484

485485
| Table | The guarantee that is not enforced on MySQL |
486486
| :--- | :--- |
487487
| `sys_metadata` | ADR-0005 overlay uniqueness. Package-less rows (`package_id` NULL) stay NULL-distinct and can duplicate, and `getMetaItem` then has no defined answer for which row wins. |
488-
| `sys_view_definition` | Active-row view-name uniqueness. An archived view keeps occupying its name slot, and two same-name **active** shared views (`owner` NULL) or environment-level views (`organization_id` NULL) can coexist even though the platform states they cannot. |
489488
| `sys_setting` | NULL-safe row identity. `user_id` is NULL on every row that is not `scope='user'`, so two tenant-scope rows for one `(namespace, key)` in one organization — or two platform defaults on the global layer — can coexist, and `SettingsService` has no defined answer for which one wins. |
490489

491-
What happens instead is the same in all three cases, and it is deliberate. Each
490+
What happens instead is the same in both cases, and it is deliberate. Each
492491
migration proves the new index is possible under a throwaway probe name *before*
493492
dropping anything, so a dialect that cannot take it is left holding **exactly the
494493
index it already had** — never an unconstrained table, and never a failed boot.
@@ -497,7 +496,7 @@ lists any rows already violating the guarantee.
497496

498497
<Callout type="warn">
499498
**There is no in-dialect fix**, and none is planned — the DDL these migrations
500-
need does not exist in MySQL/MariaDB. If you need these three guarantees enforced
499+
need does not exist in MySQL/MariaDB. If you need these guarantees enforced
501500
by the database, run the platform on SQLite or PostgreSQL. On MySQL, treat the
502501
boot's `[metadata-protocol] this database cannot build …` lines as expected
503502
rather than as a failure, and run the duplicate-listing query each one prints to

‎content/docs/deployment/cli.mdx‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1532,14 +1532,13 @@ The report carries a second section, `runtimeIndexPreflight`, answering a
15321532
different question: **will the next server start be able to finish tightening
15331533
the platform's own unique indexes?**
15341534
1535-
Three migrations run at `kernel:ready` on a serving boot (`os dev`, `os serve`,
1535+
Two migrations run at `kernel:ready` on a serving boot (`os dev`, `os serve`,
15361536
`os start`) and replace a declared UNIQUE index with the NULL-safe — and
1537-
sometimes active-rows-only — form it was always meant to have:
1537+
sometimes state-scoped — form it was always meant to have:
15381538
15391539
| Table | Index | What the tightening adds |
15401540
| :--- | :--- | :--- |
15411541
| `sys_metadata` | overlay `active` and `draft` | package-less overlays stop being NULL-distinct |
1542-
| `sys_view_definition` | `idx_sys_view_def_active` | shared and environment-level views stop being NULL-distinct, and only active rows are constrained |
15431542
| `sys_setting` | the declared row identity | tenant- and global-scope rows stop being NULL-distinct on `user_id` |
15441543
15451544
Each is a **tightening**, so rows an installation already holds can block it.

‎content/docs/plugins/packages.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ import { InMemoryDriver } from '@objectstack/driver-memory';
156156

157157
- **Purpose**: Production-ready SQL database support with migrations
158158
- **Supports**: PostgreSQL, MySQL, SQLite, and all Knex-compatible databases
159-
- **MySQL caveats**: MySQL is supported, with three dialect caveats — `upsert` cannot honour a conflict target, three runtime uniqueness indexes cannot be built, and a unique violation does not name the conflicting column. See [Drivers → MySQL dialect caveats](/docs/data-modeling/drivers#mysql-dialect-caveats)
159+
- **MySQL caveats**: MySQL is supported, with three dialect caveats — `upsert` cannot honour a conflict target, two platform tables' runtime uniqueness indexes cannot be built, and a unique violation does not name the conflicting column. See [Drivers → MySQL dialect caveats](/docs/data-modeling/drivers#mysql-dialect-caveats)
160160
- **When to use**: Traditional relational database deployments
161161
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/drivers/driver-sql/README.md)
162162

‎docs/qa/platform-checklist/areas/cli.json‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@
11021102
"title": "os migrate duplicates: a read-only JSON inventory of identifiers minted across partitions — within-partition repeats excluded, nothing written, the live two-counter condition reported, and runnable BEFORE the #8686 repair destroys the evidence",
11031103
"since": "v17",
11041104
"status": "active",
1105-
"revision": 5,
1105+
"revision": 6,
11061106
"priority": "P1",
11071107
"surface": "cli",
11081108
"personas": ["operator (local shell, pre-repair audit)"],
@@ -1122,7 +1122,7 @@
11221122
"seed via direct SQL per the fixture recipe: the cross-partition duplicate, the within-partition repeat, an organizations row for '<org>', and the paired sequence counters",
11231123
"dump the DB's CONTENT before the run — every user table's rows (sqlite3 <db> '.dump' with the CREATE lines aside, or one ordered SELECT * per table), `SELECT * FROM _objectstack_sequences ORDER BY 1`, and `SELECT type, name, tbl_name, sql FROM sqlite_master ORDER BY name` — and checksum each dump; run `os migrate duplicates > report.json; echo $?`; dump and checksum again and byte-compare the three pairs. ⛔ Do not use the whole-file md5 as the oracle: it is not what the read-only contract promises (rows, counters and schema)",
11241124
"jq the report: .report/.reportVersion/.generatedAt/.database/.globalPartition/.filter/.counters/.scanned/.skipped/.duplicates/.liveConditions/.runtimeIndexPreflight/.summary",
1125-
"seed the kernel:ready blocker too (#8725): two ACTIVE sys_view_definition rows with the SAME name and organization_id/owner both NULL — then jq .runtimeIndexPreflight and .summary.runtimeIndexesBlocked",
1125+
"seed the kernel:ready blocker too (#8725): the first boot already TIGHTENED idx_sys_metadata_overlay_active, so first put back the declared NULL-distinct form an untightened install still has (DROP INDEX idx_sys_metadata_overlay_active, then CREATE UNIQUE INDEX idx_sys_metadata_overlay_active ON sys_metadata (type, name, organization_id, package_id)); then insert two ACTIVE sys_metadata rows with the SAME type and name, the SAME non-NULL organization_id and package_id NULL, every other NOT NULL column filled — then jq .runtimeIndexPreflight and .summary.runtimeIndexesBlocked",
11261126
"run `os migrate duplicates --object <the-object>` and `--object <an-object-with-no-findings>` — capture .filter in both payloads",
11271127
"run `os migrate duplicates --database-url file:/tmp/<run>/dup.db` and confirm it reaches the same DB (the flag also honors OS_DATABASE_URL)",
11281128
"negative (no_sql_seam): run `pnpm --filter @objectstack/cli exec vitest run src/commands/migrate/duplicates.null-seam.test.ts` and cite its pass — the seam double that answers no result set, driving the real command; then run `os migrate duplicates --database-url memory://qa; echo $?` and capture the boot_failed payload (the retired engine is refused at boot, naming the replacement)",
@@ -1148,9 +1148,9 @@
11481148
"evidence": "the three before/after dump-checksum pairs (rows, _objectstack_sequences, sqlite_master)"
11491149
},
11501150
{
1151-
"clause": "the kernel:ready pre-flight (#8725) reports the RUNTIME-migration class os migrate plan cannot see: one entry per index the three kernel:ready migrations tighten (four — the overlay migration owns two), each blocked|clear|table-absent|unreadable, a blocked one naming every colliding key group and its row count",
1151+
"clause": "the kernel:ready pre-flight (#8725) reports the RUNTIME-migration class os migrate plan cannot see: one entry per index the two kernel:ready migrations tighten (three — the overlay migration owns two; the third migration, sys_view_definition's, retired with its table under ADR-0131 D13), each blocked|clear|table-absent|unreadable, a blocked one naming every colliding key group and its row count",
11521152
"oracle": "log",
1153-
"verify": ".runtimeIndexPreflight names idx_sys_view_def_active as blocked with the seeded view name, organization_id_key '__global__' and owner_key '' — and the SAME database run through `os migrate plan` mentions neither the index nor the view name (the matched control: the declared-index duplicate above IS reported by plan, this one is not)",
1153+
"verify": ".runtimeIndexPreflight names idx_sys_metadata_overlay_active as blocked with the seeded type, name and organization_id and package_id_key '' — and the SAME database run through `os migrate plan` mentions neither the index nor the seeded name (the matched control: the declared-index duplicate above IS reported by plan, this one is not); no entry names sys_view_definition",
11541154
"evidence": "the pre-flight section beside the plan output for one database"
11551155
},
11561156
{
@@ -1201,6 +1201,12 @@
12011201
"date": "2026-10-04",
12021202
"change": "A3 and its step re-pointed: the read-only oracle is the DB's content — rows, _objectstack_sequences and sqlite_master byte-identical before and after — not the whole-file md5, which is not what the contract promises (duplicates.ts header: 'a full run leaves both the rows and `_objectstack_sequences` byte-identical'; duplicates.pre-repair.test.ts pins the same). Assertion defect. Source duplicates.ts contract header",
12031203
"ref": "#21735"
1204+
},
1205+
{
1206+
"revision": 6,
1207+
"date": "2026-10-08",
1208+
"change": "the kernel:ready blocker re-seeded on sys_metadata: sys_view_definition retired as inert with its active-row index migration (ADR-0131 D13), so the pre-flight now probes three indexes from two migrations and never names that table. The seeding step and A4 now use two ACTIVE package-less sys_metadata overlays for one type, name and NON-NULL organization — the organization key part stays bare in the tightened index (#6418), so a NULL-organization pair would not block its CREATE — and the step first puts back the declared NULL-distinct index the first boot replaced, without which the insert is refused. Source runtime-index-preflight.ts runtimeIndexProbes",
1209+
"ref": "#15206"
12041210
}
12051211
]
12061212
},

‎packages/cli/src/commands/migrate/data-commands.absent-database.integration.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ const OWN_TABLES: Record<CommandName, readonly string[]> = {
207207
resume: ['sys_migration_journal'],
208208
'recorded-by': ['sys_metadata_history'],
209209
// The objects with a covered field: the scan walks each.
210-
'value-shapes': ['os21529_contact', 'sys_metadata', 'sys_view_definition'],
210+
'value-shapes': ['os21529_contact', 'sys_metadata'],
211211
};
212212

213213
const absentJson = {} as Record<CommandName, Run>;

‎packages/cli/src/commands/migrate/duplicates.contract.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,15 @@ const collect = async (objectFilter?: string) =>
134134
client: 'better-sqlite3',
135135
now: () => new Date('2026-08-17T12:00:00.000Z'),
136136
// The real pre-flight against the real fixture — never a stand-in. This
137-
// database has none of the four platform tables, so every entry is
137+
// database has none of the platform tables the probes read, so every entry is
138138
// `table-absent`, and the `blocked` shape is pinned in its own test below
139139
// over a database that really carries the damage.
140140
runtimeIndexPreflight: await collectRuntimeIndexPreflight(exec, { client: 'better-sqlite3' }),
141141
...(objectFilter ? { objectFilter } : {}),
142142
});
143143

144144
/**
145-
* The four probes, as `@objectstack/metadata-protocol` declares them.
145+
* The three probes, as `@objectstack/metadata-protocol` declares them.
146146
*
147147
* Read from the producer rather than restated here: the descriptor (table,
148148
* index name, key parts, row scope, the two statements) is the migration's own
@@ -231,7 +231,7 @@ describe('#8928 os migrate duplicates — the report document', () => {
231231
organizationCounters: [{ organization: 'org_x', lastValue: 4 }],
232232
},
233233
],
234-
// One entry per index the three `kernel:ready` migrations tighten — FOUR,
234+
// One entry per index the two `kernel:ready` migrations tighten — THREE,
235235
// because the overlay migration builds one per state and either can be
236236
// blocked on its own. Present whatever the outcome: an index left out
237237
// would make "nothing blocks it" and "it was never probed" the same
@@ -253,14 +253,13 @@ describe('#8928 os migrate duplicates — the report document', () => {
253253
});
254254
});
255255

256-
it('names the four kernel:ready indexes, and the migration behind each', async () => {
256+
it('names the three kernel:ready indexes, and the migration behind each — none for the retired sys_view_definition (ADR-0131 D13)', async () => {
257257
const report = await collect();
258258
expect(
259259
report.runtimeIndexPreflight.map((p) => `${p.migration}:${p.table}:${p.index}`),
260260
).toEqual([
261261
'ensureMetadataOverlayIndexes:sys_metadata:idx_sys_metadata_overlay_active',
262262
'ensureMetadataOverlayIndexes:sys_metadata:idx_sys_metadata_overlay_draft',
263-
'ensureViewDefinitionActiveIndex:sys_view_definition:idx_sys_view_def_active',
264263
'ensureSysSettingIdentityIndex:sys_setting:uniq_sys_setting_organization_id_namespace_key_scope_user_id',
265264
]);
266265
});

0 commit comments

Comments
 (0)