Skip to content

Commit 4e0f473

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22274-option-visible-when-members
2 parents f270d45 + 117d34d commit 4e0f473

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)