From 7ab8c23e0c58c5b3409ef6a9836d3fe53a014f26 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 06:09:48 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(spec)!:=20=E9=80=80=E5=BD=B9=20DriverC?= =?UTF-8?q?apabilities=2031=20=E4=B8=AA=E9=9B=B6=E8=AF=BB=E8=80=85?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=E4=BD=8D=20=E2=80=94=E2=80=94=20=E5=85=A8?= =?UTF-8?q?=E8=A1=A8=E6=B4=BB=E6=80=A7=E5=AE=A1=E8=AE=A1,3=20=E6=B4=BB=203?= =?UTF-8?q?1=20=E6=AD=BB=20(#4634)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #4484 findStream 收尾发现 streaming 位指向已删除的契约方法;按 issue 自述倾向对 DriverCapabilitiesSchema 全部 34 位做两仓活性审计(objectstack + cloud,objectui 零引用确认):仅 queryDateGranularity(engine 聚合分派 + date-bucket parity)、 autonumber(engine 让渡自增生成)、batchSchemaSync(engine 与方法存在性 AND) 三位有决策读者;其余 31 位人人写、无人读,describe 承诺的引擎降级路径从未存在, 且零读者让写错的值(SqlDriver streaming:false 却实现 findStream;InMemoryDriver streaming:true 却全表物化)三个版本无人发现 —— ADR-0078 假可供性。 - driver.zod.ts:31 位 retiredKey() 墓碑(非 strict schema,裸删会被静默剥离; DriverConfigSchema.capabilities 及 SQL/NoSQL 扩展确有 parse 路径),逐位 prescription 指明真实机制(方法存在性门控:beginTransaction / aggregate / syncSchema;#4001 教训);batchSchemaSync .default(false) → .optional() (两处读者本就按缺省=false 消费) - ADR-0087 评估(#4484/#4764 先例):driver 是代码不是 stack 树元数据, supports 字面量在 driver 类里、DriverConfig 是插件 TS 配置,均非 sys_metadata 形态(stack 树近邻 datasource.capabilities 已于 #4583 单独退役)⇒ 不登记 D2 conversion,登记 D3 semantic migration driver-capabilities-inert-bits-removed - driver 字面量同步:memory {} / mongodb { batchSchemaSync: true } / sql { queryDateGranularity, autonumber, batchSchemaSync: false } - 回归 pin(driver.test.ts):31 位逐位 parse 拒绝 + prescription 断言; compiler-API 断言 34 键形状且退役位类型坍缩为 undefined、活位可写(反空转 守卫);S1(schema 复活 streaming)5 pin 红、S2(driver 字面量复活)TS2416 红,均实跑验证后还原 - 生成物:authorable-surface 31 行 [RETIRED]、spec-changes / upgrade-guide / references docs 全量再生成;14 项 spec 门禁全绿,全仓 typecheck 122 任务绿 - changeset:spec + 三 driver 包 major,FROM→TO 映射与一行修复齐备 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- .../driver-capabilities-inert-bits-removed.md | 78 ++ .../docs/protocol/objectql/query-syntax.mdx | 5 +- content/docs/references/data/driver-nosql.mdx | 2 +- content/docs/references/data/driver-sql.mdx | 2 +- content/docs/references/data/driver.mdx | 66 +- docs/protocol-upgrade-guide.md | 3 + .../driver-memory/src/memory-driver.test.ts | 7 +- .../driver-memory/src/memory-driver.ts | 57 +- .../driver-mongodb/src/mongodb-driver.ts | 52 +- .../src/sql-driver-queryast.test.ts | 7 +- .../driver-sql/src/sql-driver-schema.test.ts | 6 +- packages/plugins/driver-sql/src/sql-driver.ts | 55 +- .../src/sqlite-wasm-driver-queryast.test.ts | 5 +- packages/spec/authorable-surface.json | 62 +- packages/spec/spec-changes.json | 14 + .../spec/src/contracts/data-driver.test.ts | 109 +-- .../spec/src/contracts/data-engine.test.ts | 40 +- packages/spec/src/data/driver-nosql.test.ts | 26 +- packages/spec/src/data/driver-nosql.zod.ts | 55 +- packages/spec/src/data/driver-sql.test.ts | 102 +-- packages/spec/src/data/driver-sql.zod.ts | 37 +- packages/spec/src/data/driver.test.ts | 800 +++++------------- packages/spec/src/data/driver.zod.ts | 379 ++++----- packages/spec/src/migrations/registry.ts | 64 ++ 24 files changed, 723 insertions(+), 1310 deletions(-) create mode 100644 .changeset/driver-capabilities-inert-bits-removed.md diff --git a/.changeset/driver-capabilities-inert-bits-removed.md b/.changeset/driver-capabilities-inert-bits-removed.md new file mode 100644 index 0000000000..69430757c3 --- /dev/null +++ b/.changeset/driver-capabilities-inert-bits-removed.md @@ -0,0 +1,78 @@ +--- +"@objectstack/spec": major +"@objectstack/driver-memory": major +"@objectstack/driver-mongodb": major +"@objectstack/driver-sql": major +--- + +refactor(spec)!: retire the 31 inert `DriverCapabilities` bits — declared by every driver, read by nothing (#4634, ADR-0049) + +The #4484 findStream close-out left one loose end: `DriverCapabilities.streaming` +described a contract method that no longer exists — and a full liveness audit of +the record (#4634, across objectstack + cloud, objectui confirmed clean) found +`streaming` was not the exception but the rule. Of 34 declared bits, **three** +have a decision-making reader and **thirty-one** were written by every driver +and consulted by no engine, planner, REST layer or renderer: + +- Their `.describe()` strings promised engine adaptation that was never built + ("If false, ObjectQL will fetch all records and filter in memory" — no such + fallback ever keyed off the bit). +- Zero readers let values go WRONG unnoticed: `SqlDriver` declared + `streaming: false` while implementing `findStream`; `InMemoryDriver` declared + `streaming: true` over a full-table read — the exact inverse of the guarantee. +- The real mechanism everywhere else is **method presence**: transactions gate + on `driver.beginTransaction`, aggregate pushdown on + `typeof driver.aggregate === 'function'`, schema sync on + `typeof driver.syncSchema === 'function'`, and the REQUIRED CRUD/bulk methods + are called unconditionally. + +Survivors (each with a named reader — the bits method presence cannot carry): + +| bit | reader | +|---|---| +| `queryDateGranularity` | engine aggregate dispatch (`engine.ts`), `checkDateBucketParity` (`@objectstack/verify`) | +| `autonumber` | engine defers autonumber generation to the driver (`engine.ts`) | +| `batchSchemaSync` | engine ANDs it with `syncSchemasBatch` presence (`engine.ts` / `plugin.ts`) | + +Migration (FROM → TO): + +- Any of the 31 bits (`create`/`read`/`update`/`delete`, `bulkCreate`/ + `bulkUpdate`/`bulkDelete`, `transactions`/`savepoints`/`isolationLevels`, + `queryFilters`/`queryAggregations`/`querySorting`/`queryPagination`/ + `queryWindowFunctions`/`querySubqueries`/`queryCTE`/`joins`, + `fullTextSearch`/`jsonQuery`/`geospatialQuery`/`streaming`/`jsonFields`/ + `arrayFields`/`vectorSearch`, `schemaSync`/`migrations`/`indexes`, + `connectionPooling`/`preparedStatements`/`queryCache`) in a `supports` + literal or a `DriverConfig.capabilities` object → **delete the key**. Each is + tombstoned (`retiredKey()`), not silently stripped: authoring one is a `tsc` + error against `IDataDriver.supports` and a parse error carrying the per-key + prescription, which names the mechanism that actually decides the behaviour. +- `batchSchemaSync` dropped its `.default(false)` for `.optional()` — absence + already meant `false` at both readers, so `supports: {}` is now a valid, + minimal advertisement. If you read `capabilities.batchSchemaSync` from a + *parsed* config and relied on the materialised `false`, treat absence as + `false` (both engine readers always did). +- Driver packages: `InMemoryDriver.supports` is now `{}`, + `MongoDBDriver.supports` is `{ batchSchemaSync: true }`, `SqlDriver.supports` + is `{ queryDateGranularity, autonumber: true, batchSchemaSync: false }`. + Reading a removed bit off these literals no longer type-checks — and no code + in any repository did. +- A future capability (streaming reads, vector search, …) returns **with its + caller and its reader in the same change** — the enforce route of ADR-0049 — + never as a dangling boolean. + +The retirement kit: 31 `retiredKey()` tombstones on the non-strict schema +(parse + `tsc` both audible; the schema IS parsed via +`DriverConfigSchema.capabilities` and its SQL/NoSQL extensions); ADR-0087 D3 +semantic migration `driver-capabilities-inert-bits-removed` (a driver is CODE, +never stack metadata — `supports` lives in driver classes and `DriverConfig` +is plugin TS configuration, so there is no stored row or stack source for a D2 +conversion to rewrite; the stack-tree neighbour `datasource.capabilities` was +retired separately in #4583); baselines (`authorable-surface.json` [RETIRED] +lines, `json-schema.manifest.json`) regenerated deliberately; compiler-API pin +asserting every retired bit is unwritable (`undefined`) and every live bit is +not, sabotage-verified both ways (S1 schema resurrection, S2 driver literal +resurrection). + +No runtime behaviour changes — that impossibility is the point: every removed +bit had zero readers, and the three live bits keep theirs. diff --git a/content/docs/protocol/objectql/query-syntax.mdx b/content/docs/protocol/objectql/query-syntax.mdx index 1dd4344cf5..a113fe4385 100644 --- a/content/docs/protocol/objectql/query-syntax.mdx +++ b/content/docs/protocol/objectql/query-syntax.mdx @@ -749,8 +749,9 @@ form. The `search` parameter does **not** reach a full-text index. The engine expands it into an `$or` of `$contains` predicates across the object's server-resolved searchable fields (ADR-0061) and deletes `search` from the AST before the driver sees it — every driver -already runs `$or`/`$contains`, so no driver support is needed (`SqlDriver` reports -`supports.fullTextSearch: false`). +already runs `$or`/`$contains`, so no driver support is needed (which is also why the +old `supports.fullTextSearch` capability bit had no reader and was retired in 17.0.0, +#4634). `search` takes the **query text itself** — that is the canonical spelling (ADR-0061 D1: the client says *what* to search for, the server decides *which fields*), and it is what diff --git a/content/docs/references/data/driver-nosql.mdx b/content/docs/references/data/driver-nosql.mdx index 081c4beaad..57a886ffe9 100644 --- a/content/docs/references/data/driver-nosql.mdx +++ b/content/docs/references/data/driver-nosql.mdx @@ -123,7 +123,7 @@ const result = AggregationPipelineSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Driver instance name | | **type** | `'nosql'` | ✅ | Driver type must be "nosql" | -| **capabilities** | `{ create: boolean; read: boolean; update: boolean; delete: boolean; … }` | ✅ | Driver capability flags | +| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` | ✅ | Driver capability flags | | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | | **databaseType** | `Enum<'mongodb' \| 'couchdb' \| 'dynamodb' \| 'cassandra' \| 'redis' \| 'elasticsearch' \| 'neo4j' \| 'orientdb'>` | ✅ | Specific NoSQL database type | diff --git a/content/docs/references/data/driver-sql.mdx b/content/docs/references/data/driver-sql.mdx index 9298070587..3fb2bf90d3 100644 --- a/content/docs/references/data/driver-sql.mdx +++ b/content/docs/references/data/driver-sql.mdx @@ -65,7 +65,7 @@ const result = DataTypeMappingSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Driver instance name | | **type** | `'sql'` | ✅ | Driver type must be "sql" | -| **capabilities** | `{ create: boolean; read: boolean; update: boolean; delete: boolean; … }` | ✅ | Driver capability flags | +| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` | ✅ | Driver capability flags | | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | | **dialect** | `Enum<'postgresql' \| 'mysql' \| 'sqlite' \| 'mssql' \| 'oracle' \| 'mariadb'>` | ✅ | SQL database dialect | diff --git a/content/docs/references/data/driver.mdx b/content/docs/references/data/driver.mdx index df2f1c7d46..1314091a62 100644 --- a/content/docs/references/data/driver.mdx +++ b/content/docs/references/data/driver.mdx @@ -31,40 +31,40 @@ const result = DriverCapabilitiesSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **create** | `boolean` | ✅ | Supports CREATE operations | -| **read** | `boolean` | ✅ | Supports READ operations | -| **update** | `boolean` | ✅ | Supports UPDATE operations | -| **delete** | `boolean` | ✅ | Supports DELETE operations | -| **bulkCreate** | `boolean` | ✅ | Supports bulk CREATE operations | -| **bulkUpdate** | `boolean` | ✅ | Supports bulk UPDATE operations | -| **bulkDelete** | `boolean` | ✅ | Supports bulk DELETE operations | -| **transactions** | `boolean` | ✅ | Supports ACID transactions | -| **savepoints** | `boolean` | ✅ | Supports transaction savepoints | -| **isolationLevels** | `Enum<'read_uncommitted' \| 'read_committed' \| 'repeatable_read' \| 'serializable' \| 'snapshot'>[]` | optional | Supported isolation levels | -| **queryFilters** | `boolean` | ✅ | Supports WHERE clause filtering | -| **queryAggregations** | `boolean` | ✅ | Supports GROUP BY and aggregation functions | | **queryDateGranularity** | `Record` | optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. | -| **querySorting** | `boolean` | ✅ | Supports ORDER BY sorting | -| **queryPagination** | `boolean` | ✅ | Supports LIMIT/OFFSET pagination | -| **queryWindowFunctions** | `boolean` | ✅ | Supports window functions with OVER clause | -| **querySubqueries** | `boolean` | ✅ | Supports subqueries | -| **queryCTE** | `boolean` | ✅ | Supports Common Table Expressions (WITH clause) | -| **joins** | `boolean` | ✅ | Supports SQL joins | -| **fullTextSearch** | `boolean` | ✅ | Supports full-text search | -| **jsonQuery** | `boolean` | ✅ | Supports JSON field querying | -| **geospatialQuery** | `boolean` | ✅ | Supports geospatial queries | -| **streaming** | `boolean` | ✅ | Supports result streaming (cursors/iterators) | -| **jsonFields** | `boolean` | ✅ | Supports JSON field types | -| **arrayFields** | `boolean` | ✅ | Supports array field types | -| **vectorSearch** | `boolean` | ✅ | Supports vector embeddings and similarity search | | **autonumber** | `boolean` | optional | Driver natively generates persistent autonumber/sequence values | -| **schemaSync** | `boolean` | ✅ | Supports automatic schema synchronization | -| **batchSchemaSync** | `boolean` | ✅ | Supports batched schema sync to reduce schema DDL round-trips | -| **migrations** | `boolean` | ✅ | Supports database migrations | -| **indexes** | `boolean` | ✅ | Supports index creation and management | -| **connectionPooling** | `boolean` | ✅ | Supports connection pooling | -| **preparedStatements** | `boolean` | ✅ | Supports prepared statements (SQL injection prevention) | -| **queryCache** | `boolean` | ✅ | Supports query result caching | +| **batchSchemaSync** | `boolean` | optional | Supports batched schema sync to reduce schema DDL round-trips (absence = false) | +| **create** | `any` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. | +| **read** | `any` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. | +| **update** | `any` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. | +| **delete** | `any` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. | +| **bulkCreate** | `any` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkUpdate** | `any` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkDelete** | `any` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **transactions** | `any` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. | +| **savepoints** | `any` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. | +| **isolationLevels** | `any` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. | +| **queryFilters** | `any` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. | +| **querySorting** | `any` | optional | [REMOVED] `DriverCapabilities.querySorting` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will sort in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryPagination** | `any` | optional | [REMOVED] `DriverCapabilities.queryPagination` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will paginate in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryAggregations** | `any` | optional | [REMOVED] `DriverCapabilities.queryAggregations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Aggregate pushdown is decided by `typeof driver.aggregate === 'function'` plus `queryDateGranularity` (engine aggregate dispatch) — never by this bit. Delete the key. | +| **queryWindowFunctions** | `any` | optional | [REMOVED] `DriverCapabilities.queryWindowFunctions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans window functions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **querySubqueries** | `any` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **queryCTE** | `any` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **joins** | `any` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | +| **fullTextSearch** | `any` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **jsonQuery** | `any` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | +| **geospatialQuery** | `any` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | +| **streaming** | `any` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | +| **jsonFields** | `any` | optional | [REMOVED] `DriverCapabilities.jsonFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself (e.g. `SqlDriver`'s per-object JSON/date column tracking); no engine path consulted the bit. Delete the key. | +| **arrayFields** | `any` | optional | [REMOVED] `DriverCapabilities.arrayFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself; no engine path consulted the bit. Delete the key. | +| **vectorSearch** | `any` | optional | [REMOVED] `DriverCapabilities.vectorSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No vector read path routes through `IDataDriver`. When one exists it should arrive WITH its caller and its capability bit together (the honest order under enforce-or-remove), not as a dangling boolean. Delete the key. | +| **schemaSync** | `any` | optional | [REMOVED] `DriverCapabilities.schemaSync` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === 'function'` (engine and ObjectQL plugin init). Delete the key. | +| **migrations** | `any` | optional | [REMOVED] `DriverCapabilities.migrations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No migration engine ever consulted it. Delete the key. | +| **indexes** | `any` | optional | [REMOVED] `DriverCapabilities.indexes` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Declared indexes are materialised by the driver itself during schema sync (`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit. Delete the key. | +| **connectionPooling** | `any` | optional | [REMOVED] `DriverCapabilities.connectionPooling` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is duck-typed where monitoring wants it. Nothing consulted the bit. Delete the key. | +| **preparedStatements** | `any` | optional | [REMOVED] `DriverCapabilities.preparedStatements` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Parameterised execution is an implementation detail of the driver (`execute(command, parameters)`); nothing consulted the bit. Delete the key. | +| **queryCache** | `any` | optional | [REMOVED] `DriverCapabilities.queryCache` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint to the driver, not a switch on this bit. Delete the key. | --- @@ -77,7 +77,7 @@ const result = DriverCapabilitiesSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Driver instance name | | **type** | `Enum<'sql' \| 'nosql' \| 'cache' \| 'search' \| 'graph' \| 'timeseries'>` | ✅ | Driver type category | -| **capabilities** | `{ create: boolean; read: boolean; update: boolean; delete: boolean; … }` | ✅ | Driver capability flags | +| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` | ✅ | Driver capability flags | | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index fa11352c61..b5a3e265c4 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -271,6 +271,9 @@ The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block ( - **`rest-server-openapi31-block-removed`** — `restServer.openApi31` → (removed — no replacement key exists. Delete the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. Config-driven OpenAPI 3.1 webhooks/callbacks documentation returns, if ever, via the enforce route of ADR-0049 through a new ADR) - Why not automatic: The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with `OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 document synthesis nothing delivered: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated @objectstack/spec contract enriched with the live server URL and the registered objects — a webhook declared here never appeared in any served document (ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never a `sys_metadata` shape — the stack tree's `api` block declares only its four scoping/auth knobs. The three schemas are removed with the key (zero import-level consumers in objectstack / cloud / objectui); the key itself is tombstoned because the schema is not `.strict()` and a plain delete would strip it silently. #4579. - Done when: No `RestServerConfig` value passed to the REST plugin (or `plugin-hono-server` `restConfig`) carries `openApi31` — a config that includes it now fails the parse with the retirement prescription instead of being silently stripped. No code imports `OpenApi31Extensions(Schema)`, `Callback(Schema)` or `OpenApiWebhookEvent(Schema)` from `@objectstack/spec/api` (TS2305 after upgrade). The served /openapi.json is byte-identical before and after — the block never reached it. +- **`driver-capabilities-inert-bits-removed`** — `data.DriverCapabilities.create / data.DriverCapabilities.read / data.DriverCapabilities.update / data.DriverCapabilities.delete / data.DriverCapabilities.bulkCreate / data.DriverCapabilities.bulkUpdate / data.DriverCapabilities.bulkDelete / data.DriverCapabilities.transactions / data.DriverCapabilities.savepoints / data.DriverCapabilities.isolationLevels / data.DriverCapabilities.queryFilters / data.DriverCapabilities.queryAggregations / data.DriverCapabilities.querySorting / data.DriverCapabilities.queryPagination / data.DriverCapabilities.queryWindowFunctions / data.DriverCapabilities.querySubqueries / data.DriverCapabilities.queryCTE / data.DriverCapabilities.joins / data.DriverCapabilities.fullTextSearch / data.DriverCapabilities.jsonQuery / data.DriverCapabilities.geospatialQuery / data.DriverCapabilities.streaming / data.DriverCapabilities.jsonFields / data.DriverCapabilities.arrayFields / data.DriverCapabilities.vectorSearch / data.DriverCapabilities.schemaSync / data.DriverCapabilities.migrations / data.DriverCapabilities.indexes / data.DriverCapabilities.connectionPooling / data.DriverCapabilities.preparedStatements / data.DriverCapabilities.queryCache` → (removed — delete the keys. A driver advertises a capability by implementing the corresponding IDataDriver method; the three bits that survive because method presence cannot carry the signal are `queryDateGranularity`, `autonumber` and `batchSchemaSync`) + - Why not automatic: The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a capability the contract no longer declares, and the follow-up audit (#4634) checked every bit in the record the same way, across objectstack and cloud (objectui confirmed clean): of 34 declared bits, THREE have a decision-making reader — `queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), `autonumber` (engine defers generation to the driver), `batchSchemaSync` (engine ANDs it with method presence, because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot) — and THIRTY-ONE were written by every driver and read by nothing. Their `.describe()` strings promised engine adaptation ("if false, ObjectQL will filter/sort/paginate in memory") that was never built, and zero readers let the values go WRONG unnoticed: SqlDriver declared `streaming: false` while implementing `findStream`; InMemoryDriver declared `streaming: true` over a full-table read (ADR-0078 false affordance, on the capability record itself). The real mechanism everywhere else is METHOD presence: transactions gate on `driver.beginTransaction`, aggregate pushdown on `typeof driver.aggregate`, schema sync on `typeof driver.syncSchema`, and the REQUIRED CRUD/bulk methods are called unconditionally. A driver is CODE, never stack metadata — `supports` literals live in driver classes and `DriverConfig.capabilities` is plugin TS configuration, neither ever a `sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was retired separately in #4583) — so there is no source for the D2 chain to rewrite and this entry is the D3 record. The keys are tombstoned rather than deleted because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed (DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): a plain delete would silently strip a vendor's authored bit, replacing one silent no-op with another. `batchSchemaSync` also drops its `.default(false)` for `.optional()` — absence already meant false at both readers, and the default forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634. + - Done when: No `supports` literal or `DriverConfig.capabilities` object authors any of the 31 retired bits — a driver class that still writes one fails tsc against `IDataDriver.supports` (the bit is `never`), and a parsed config fails with the per-key prescription. The three in-repo drivers (memory / mongodb / sql) declare only live bits; cloud's TursoDriver keeps compiling via its `...super.supports` spread (its stale explicit overrides are cleanup, tracked cloud-side). Engine behaviour is byte-identical: every removed bit had zero readers, and the three live bits keep their readers (engine.ts autonumber defer / aggregate dispatch, plugin.ts + engine.ts batched schema sync, verify date-bucket parity). --- diff --git a/packages/plugins/driver-memory/src/memory-driver.test.ts b/packages/plugins/driver-memory/src/memory-driver.test.ts index 126d3bf40b..880538e18e 100644 --- a/packages/plugins/driver-memory/src/memory-driver.test.ts +++ b/packages/plugins/driver-memory/src/memory-driver.test.ts @@ -260,8 +260,11 @@ describe('InMemoryDriver', () => { expect(results[0].name).toBe('Alice'); }); - it('should support capabilities.transactions = true', () => { - expect(driver.supports.transactions).toBe(true); + it('advertises no capability bits — transactions are expressed by the methods (#4634)', () => { + // `supports.transactions` was one of the 31 inert bits retired in #4634: + // the engine gates transaction use on `driver.beginTransaction` presence + // (proven by the rollback test above), never on a boolean. + expect(driver.supports).toEqual({}); }); }); diff --git a/packages/plugins/driver-memory/src/memory-driver.ts b/packages/plugins/driver-memory/src/memory-driver.ts index ed79255461..6c690a860b 100644 --- a/packages/plugins/driver-memory/src/memory-driver.ts +++ b/packages/plugins/driver-memory/src/memory-driver.ts @@ -169,52 +169,17 @@ export class InMemoryDriver implements IDataDriver { } } - readonly supports = { - // Basic CRUD Operations - create: true, - read: true, - update: true, - delete: true, - - // Bulk Operations - bulkCreate: true, - bulkUpdate: true, - bulkDelete: true, - - // Transaction & Connection Management - transactions: true, // Snapshot-based transactions - savepoints: false, - - // Query Operations - queryFilters: true, // Implemented via memory-matcher - queryAggregations: true, // Implemented - querySorting: true, // Implemented via JS sort - queryPagination: true, // Implemented - queryWindowFunctions: false, // @planned: Window functions (ROW_NUMBER, RANK, etc.) - querySubqueries: false, // @planned: Subquery execution - queryCTE: false, - joins: false, // @planned: In-memory join operations - - // Advanced Features - fullTextSearch: false, // @planned: Text tokenization + matching - jsonQuery: false, - geospatialQuery: false, - streaming: true, // Unread by anything; described findStream(), retired in #4484 — see #4634 - jsonFields: true, // Native JS object support - arrayFields: true, // Native JS array support - vectorSearch: false, // @planned: Cosine similarity search - - // Schema Management - schemaSync: true, // Implemented via syncSchema() - batchSchemaSync: false, - migrations: false, - indexes: false, - - // Performance & Optimization - connectionPooling: false, - preparedStatements: false, - queryCache: false, - }; + /** + * Capability advertisement (#4634, ADR-0049): only the bits with an engine + * reader survive, and this driver truthfully claims none of them — the + * engine's in-memory autonumber counter, in-memory date bucketing and + * per-object `syncSchema()` calls are exactly what it needs. Everything the + * old 30-bit literal declared (transactions, filters, sorting, …) is + * expressed by the methods this class implements; the bits were read by + * nothing and two of them were WRONG for years (`streaming: true` over a + * full-table read — see #4484/#4634). + */ + readonly supports = {}; /** * The "Database": A map of TableName -> Array of Records diff --git a/packages/plugins/driver-mongodb/src/mongodb-driver.ts b/packages/plugins/driver-mongodb/src/mongodb-driver.ts index 832bcf4329..70863646ae 100644 --- a/packages/plugins/driver-mongodb/src/mongodb-driver.ts +++ b/packages/plugins/driver-mongodb/src/mongodb-driver.ts @@ -82,51 +82,17 @@ export class MongoDBDriver implements IDataDriver { public readonly name: string = 'com.objectstack.driver.mongodb'; public readonly version: string = '1.0.0'; + /** + * Capability advertisement (#4634, ADR-0049): only the bits with an engine + * reader survive. This driver batches its schema DDL round-trips + * ({@link syncSchemasBatch}), so it opts in via the one bit the engine ANDs + * with method presence. It owns neither persistent autonumber sequences nor + * native date bucketing here, so `autonumber`/`queryDateGranularity` stay + * absent and the engine keeps its fallbacks. Everything the old 30-bit + * literal declared is expressed by the methods this class implements. + */ public readonly supports = { - // Basic CRUD Operations - create: true, - read: true, - update: true, - delete: true, - - // Bulk Operations - bulkCreate: true, - bulkUpdate: true, - bulkDelete: true, - - // Transaction & Connection Management - transactions: true, - savepoints: false, - - // Query Operations - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: false, - queryCTE: false, - joins: false, - - // Advanced Features - fullTextSearch: true, - jsonQuery: true, - geospatialQuery: true, - streaming: true, - jsonFields: true, - arrayFields: true, - vectorSearch: false, - - // Schema Management - schemaSync: true, batchSchemaSync: true, - migrations: false, - indexes: true, - - // Performance & Optimization - connectionPooling: true, - preparedStatements: false, - queryCache: false, }; private client: MongoClient; diff --git a/packages/plugins/driver-sql/src/sql-driver-queryast.test.ts b/packages/plugins/driver-sql/src/sql-driver-queryast.test.ts index df51ee937d..b2e99f5383 100644 --- a/packages/plugins/driver-sql/src/sql-driver-queryast.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-queryast.test.ts @@ -43,8 +43,11 @@ describe('SqlDriver (QueryAST Format)', () => { expect(driver.name).toBe('com.objectstack.driver.sql'); expect(driver.version).toBeDefined(); expect(driver.supports).toBeDefined(); - expect(driver.supports.transactions).toBe(true); - expect(driver.supports.joins).toBe(true); + // #4634: only the live capability bits remain — transactions/joins are + // expressed by the methods this driver implements. + expect(driver.supports.autonumber).toBe(true); + expect(driver.supports.batchSchemaSync).toBe(false); + expect(driver.supports).not.toHaveProperty('transactions'); }); }); diff --git a/packages/plugins/driver-sql/src/sql-driver-schema.test.ts b/packages/plugins/driver-sql/src/sql-driver-schema.test.ts index a7f133c8a6..c6556857af 100644 --- a/packages/plugins/driver-sql/src/sql-driver-schema.test.ts +++ b/packages/plugins/driver-sql/src/sql-driver-schema.test.ts @@ -468,8 +468,10 @@ describe('SqlDriver Schema Sync (SQLite)', () => { expect(warnings.some((w) => w.includes('total'))).toBe(true); }); - it('reports indexes capability as supported', () => { - expect((driver as any).supports.indexes).toBe(true); + it('no longer advertises an `indexes` capability bit (#4634) — syncDeclaredIndexes IS the capability', () => { + // The bit had zero readers; the declared-index materialisation above is the + // enforced mechanism, and this suite already proves it end to end. + expect((driver as any).supports).not.toHaveProperty('indexes'); }); it('should use short object name as physical table name in initObjects', async () => { diff --git a/packages/plugins/driver-sql/src/sql-driver.ts b/packages/plugins/driver-sql/src/sql-driver.ts index ce0bcba5a3..44a08ee743 100644 --- a/packages/plugins/driver-sql/src/sql-driver.ts +++ b/packages/plugins/driver-sql/src/sql-driver.ts @@ -512,64 +512,29 @@ export class SqlDriver implements IDataDriver { // IDataDriver metadata public readonly name: string = 'com.objectstack.driver.sql'; public readonly version: string = '1.0.0'; + /** + * Capability advertisement (#4634, ADR-0049): only the bits with an engine + * reader survive — everything the old 30-bit literal declared (transactions, + * joins, filters, …) is expressed by the methods this class implements, and + * subclasses (`SqliteWasmDriver`, cloud's `TursoDriver`) inherit or spread + * this getter, so keep it truthful per instance. + */ public get supports() { return { - // Basic CRUD Operations - create: true, - read: true, - update: true, - delete: true, - - // Bulk Operations - bulkCreate: true, - bulkUpdate: true, - bulkDelete: true, - - // Transaction & Connection Management - transactions: true, - savepoints: false, - - // Query Operations - queryFilters: true, - queryAggregations: true, /** * Per-granularity native date bucket support. Granularities marked * `false` (or absent) fall back to in-memory `bucketDateValue()` via * `engine.findData` — see `buildDateBucketExpr()` for the SQL emitted. */ queryDateGranularity: this.dateGranularityCapabilities, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - queryCTE: false, - joins: true, - - // Advanced Features - fullTextSearch: false, - jsonQuery: false, - geospatialQuery: false, - streaming: false, - jsonFields: true, - arrayFields: true, - vectorSearch: false, // Persistent, atomic autonumber sequences via `_objectstack_sequences` // (see fillAutoNumberFields / getNextSequenceValue). The engine defers // autonumber generation to this driver — it is the single source of truth. autonumber: true, - - // Schema Management - schemaSync: true, + // No syncSchemasBatch() here: the engine calls syncSchema() per object. + // Subclasses whose transport batches (Turso) implement the method AND + // flip this bit — the engine requires both. batchSchemaSync: false, - migrations: false, - // Object-level declared `indexes` (incl. multi-column UNIQUE) are - // materialized during `initObjects` — see `syncDeclaredIndexes`. - indexes: true, - - // Performance & Optimization - connectionPooling: true, - preparedStatements: true, - queryCache: false, }; } diff --git a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-queryast.test.ts b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-queryast.test.ts index 5711bd0874..c7eac28d80 100644 --- a/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-queryast.test.ts +++ b/packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver-queryast.test.ts @@ -39,8 +39,9 @@ describe('SqliteWasmDriver (QueryAST Format)', () => { expect(driver.name).toBe('com.objectstack.driver.sqlite-wasm'); expect(driver.version).toBeDefined(); expect(driver.supports).toBeDefined(); - expect(driver.supports.transactions).toBe(true); - expect(driver.supports.joins).toBe(true); + // #4634: only the live capability bits remain (inherited from SqlDriver). + expect(driver.supports.autonumber).toBe(true); + expect(driver.supports).not.toHaveProperty('transactions'); }); }); diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 11a3986f87..14a918c29c 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -3228,40 +3228,40 @@ "data/DocumentVersion:isLatest", "data/DocumentVersion:size", "data/DocumentVersion:versionNumber", - "data/DriverCapabilities:arrayFields", + "data/DriverCapabilities:arrayFields [RETIRED]", "data/DriverCapabilities:autonumber", "data/DriverCapabilities:batchSchemaSync", - "data/DriverCapabilities:bulkCreate", - "data/DriverCapabilities:bulkDelete", - "data/DriverCapabilities:bulkUpdate", - "data/DriverCapabilities:connectionPooling", - "data/DriverCapabilities:create", - "data/DriverCapabilities:delete", - "data/DriverCapabilities:fullTextSearch", - "data/DriverCapabilities:geospatialQuery", - "data/DriverCapabilities:indexes", - "data/DriverCapabilities:isolationLevels", - "data/DriverCapabilities:joins", - "data/DriverCapabilities:jsonFields", - "data/DriverCapabilities:jsonQuery", - "data/DriverCapabilities:migrations", - "data/DriverCapabilities:preparedStatements", - "data/DriverCapabilities:queryAggregations", - "data/DriverCapabilities:queryCTE", - "data/DriverCapabilities:queryCache", + "data/DriverCapabilities:bulkCreate [RETIRED]", + "data/DriverCapabilities:bulkDelete [RETIRED]", + "data/DriverCapabilities:bulkUpdate [RETIRED]", + "data/DriverCapabilities:connectionPooling [RETIRED]", + "data/DriverCapabilities:create [RETIRED]", + "data/DriverCapabilities:delete [RETIRED]", + "data/DriverCapabilities:fullTextSearch [RETIRED]", + "data/DriverCapabilities:geospatialQuery [RETIRED]", + "data/DriverCapabilities:indexes [RETIRED]", + "data/DriverCapabilities:isolationLevels [RETIRED]", + "data/DriverCapabilities:joins [RETIRED]", + "data/DriverCapabilities:jsonFields [RETIRED]", + "data/DriverCapabilities:jsonQuery [RETIRED]", + "data/DriverCapabilities:migrations [RETIRED]", + "data/DriverCapabilities:preparedStatements [RETIRED]", + "data/DriverCapabilities:queryAggregations [RETIRED]", + "data/DriverCapabilities:queryCTE [RETIRED]", + "data/DriverCapabilities:queryCache [RETIRED]", "data/DriverCapabilities:queryDateGranularity", - "data/DriverCapabilities:queryFilters", - "data/DriverCapabilities:queryPagination", - "data/DriverCapabilities:querySorting", - "data/DriverCapabilities:querySubqueries", - "data/DriverCapabilities:queryWindowFunctions", - "data/DriverCapabilities:read", - "data/DriverCapabilities:savepoints", - "data/DriverCapabilities:schemaSync", - "data/DriverCapabilities:streaming", - "data/DriverCapabilities:transactions", - "data/DriverCapabilities:update", - "data/DriverCapabilities:vectorSearch", + "data/DriverCapabilities:queryFilters [RETIRED]", + "data/DriverCapabilities:queryPagination [RETIRED]", + "data/DriverCapabilities:querySorting [RETIRED]", + "data/DriverCapabilities:querySubqueries [RETIRED]", + "data/DriverCapabilities:queryWindowFunctions [RETIRED]", + "data/DriverCapabilities:read [RETIRED]", + "data/DriverCapabilities:savepoints [RETIRED]", + "data/DriverCapabilities:schemaSync [RETIRED]", + "data/DriverCapabilities:streaming [RETIRED]", + "data/DriverCapabilities:transactions [RETIRED]", + "data/DriverCapabilities:update [RETIRED]", + "data/DriverCapabilities:vectorSearch [RETIRED]", "data/DriverConfig:capabilities", "data/DriverConfig:connectionString", "data/DriverConfig:name", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index b702f677c3..4bf8ed0153 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -467,6 +467,13 @@ "migrationId": "rest-server-openapi31-block-removed", "toMajor": 17, "rationale": "The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with `OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 document synthesis nothing delivered: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated @objectstack/spec contract enriched with the live server URL and the registered objects — a webhook declared here never appeared in any served document (ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never a `sys_metadata` shape — the stack tree's `api` block declares only its four scoping/auth knobs. The three schemas are removed with the key (zero import-level consumers in objectstack / cloud / objectui); the key itself is tombstoned because the schema is not `.strict()` and a plain delete would strip it silently. #4579." + }, + { + "surface": "data.DriverCapabilities.create / data.DriverCapabilities.read / data.DriverCapabilities.update / data.DriverCapabilities.delete / data.DriverCapabilities.bulkCreate / data.DriverCapabilities.bulkUpdate / data.DriverCapabilities.bulkDelete / data.DriverCapabilities.transactions / data.DriverCapabilities.savepoints / data.DriverCapabilities.isolationLevels / data.DriverCapabilities.queryFilters / data.DriverCapabilities.queryAggregations / data.DriverCapabilities.querySorting / data.DriverCapabilities.queryPagination / data.DriverCapabilities.queryWindowFunctions / data.DriverCapabilities.querySubqueries / data.DriverCapabilities.queryCTE / data.DriverCapabilities.joins / data.DriverCapabilities.fullTextSearch / data.DriverCapabilities.jsonQuery / data.DriverCapabilities.geospatialQuery / data.DriverCapabilities.streaming / data.DriverCapabilities.jsonFields / data.DriverCapabilities.arrayFields / data.DriverCapabilities.vectorSearch / data.DriverCapabilities.schemaSync / data.DriverCapabilities.migrations / data.DriverCapabilities.indexes / data.DriverCapabilities.connectionPooling / data.DriverCapabilities.preparedStatements / data.DriverCapabilities.queryCache", + "replacement": "(removed — delete the keys. A driver advertises a capability by implementing the corresponding IDataDriver method; the three bits that survive because method presence cannot carry the signal are `queryDateGranularity`, `autonumber` and `batchSchemaSync`)", + "migrationId": "driver-capabilities-inert-bits-removed", + "toMajor": 17, + "rationale": "The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a capability the contract no longer declares, and the follow-up audit (#4634) checked every bit in the record the same way, across objectstack and cloud (objectui confirmed clean): of 34 declared bits, THREE have a decision-making reader — `queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), `autonumber` (engine defers generation to the driver), `batchSchemaSync` (engine ANDs it with method presence, because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot) — and THIRTY-ONE were written by every driver and read by nothing. Their `.describe()` strings promised engine adaptation (\"if false, ObjectQL will filter/sort/paginate in memory\") that was never built, and zero readers let the values go WRONG unnoticed: SqlDriver declared `streaming: false` while implementing `findStream`; InMemoryDriver declared `streaming: true` over a full-table read (ADR-0078 false affordance, on the capability record itself). The real mechanism everywhere else is METHOD presence: transactions gate on `driver.beginTransaction`, aggregate pushdown on `typeof driver.aggregate`, schema sync on `typeof driver.syncSchema`, and the REQUIRED CRUD/bulk methods are called unconditionally. A driver is CODE, never stack metadata — `supports` literals live in driver classes and `DriverConfig.capabilities` is plugin TS configuration, neither ever a `sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was retired separately in #4583) — so there is no source for the D2 chain to rewrite and this entry is the D3 record. The keys are tombstoned rather than deleted because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed (DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): a plain delete would silently strip a vendor's authored bit, replacing one silent no-op with another. `batchSchemaSync` also drops its `.default(false)` for `.optional()` — absence already meant false at both readers, and the default forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634." } ], "removed": [] @@ -993,6 +1000,13 @@ "migrationId": "rest-server-openapi31-block-removed", "toMajor": 17, "rationale": "The `openApi31` block (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`, typed by `OpenApi31ExtensionsSchema` with `OpenApiWebhookEventSchema` and `CallbackSchema` under it) promised OpenAPI 3.1 document synthesis nothing delivered: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated @objectstack/spec contract enriched with the live server URL and the registered objects — a webhook declared here never appeared in any served document (ADR-0049; the #3197 connector-webhook shape one layer up). There is no behaviour to preserve and nothing stored to rewrite: `RestServerConfig` is plugin TS configuration (REST plugin constructor / `plugin-hono-server` `restConfig`), never a `sys_metadata` shape — the stack tree's `api` block declares only its four scoping/auth knobs. The three schemas are removed with the key (zero import-level consumers in objectstack / cloud / objectui); the key itself is tombstoned because the schema is not `.strict()` and a plain delete would strip it silently. #4579." + }, + { + "surface": "data.DriverCapabilities.create / data.DriverCapabilities.read / data.DriverCapabilities.update / data.DriverCapabilities.delete / data.DriverCapabilities.bulkCreate / data.DriverCapabilities.bulkUpdate / data.DriverCapabilities.bulkDelete / data.DriverCapabilities.transactions / data.DriverCapabilities.savepoints / data.DriverCapabilities.isolationLevels / data.DriverCapabilities.queryFilters / data.DriverCapabilities.queryAggregations / data.DriverCapabilities.querySorting / data.DriverCapabilities.queryPagination / data.DriverCapabilities.queryWindowFunctions / data.DriverCapabilities.querySubqueries / data.DriverCapabilities.queryCTE / data.DriverCapabilities.joins / data.DriverCapabilities.fullTextSearch / data.DriverCapabilities.jsonQuery / data.DriverCapabilities.geospatialQuery / data.DriverCapabilities.streaming / data.DriverCapabilities.jsonFields / data.DriverCapabilities.arrayFields / data.DriverCapabilities.vectorSearch / data.DriverCapabilities.schemaSync / data.DriverCapabilities.migrations / data.DriverCapabilities.indexes / data.DriverCapabilities.connectionPooling / data.DriverCapabilities.preparedStatements / data.DriverCapabilities.queryCache", + "replacement": "(removed — delete the keys. A driver advertises a capability by implementing the corresponding IDataDriver method; the three bits that survive because method presence cannot carry the signal are `queryDateGranularity`, `autonumber` and `batchSchemaSync`)", + "migrationId": "driver-capabilities-inert-bits-removed", + "toMajor": 17, + "rationale": "The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a capability the contract no longer declares, and the follow-up audit (#4634) checked every bit in the record the same way, across objectstack and cloud (objectui confirmed clean): of 34 declared bits, THREE have a decision-making reader — `queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), `autonumber` (engine defers generation to the driver), `batchSchemaSync` (engine ANDs it with method presence, because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot) — and THIRTY-ONE were written by every driver and read by nothing. Their `.describe()` strings promised engine adaptation (\"if false, ObjectQL will filter/sort/paginate in memory\") that was never built, and zero readers let the values go WRONG unnoticed: SqlDriver declared `streaming: false` while implementing `findStream`; InMemoryDriver declared `streaming: true` over a full-table read (ADR-0078 false affordance, on the capability record itself). The real mechanism everywhere else is METHOD presence: transactions gate on `driver.beginTransaction`, aggregate pushdown on `typeof driver.aggregate`, schema sync on `typeof driver.syncSchema`, and the REQUIRED CRUD/bulk methods are called unconditionally. A driver is CODE, never stack metadata — `supports` literals live in driver classes and `DriverConfig.capabilities` is plugin TS configuration, neither ever a `sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was retired separately in #4583) — so there is no source for the D2 chain to rewrite and this entry is the D3 record. The keys are tombstoned rather than deleted because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed (DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): a plain delete would silently strip a vendor's authored bit, replacing one silent no-op with another. `batchSchemaSync` also drops its `.default(false)` for `.optional()` — absence already meant false at both readers, and the default forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634." } ], "removed": [] diff --git a/packages/spec/src/contracts/data-driver.test.ts b/packages/spec/src/contracts/data-driver.test.ts index 7ca6f7a0c2..3514aca37e 100644 --- a/packages/spec/src/contracts/data-driver.test.ts +++ b/packages/spec/src/contracts/data-driver.test.ts @@ -6,38 +6,12 @@ describe('IDataDriver', () => { const mockDriver: IDataDriver = { name: 'mock_driver', version: '1.0.0', + // #4634: capability bits are opt-in advertisements read by the engine; + // the 31 inert bits the old mock spelled out are tombstoned (`never`), + // so a conforming driver declares only what has a reader. supports: { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: false, - bulkUpdate: false, - bulkDelete: false, - transactions: false, - savepoints: false, - queryFilters: true, - queryAggregations: false, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: false, - queryCTE: false, - joins: false, - fullTextSearch: false, - jsonQuery: false, - geospatialQuery: false, - streaming: false, - jsonFields: false, - arrayFields: false, - vectorSearch: false, - schemaSync: false, + autonumber: true, batchSchemaSync: false, - migrations: false, - indexes: false, - connectionPooling: false, - preparedStatements: false, - queryCache: false, }, connect: async () => {}, disconnect: async () => {}, @@ -62,47 +36,16 @@ describe('IDataDriver', () => { expect(mockDriver.name).toBe('mock_driver'); expect(mockDriver.version).toBe('1.0.0'); - expect(mockDriver.supports.create).toBe(true); - expect(mockDriver.supports.transactions).toBe(false); + expect(mockDriver.supports.autonumber).toBe(true); + expect(mockDriver.supports.batchSchemaSync).toBe(false); }); it('should allow optional methods', () => { const minimalDriver: IDataDriver = { name: 'minimal', version: '0.1.0', - supports: { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: false, - bulkUpdate: false, - bulkDelete: false, - transactions: false, - savepoints: false, - queryFilters: false, - queryAggregations: false, - querySorting: false, - queryPagination: false, - queryWindowFunctions: false, - querySubqueries: false, - queryCTE: false, - joins: false, - fullTextSearch: false, - jsonQuery: false, - geospatialQuery: false, - streaming: false, - jsonFields: false, - arrayFields: false, - vectorSearch: false, - schemaSync: false, - batchSchemaSync: false, - migrations: false, - indexes: false, - connectionPooling: false, - preparedStatements: false, - queryCache: false, - }, + // #4634: an empty advertisement is a valid one — every live bit is opt-in. + supports: {}, connect: async () => {}, disconnect: async () => {}, checkHealth: async () => true, @@ -135,38 +78,12 @@ describe('IDataDriver', () => { const extendedDriver: IDataDriver = { name: 'extended', version: '2.0.0', + // #4634: the full live surface — native date buckets, driver-owned + // autonumber, batched DDL. Everything else is expressed by the methods. supports: { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: true, - bulkUpdate: true, - bulkDelete: true, - transactions: true, - savepoints: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - queryCTE: true, - joins: true, - fullTextSearch: true, - jsonQuery: true, - geospatialQuery: false, - streaming: true, - jsonFields: true, - arrayFields: true, - vectorSearch: false, - schemaSync: true, - batchSchemaSync: false, - migrations: true, - indexes: true, - connectionPooling: true, - preparedStatements: true, - queryCache: true, + queryDateGranularity: { day: true, week: true, month: true, quarter: true, year: true }, + autonumber: true, + batchSchemaSync: true, }, connect: async () => {}, disconnect: async () => {}, diff --git a/packages/spec/src/contracts/data-engine.test.ts b/packages/spec/src/contracts/data-engine.test.ts index 58f265db4f..c5beabefd1 100644 --- a/packages/spec/src/contracts/data-engine.test.ts +++ b/packages/spec/src/contracts/data-engine.test.ts @@ -4,39 +4,13 @@ import type { IDataDriver } from './data-driver'; /** * Minimal DriverCapabilities object for tests. + * + * #4634: capability bits are opt-in advertisements — the 31 inert bits the old + * literal spelled out are tombstoned (`never`), and `batchSchemaSync` is the + * one live bit a batching driver would flip. Minimal = advertise nothing. */ const minimalCapabilities = { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: false, - bulkUpdate: false, - bulkDelete: false, - transactions: false, - savepoints: false, - queryFilters: true, - queryAggregations: false, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: false, - queryCTE: false, - joins: false, - fullTextSearch: false, - jsonQuery: false, - geospatialQuery: false, - streaming: false, - jsonFields: false, - arrayFields: false, - vectorSearch: false, - schemaSync: false, batchSchemaSync: false, - migrations: false, - indexes: false, - connectionPooling: false, - preparedStatements: false, - queryCache: false, }; describe('Data Engine Contract', () => { @@ -223,7 +197,7 @@ describe('Data Engine Contract', () => { expect(typeof driverAsInterface.connect).toBe('function'); expect(typeof driverAsInterface.disconnect).toBe('function'); expect(typeof driverAsInterface.checkHealth).toBe('function'); - expect(driverAsInterface.supports.queryFilters).toBe(true); + expect(driverAsInterface.supports.batchSchemaSync).toBe(false); }); it('should support full IDataDriver lifecycle and CRUD', async () => { @@ -265,7 +239,9 @@ describe('Data Engine Contract', () => { const driver: IDataDriver = { name: 'postgres', version: '1.0.0', - supports: { ...minimalCapabilities, transactions: true, bulkCreate: true }, + // #4634: transactions/bulk are expressed by the methods below, not by + // capability bits — those two bits are tombstoned. + supports: { ...minimalCapabilities, autonumber: true }, connect: async () => {}, disconnect: async () => {}, checkHealth: async () => true, diff --git a/packages/spec/src/data/driver-nosql.test.ts b/packages/spec/src/data/driver-nosql.test.ts index 5554971979..0113ced4ae 100644 --- a/packages/spec/src/data/driver-nosql.test.ts +++ b/packages/spec/src/data/driver-nosql.test.ts @@ -145,17 +145,9 @@ describe('NoSQL Driver Protocol', () => { date: 'date', datetime: 'date', }, + // #4634: only the live bits are authorable; MongoDB batches its DDL. capabilities: { - create: true, - read: true, - update: true, - delete: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: false, + batchSchemaSync: true, }, consistency: 'quorum' as const, replication: { @@ -196,18 +188,8 @@ describe('NoSQL Driver Protocol', () => { date: 'S', datetime: 'S', }, - capabilities: { - create: true, - read: true, - update: true, - delete: true, - queryFilters: true, - queryAggregations: false, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: false, - }, + // #4634: an empty advertisement is a valid one — every live bit is opt-in. + capabilities: {}, consistency: 'eventual' as const, }; diff --git a/packages/spec/src/data/driver-nosql.zod.ts b/packages/spec/src/data/driver-nosql.zod.ts index 91f67d8943..06d0a8679c 100644 --- a/packages/spec/src/data/driver-nosql.zod.ts +++ b/packages/spec/src/data/driver-nosql.zod.ts @@ -196,39 +196,14 @@ export type NoSQLDataTypeMapping = z.infer; * numShards: 4 * }, * capabilities: { - * create: true, - * read: true, - * update: true, - * delete: true, - * bulkCreate: true, - * bulkUpdate: true, - * bulkDelete: true, - * transactions: true, - * savepoints: false, - * queryFilters: true, - * queryAggregations: true, - * querySorting: true, - * queryPagination: true, - * queryWindowFunctions: false, - * querySubqueries: false, - * queryCTE: false, - * joins: false, - * fullTextSearch: true, - * jsonQuery: true, - * geospatialQuery: true, - * streaming: true, - * jsonFields: true, - * arrayFields: true, - * vectorSearch: false, - * schemaSync: true, - * migrations: false, - * indexes: true, - * connectionPooling: true, - * preparedStatements: false, - * queryCache: false + * // Only the live bits remain authorable (#4634, ADR-0049): MongoDB batches + * // its DDL round-trips, owns neither autonumber sequences nor native date + * // bucketing here. Everything else a driver "supports" is expressed by + * // implementing the corresponding IDataDriver method. + * batchSchemaSync: true * } * } - * + * * @example DynamoDB driver configuration * { * name: 'dynamodb-main', @@ -238,23 +213,7 @@ export type NoSQLDataTypeMapping = z.infer; * accessKeyId: 'AKIAIOSFODNN7EXAMPLE', * secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY', * consistency: 'eventual', - * capabilities: { - * create: true, - * read: true, - * update: true, - * delete: true, - * bulkCreate: true, - * bulkUpdate: false, - * bulkDelete: false, - * transactions: true, - * queryFilters: true, - * queryAggregations: false, - * querySorting: true, - * queryPagination: true, - * fullTextSearch: false, - * jsonQuery: true, - * indexes: true - * } + * capabilities: {} * } */ export const NoSQLDriverConfigSchema = lazySchema(() => DriverConfigSchema.extend({ diff --git a/packages/spec/src/data/driver-sql.test.ts b/packages/spec/src/data/driver-sql.test.ts index 88dd795017..31c0c4933c 100644 --- a/packages/spec/src/data/driver-sql.test.ts +++ b/packages/spec/src/data/driver-sql.test.ts @@ -160,39 +160,12 @@ describe('SQLDriverConfigSchema', () => { idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }, + // #4634: only the live bits are authorable — the 31 inert bits the old + // block spelled out are tombstoned and now fail the parse. capabilities: { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: true, - bulkUpdate: true, - bulkDelete: true, - transactions: true, - savepoints: true, - isolationLevels: ['read_committed', 'repeatable_read', 'serializable'], - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - queryCTE: true, - joins: true, - fullTextSearch: true, - jsonQuery: true, - geospatialQuery: false, - streaming: true, - jsonFields: true, - arrayFields: true, - vectorSearch: true, - schemaSync: true, + queryDateGranularity: { day: true, week: true, month: true, quarter: true, year: true }, + autonumber: true, batchSchemaSync: false, - migrations: true, - indexes: true, - connectionPooling: true, - preparedStatements: true, - queryCache: false, }, }; @@ -216,37 +189,7 @@ describe('SQLDriverConfigSchema', () => { }, ssl: false, capabilities: { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: true, - bulkUpdate: true, - bulkDelete: true, - transactions: true, - savepoints: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - queryCTE: true, - joins: true, - fullTextSearch: true, - jsonQuery: true, - geospatialQuery: false, - streaming: false, - jsonFields: true, - arrayFields: false, - vectorSearch: false, - schemaSync: true, - batchSchemaSync: false, - migrations: true, - indexes: true, - connectionPooling: true, - preparedStatements: true, - queryCache: false, + autonumber: true, }, }; @@ -305,38 +248,11 @@ describe('SQLDriverConfigSchema', () => { datetime: 'TEXT', }, ssl: false, + // #4634: SQLite's strftime lacks ISO week — a truthful per-granularity + // advertisement is exactly what the live bit exists for. capabilities: { - create: true, - read: true, - update: true, - delete: true, - bulkCreate: false, - bulkUpdate: false, - bulkDelete: false, - transactions: true, - savepoints: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - queryCTE: true, - joins: true, - fullTextSearch: true, - jsonQuery: true, - geospatialQuery: false, - streaming: false, - jsonFields: true, - arrayFields: false, - vectorSearch: false, - schemaSync: true, - batchSchemaSync: false, - migrations: false, - indexes: true, - connectionPooling: false, - preparedStatements: true, - queryCache: false, + queryDateGranularity: { day: true, week: false, month: true, quarter: true, year: true }, + autonumber: true, }, }; diff --git a/packages/spec/src/data/driver-sql.zod.ts b/packages/spec/src/data/driver-sql.zod.ts index 22382fe1d4..c637ed49a2 100644 --- a/packages/spec/src/data/driver-sql.zod.ts +++ b/packages/spec/src/data/driver-sql.zod.ts @@ -108,37 +108,12 @@ export type SSLConfig = z.infer; * connectionTimeoutMillis: 5000 * }, * capabilities: { - * create: true, - * read: true, - * update: true, - * delete: true, - * bulkCreate: true, - * bulkUpdate: true, - * bulkDelete: true, - * transactions: true, - * savepoints: true, - * isolationLevels: ['read-committed', 'repeatable-read', 'serializable'], - * queryFilters: true, - * queryAggregations: true, - * querySorting: true, - * queryPagination: true, - * queryWindowFunctions: true, - * querySubqueries: true, - * queryCTE: true, - * joins: true, - * fullTextSearch: true, - * jsonQuery: true, - * geospatialQuery: false, - * streaming: true, - * jsonFields: true, - * arrayFields: true, - * vectorSearch: true, - * schemaSync: true, - * migrations: true, - * indexes: true, - * connectionPooling: true, - * preparedStatements: true, - * queryCache: false + * // Only the live bits remain authorable (#4634, ADR-0049) — everything a + * // driver "supports" beyond these is expressed by implementing the + * // corresponding IDataDriver method, not by declaring a boolean. + * queryDateGranularity: { day: true, week: true, month: true, quarter: true, year: true }, + * autonumber: true, + * batchSchemaSync: false * } * } */ diff --git a/packages/spec/src/data/driver.test.ts b/packages/spec/src/data/driver.test.ts index 9f9ea8cc5b..76f22134f7 100644 --- a/packages/spec/src/data/driver.test.ts +++ b/packages/spec/src/data/driver.test.ts @@ -6,62 +6,204 @@ import { type DriverInterface, } from './driver.zod'; +/** + * The 31 capability bits retired in 17.0.0 (#4634, ADR-0049 enforce-or-remove). + * Kept as a literal list so the pins below cannot drift from the PR's audit + * table silently — adding a 32nd tombstone (or resurrecting one of these) + * must touch this file. + */ +const RETIRED_BITS = [ + 'create', + 'read', + 'update', + 'delete', + 'bulkCreate', + 'bulkUpdate', + 'bulkDelete', + 'transactions', + 'savepoints', + 'isolationLevels', + 'queryFilters', + 'queryAggregations', + 'querySorting', + 'queryPagination', + 'queryWindowFunctions', + 'querySubqueries', + 'queryCTE', + 'joins', + 'fullTextSearch', + 'jsonQuery', + 'geospatialQuery', + 'streaming', + 'jsonFields', + 'arrayFields', + 'vectorSearch', + 'schemaSync', + 'migrations', + 'indexes', + 'connectionPooling', + 'preparedStatements', + 'queryCache', +] as const; + +/** The bits that survive — each with a named engine reader. */ +const LIVE_BITS = ['queryDateGranularity', 'autonumber', 'batchSchemaSync'] as const; + describe('DriverCapabilitiesSchema', () => { - it('should accept valid capabilities', () => { + it('accepts the live capability bits', () => { const capabilities: DriverCapabilities = { - transactions: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - joins: true, - fullTextSearch: true, - jsonFields: true, - arrayFields: true, + queryDateGranularity: { day: true, week: false, month: true, quarter: true, year: true }, + autonumber: true, + batchSchemaSync: true, }; expect(() => DriverCapabilitiesSchema.parse(capabilities)).not.toThrow(); }); - it('should accept minimal capabilities', () => { - const capabilities: DriverCapabilities = { - transactions: false, - queryFilters: false, - queryAggregations: false, - querySorting: false, - queryPagination: false, - queryWindowFunctions: false, - querySubqueries: false, - joins: false, - fullTextSearch: false, - jsonFields: false, - arrayFields: false, - }; + it('accepts an empty capabilities object — every live bit is opt-in (absence = false)', () => { + const parsed = DriverCapabilitiesSchema.parse({}); + // `batchSchemaSync` dropped its `.default(false)` in #4634: absence already + // meant false at both readers (`supports?.batchSchemaSync` truthiness), and + // the default forced every capability object to spell out dead weight. + expect(parsed).not.toHaveProperty('batchSchemaSync'); + expect(parsed).not.toHaveProperty('autonumber'); + }); - expect(() => DriverCapabilitiesSchema.parse(capabilities)).not.toThrow(); + it('declares exactly the audited shape: 3 live bits + 31 tombstones', () => { + const shape = (DriverCapabilitiesSchema as unknown as { shape: Record }).shape; + const keys = Object.keys(shape).sort(); + expect(keys).toEqual([...RETIRED_BITS, ...LIVE_BITS].slice().sort()); }); +}); - it('should accept capabilities with defaults', () => { - const incomplete = { - transactions: true, - joins: true, - queryFilters: true, - // missing other fields - they should use defaults - }; +// =========================================================================== +// Retired capability bits (#4634, ADR-0049 enforce-or-remove) +// =========================================================================== + +describe('[#4634] the 31 inert capability bits are tombstoned, not stripped', () => { + it.each(RETIRED_BITS)('REJECTS an authored `%s`, with the prescription in the message', (bit) => { + const value = bit === 'isolationLevels' ? ['read-committed'] : true; + expect(() => DriverCapabilitiesSchema.parse({ [bit]: value })).toThrow( + new RegExp(`DriverCapabilities\\.${bit}.*removed.*Delete the key`, 's'), + ); + }); + + it('the streaming prescription carries the #4484 findStream story and the paged-find fix', () => { + expect(() => DriverCapabilitiesSchema.parse({ streaming: true })).toThrow( + /DriverCapabilities\.streaming.*removed.*findStream.*#4484.*`find\(\)` with `limit`\/`offset`.*Delete the key/s, + ); + }); + + it('the queryFilters prescription names the real mechanism — find() executes the full QueryAST', () => { + expect(() => DriverCapabilitiesSchema.parse({ queryFilters: false })).toThrow( + /DriverCapabilities\.queryFilters.*removed.*full QueryAST.*never built.*Delete the key/s, + ); + }); + + it('the transactions prescription points at method presence, not a replacement bit', () => { + expect(() => DriverCapabilitiesSchema.parse({ transactions: true })).toThrow( + /DriverCapabilities\.transactions.*removed.*METHOD PRESENCE.*beginTransaction.*Delete the key/s, + ); + }); + + it('still parses the live bits cleanly — the tombstones reject a key, not the record', () => { + const parsed = DriverCapabilitiesSchema.parse({ + // z.record over the DateGranularity enum parses exhaustively — a partial + // advertisement is expressed with explicit `false`, as SqlDriver does. + queryDateGranularity: { day: true, week: false, month: true, quarter: true, year: true }, + autonumber: true, + batchSchemaSync: false, + }); + expect(parsed.autonumber).toBe(true); + expect(parsed.batchSchemaSync).toBe(false); + for (const bit of RETIRED_BITS) { + expect(parsed).not.toHaveProperty(bit); + } + }); +}); - const result = DriverCapabilitiesSchema.safeParse(incomplete); - expect(result.success).toBe(true); - if (result.success) { - // Check that defaults are applied - expect(result.data.create).toBe(true); // default - expect(result.data.bulkCreate).toBe(false); // default - expect(result.data.queryAggregations).toBe(false); // default +// #4642 established that a compile-time conditional-type pin in this package is +// a no-op (tsconfig excludes `**/*.test.ts`; vitest never enables `typecheck`), +// so the load-bearing tsc-channel proof is the compiler-API test below, with +// anti-vacuity guards; sabotage-verified in the PR (S1: re-adding a live +// `streaming: z.boolean()` turns it red). +describe('[#4634] tsc channel: the retired bits are unwritable in DriverCapabilities', () => { + it('types every retired bit as authored-unwritable and every live bit as writable', async () => { + const ts = (await import('typescript')).default; + const { resolve, dirname } = await import('node:path'); + const { fileURLToPath } = await import('node:url'); + + const dataEntry = resolve(dirname(fileURLToPath(import.meta.url)), './index.ts'); + const program = ts.createProgram([dataEntry], { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + noEmit: true, + strict: true, + }); + const checker = program.getTypeChecker(); + const sf = program.getSourceFile(dataEntry); + const moduleSym = sf && checker.getSymbolAtLocation(sf); + // Anti-vacuity: a resolution failure must fail loudly, not pass everything. + expect(moduleSym, './data module symbol must resolve').toBeTruthy(); + + const exports = checker.getExportsOfModule(moduleSym!); + const capsAlias = exports.find((e) => e.getName() === 'DriverCapabilities'); + expect(capsAlias, 'DriverCapabilities must stay exported from ./data').toBeTruthy(); + + const capsType = checker.getDeclaredTypeOfSymbol(capsAlias!); + const props = new Map(capsType.getProperties().map((p) => [p.getName(), p])); + + // Anti-vacuity: the walked shape is the audited 34-key shape. + expect([...props.keys()].sort()).toEqual([...RETIRED_BITS, ...LIVE_BITS].slice().sort()); + + const decl = capsAlias!.declarations?.[0]; + expect(decl, 'DriverCapabilities alias declaration must exist').toBeTruthy(); + const typeAt = (name: string) => + checker.typeToString(checker.getTypeOfSymbolAtLocation(props.get(name)!, decl!)); + + for (const bit of RETIRED_BITS) { + // `retiredKey()` is `z.never().optional()`: the property type collapses + // to `undefined` — there is no value an author can write. A resurrected + // live bit reads `boolean` (or wider) here and turns this red. + expect(typeAt(bit), `retired bit ${bit} must be unwritable`).toBe('undefined'); + } + for (const bit of LIVE_BITS) { + expect(typeAt(bit), `live bit ${bit} must stay writable`).not.toBe('undefined'); } }); }); +// =========================================================================== +// DriverInterfaceSchema — the structural contract +// =========================================================================== + +/** A minimal, contract-complete driver object for the structural tests. */ +const baseDriver = { + name: 'test-driver', + version: '1.0.0', + connect: async () => {}, + disconnect: async () => {}, + checkHealth: async () => true, + execute: async () => ({}), + find: async () => [], + findOne: async () => null, + create: async () => ({}), + update: async () => ({}), + upsert: async () => ({}), + delete: async () => true, + count: async () => 0, + bulkCreate: async () => [], + bulkUpdate: async () => [], + bulkDelete: async () => {}, + beginTransaction: async () => ({}), + commit: async () => {}, + rollback: async () => {}, + syncSchema: async () => {}, + dropTable: async () => {}, + supports: {}, +}; + describe('DriverInterfaceSchema', () => { describe('Basic Properties', () => { it('should require name and version', () => { @@ -74,90 +216,12 @@ describe('DriverInterfaceSchema', () => { expect(result.success).toBe(false); }); - it('should accept name and version', () => { - const driver = { - name: 'postgresql', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async () => [], - findOne: async () => null, - create: async () => ({}), - update: async () => ({}), - upsert: async () => ({}), - delete: async () => true, - count: async () => 0, - bulkCreate: async () => [], - bulkUpdate: async () => [], - bulkDelete: async () => {}, - beginTransaction: async () => ({}), - commit: async () => {}, - rollback: async () => {}, - syncSchema: async () => {}, - dropTable: async () => {}, - supports: { - transactions: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - joins: true, - fullTextSearch: true, - jsonFields: true, - arrayFields: true, - }, - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); + it('should accept a contract-complete driver', () => { + expect(() => DriverInterfaceSchema.parse(baseDriver)).not.toThrow(); }); }); describe('CRUD Operations', () => { - const baseDriver = { - name: 'test-driver', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async (object: string, query: any) => [], - findOne: async (object: string, query: any) => null, - create: async (object: string, data: any) => data, - update: async (object: string, id: any, data: any) => data, - upsert: async (object: string, data: any) => data, - delete: async (object: string, id: any) => true, - count: async () => 0, - bulkCreate: async (object: string, data: any[]) => data, - bulkUpdate: async (object: string, updates: any[]) => updates, - bulkDelete: async (object: string, ids: any[]) => {}, - beginTransaction: async () => ({}), - commit: async () => {}, - rollback: async () => {}, - syncSchema: async (object: string, schema: any) => {}, - dropTable: async (object: string) => {}, - supports: { - transactions: false, - queryFilters: false, - queryAggregations: false, - querySorting: false, - queryPagination: false, - queryWindowFunctions: false, - querySubqueries: false, - joins: false, - fullTextSearch: false, - jsonFields: false, - arrayFields: false, - }, - }; - - it('should accept driver with CRUD operations', () => { - expect(() => DriverInterfaceSchema.parse(baseDriver)).not.toThrow(); - }); - it('should validate find method signature', () => { const driver = { ...baseDriver, @@ -170,18 +234,6 @@ describe('DriverInterfaceSchema', () => { expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); }); - it('should validate findOne method signature', () => { - const driver = { - ...baseDriver, - findOne: async (object: string, id: any) => ({ - id: '1', - name: 'Record 1', - }), - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); - it('should validate create method signature', () => { const driver = { ...baseDriver, @@ -207,94 +259,17 @@ describe('DriverInterfaceSchema', () => { expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); }); - - it('should validate delete method signature', () => { - const driver = { - ...baseDriver, - delete: async (object: string, id: any) => ({ - id, - deleted: true, - }), - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); }); describe('Bulk Operations', () => { - const baseDriver = { - name: 'test-driver', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async () => [], - findOne: async () => null, - create: async () => ({}), - update: async () => ({}), - upsert: async () => ({}), - delete: async () => true, - count: async () => 0, - bulkCreate: async () => [], - bulkUpdate: async () => [], - bulkDelete: async () => {}, - beginTransaction: async () => ({}), - commit: async () => {}, - rollback: async () => {}, - syncSchema: async () => {}, - dropTable: async () => {}, - supports: { - transactions: false, - queryFilters: false, - queryAggregations: false, - querySorting: false, - queryPagination: false, - queryWindowFunctions: false, - querySubqueries: false, - joins: false, - fullTextSearch: false, - jsonFields: false, - arrayFields: false, - }, - }; - - it('should validate bulkCreate method', () => { - const driver = { - ...baseDriver, - bulkCreate: async (object: string, data: any[]) => { - return data.map((item, i) => ({ - ...item, - id: `generated-${i}`, - })); - }, - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); - - it('should validate bulkUpdate method', () => { + it('should validate bulk method signatures', () => { const driver = { ...baseDriver, - bulkUpdate: async (object: string, updates: any[]) => { - return updates.map(u => ({ - ...u.data, - id: u.id, - updated_at: new Date(), - })); - }, - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); - - it('should validate bulkDelete method', () => { - const driver = { - ...baseDriver, - bulkDelete: async (object: string, ids: any[]) => ({ - deleted: ids.length, - ids, - }), + bulkCreate: async (object: string, data: any[]) => + data.map((item, i) => ({ ...item, id: `generated-${i}` })), + bulkUpdate: async (object: string, updates: any[]) => + updates.map((u) => ({ ...u.data, id: u.id, updated_at: new Date() })), + bulkDelete: async (object: string, ids: any[]) => {}, }; expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); @@ -302,355 +277,46 @@ describe('DriverInterfaceSchema', () => { }); describe('DDL Operations', () => { - const baseDriver = { - name: 'test-driver', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async () => [], - findOne: async () => null, - create: async () => ({}), - update: async () => ({}), - upsert: async () => ({}), - delete: async () => true, - count: async () => 0, - bulkCreate: async () => [], - bulkUpdate: async () => [], - bulkDelete: async () => {}, - beginTransaction: async () => ({}), - commit: async () => {}, - rollback: async () => {}, - syncSchema: async () => {}, - dropTable: async () => {}, - supports: { - transactions: false, - queryFilters: false, - queryAggregations: false, - querySorting: false, - queryPagination: false, - queryWindowFunctions: false, - querySubqueries: false, - joins: false, - fullTextSearch: false, - jsonFields: false, - arrayFields: false, - }, - }; - - it('should validate syncSchema method', () => { - const driver = { - ...baseDriver, - syncSchema: async (object: string, schema: any) => { - // Create table if not exists - // Alter table to match schema - }, - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); - - it('should validate dropTable method', () => { + it('should validate syncSchema and dropTable methods', () => { const driver = { ...baseDriver, - dropTable: async (object: string) => { - // DROP TABLE IF EXISTS - }, + syncSchema: async (object: string, schema: any) => {}, + dropTable: async (object: string) => {}, }; expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); }); }); - describe('Transaction Support', () => { - it('should accept driver without transaction support', () => { - const driver = { - name: 'simple-driver', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async () => [], - findOne: async () => null, - create: async () => ({}), - update: async () => ({}), - upsert: async () => ({}), - delete: async () => true, - count: async () => 0, - bulkCreate: async () => [], - bulkUpdate: async () => [], - bulkDelete: async () => {}, - beginTransaction: async () => ({}), - commit: async () => {}, - rollback: async () => {}, - syncSchema: async () => {}, - dropTable: async () => {}, - supports: { - transactions: false, - queryFilters: false, - queryAggregations: false, - querySorting: false, - queryPagination: false, - queryWindowFunctions: false, - querySubqueries: false, - joins: false, - fullTextSearch: false, - jsonFields: false, - arrayFields: false, - }, - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); - - it('should accept driver with transaction support', () => { - const driver = { - name: 'transactional-driver', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async () => [], - findOne: async () => null, - create: async () => ({}), - update: async () => ({}), - upsert: async () => ({}), - delete: async () => true, - count: async () => 0, - bulkCreate: async () => [], - bulkUpdate: async () => [], - bulkDelete: async () => {}, - beginTransaction: async () => ({ id: 'tx-123' }), - commit: async (tx: any) => {}, - rollback: async (tx: any) => {}, - syncSchema: async () => {}, - dropTable: async () => {}, - supports: { - transactions: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: true, - joins: true, - fullTextSearch: false, - jsonFields: true, - arrayFields: false, - }, - }; - - expect(() => DriverInterfaceSchema.parse(driver)).not.toThrow(); - }); - }); - - describe('Real-World Driver Examples', () => { - it('should accept PostgreSQL-like driver', () => { - const postgresDriver: DriverInterface = { - name: 'postgresql', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async (object, query) => [], - findOne: async (object, query) => null, - create: async (object, data) => data, - update: async (object, id, data) => data, - upsert: async (object, data) => data, - delete: async (object, id) => true, - count: async () => 0, - bulkCreate: async (object, data) => data, - bulkUpdate: async (object, updates) => updates, - bulkDelete: async (object, ids) => {}, - beginTransaction: async () => ({}), - commit: async (tx) => {}, - rollback: async (tx) => {}, - syncSchema: async (object, schema) => {}, - dropTable: async (object) => {}, - supports: { - transactions: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: true, - querySubqueries: true, - joins: true, - fullTextSearch: true, - jsonFields: true, - arrayFields: true, - }, - }; - - expect(() => DriverInterfaceSchema.parse(postgresDriver)).not.toThrow(); - }); - - it('should accept MongoDB-like driver', () => { - const mongoDriver: DriverInterface = { - name: 'mongodb', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async (object, query) => [], - findOne: async (object, query) => null, - create: async (object, data) => data, - update: async (object, id, data) => data, - upsert: async (object, data) => data, - delete: async (object, id) => true, - count: async () => 0, - bulkCreate: async (object, data) => data, - bulkUpdate: async (object, updates) => updates, - bulkDelete: async (object, ids) => {}, - beginTransaction: async () => ({}), - commit: async (tx) => {}, - rollback: async (tx) => {}, - syncSchema: async (object, schema) => {}, - dropTable: async (object) => {}, - supports: { - transactions: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, // MongoDB has limited window function support - querySubqueries: true, - joins: false, // MongoDB has limited join support - fullTextSearch: true, - jsonFields: true, // Native JSON support - arrayFields: true, // Native array support - }, - }; - - expect(() => DriverInterfaceSchema.parse(mongoDriver)).not.toThrow(); - }); - - it('should accept Salesforce-like driver', () => { - const salesforceDriver: DriverInterface = { - name: 'salesforce', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async (object, query) => [], - findOne: async (object, query) => null, - create: async (object, data) => data, - update: async (object, id, data) => data, - upsert: async (object, data) => data, - delete: async (object, id) => true, - count: async () => 0, - bulkCreate: async (object, data) => data, - bulkUpdate: async (object, updates) => updates, - bulkDelete: async (object, ids) => {}, - beginTransaction: async () => ({}), - commit: async (tx) => {}, - rollback: async (tx) => {}, - syncSchema: async (object, schema) => {}, - dropTable: async (object) => {}, + describe('Capability advertisement', () => { + it('accepts a driver that advertises the live bits', () => { + // The shape SqlDriver actually returns after #4634: native date buckets, + // driver-owned autonumber, no batched DDL. + const sqlLike: DriverInterface = { + ...baseDriver, + name: 'sql-like', supports: { - transactions: false, // Salesforce doesn't support transactions - queryFilters: true, // SOQL WHERE clause - queryAggregations: true, // SOQL GROUP BY - querySorting: true, // SOQL ORDER BY - queryPagination: true, // SOQL LIMIT/OFFSET - queryWindowFunctions: false, // No window functions - querySubqueries: true, // SOQL supports subqueries - joins: true, // SOQL supports relationships - fullTextSearch: true, // SOSL - jsonFields: false, // No native JSON type - arrayFields: false, // No native array type + queryDateGranularity: { day: true, week: true, month: true, quarter: true, year: true }, + autonumber: true, + batchSchemaSync: false, }, }; - - expect(() => DriverInterfaceSchema.parse(salesforceDriver)).not.toThrow(); + expect(() => DriverInterfaceSchema.parse(sqlLike)).not.toThrow(); }); - it('should accept Redis-like driver', () => { - const redisDriver: DriverInterface = { - name: 'redis', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async (object, query) => [], - findOne: async (object, query) => null, - create: async (object, data) => data, - update: async (object, id, data) => data, - upsert: async (object, data) => data, - delete: async (object, id) => true, - count: async () => 0, - bulkCreate: async (object, data) => data, - bulkUpdate: async (object, updates) => updates, - bulkDelete: async (object, ids) => {}, - beginTransaction: async () => ({}), - commit: async (tx) => {}, - rollback: async (tx) => {}, - syncSchema: async (object, schema) => {}, - dropTable: async (object) => {}, - supports: { - transactions: true, // Redis supports transactions - queryFilters: false, // Limited query support - key-based lookup - queryAggregations: false, // No aggregation support - querySorting: false, // No native sorting - queryPagination: false, // No pagination support - queryWindowFunctions: false, // No window functions - querySubqueries: false, // No subqueries - joins: false, // No join support - fullTextSearch: false, // No native full-text search - jsonFields: true, // RedisJSON module - arrayFields: true, // Redis lists - }, - }; - - expect(() => DriverInterfaceSchema.parse(redisDriver)).not.toThrow(); + it('accepts a driver that advertises nothing — capability bits are opt-in', () => { + // The shape InMemoryDriver returns after #4634. "Supports transactions" + // etc. is expressed by the methods it implements, not by booleans. + const memoryLike: DriverInterface = { ...baseDriver, name: 'memory-like', supports: {} }; + expect(() => DriverInterfaceSchema.parse(memoryLike)).not.toThrow(); }); - it('should accept memory-like driver with limited query support', () => { - const memoryDriver: DriverInterface = { - name: 'memory', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async (object, query) => [], - findOne: async (object, query) => null, - create: async (object, data) => data, - update: async (object, id, data) => data, - upsert: async (object, data) => data, - delete: async (object, id) => true, - count: async () => 0, - bulkCreate: async (object, data) => data, - bulkUpdate: async (object, updates) => updates, - bulkDelete: async (object, ids) => {}, - beginTransaction: async () => ({}), - commit: async (tx) => {}, - rollback: async (tx) => {}, - syncSchema: async (object, schema) => {}, - dropTable: async (object) => {}, - supports: { - transactions: false, // No transactions in memory - queryFilters: false, // Memory driver doesn't support query conditions - all filtering done in memory - queryAggregations: false, // No aggregation support - querySorting: false, // No native sorting - queryPagination: false, // No pagination support - queryWindowFunctions: false, // No window functions - querySubqueries: false, // No subqueries - joins: false, // No join support - joins done in memory - fullTextSearch: false, // No full-text search - jsonFields: true, // Memory can store any type - arrayFields: true, // Memory can store any type - }, + it('REJECTS a driver whose supports still authors a retired bit', () => { + const legacy = { + ...baseDriver, + supports: { transactions: true, streaming: true }, }; - - expect(() => DriverInterfaceSchema.parse(memoryDriver)).not.toThrow(); + expect(() => DriverInterfaceSchema.parse(legacy)).toThrow(/removed.*Delete the key/s); }); }); @@ -672,42 +338,12 @@ describe('DriverInterfaceSchema', () => { // (see contracts/data-driver.test.ts), which breaks callers, and there were // none. A stray `findStream` on a driver object therefore just parses and // is dropped, exactly as any other non-contract method on it always has. + // (Contrast `DriverCapabilities`, whose keys ARE tombstoned: capability + // records DO get parsed, via DriverConfigSchema.capabilities.) const withLegacyMethod = { + ...baseDriver, name: 'legacy', - version: '1.0.0', - connect: async () => {}, - disconnect: async () => {}, - checkHealth: async () => true, - execute: async () => ({}), - find: async () => [], findStream: async function* () {}, - findOne: async () => null, - create: async () => ({}), - update: async () => ({}), - upsert: async () => ({}), - delete: async () => true, - count: async () => 0, - bulkCreate: async () => [], - bulkUpdate: async () => [], - bulkDelete: async () => {}, - beginTransaction: async () => ({}), - commit: async () => {}, - rollback: async () => {}, - syncSchema: async () => {}, - dropTable: async () => {}, - supports: { - transactions: true, - queryFilters: true, - queryAggregations: true, - querySorting: true, - queryPagination: true, - queryWindowFunctions: false, - querySubqueries: false, - joins: false, - fullTextSearch: false, - jsonFields: true, - arrayFields: true, - }, }; const parsed = DriverInterfaceSchema.parse(withLegacyMethod); diff --git a/packages/spec/src/data/driver.zod.ts b/packages/spec/src/data/driver.zod.ts index 54ea8dd7b9..c89560e000 100644 --- a/packages/spec/src/data/driver.zod.ts +++ b/packages/spec/src/data/driver.zod.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { QuerySchema, DateGranularity } from '../data/query.zod'; import { IsolationLevelEnum } from '../shared/enums.zod'; +import { retiredKey } from '../shared/retired-key'; /** * Common Driver Options @@ -98,94 +99,59 @@ export const DriverOptionsSchema = lazySchema(() => z.object({ bypassTenantAudit: z.boolean().optional().describe('Suppress the driver tenant-audit warning for a deliberately global write on a tenant-scoped object (diagnostics only — never changes what the write touches)'), })); +/** + * Shared builder for the 31 capability-bit tombstones below (#4634). + * + * Kept per-key so each prescription can name the mechanism that ACTUALLY + * decides the behaviour — a prescription naming the wrong mechanism is worse + * than none (the #4001 lesson, restated by the datasource `capabilities` + * tombstones in `datasource.zod.ts`). No `os migrate meta` line on any of + * them: a driver is CODE, never stack metadata, so there is no source for the + * conversion chain to rewrite (the #4484 `findStream` precedent) — the D3 + * record is the semantic migration `driver-capabilities-inert-bits-removed`. + */ +const capRemoved = (key: string, mechanism: string) => + `\`DriverCapabilities.${key}\` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 ` + + `enforce-or-remove) — no code in any repository ever read it, so its value never changed ` + + `which code path ran. ${mechanism} Delete the key.`; + /** * Driver Capabilities Schema - * - * Defines what features a database driver supports. - * This allows ObjectQL to adapt its behavior based on underlying database capabilities. - * Enhanced with granular capability flags for better feature detection. + * + * Feature flags a driver ADVERTISES so the ObjectQL engine can pick a code + * path it cannot infer from the driver contract itself. + * + * [#4634, ADR-0049 / ADR-0078] This record once declared 34 bits; a two-repo + * liveness audit (objectstack + cloud, with objectui confirmed clean) found + * THREE with a decision-making reader and thirty-one that no engine, planner, + * REST layer or renderer ever consulted — self-description whose `.describe()` + * strings promised engine adaptation ("if false, ObjectQL will …") that never + * existed: the ADR-0078 false-affordance shape. The thirty-one are tombstoned + * below; each prescription names the mechanism that really decides the + * behaviour. + * + * The surviving contract — a capability bit exists here ONLY where method + * presence on the driver cannot carry the signal: + * + * - `queryDateGranularity` — modulates HOW `aggregate()` is planned, per + * granularity (engine aggregate dispatch; `checkDateBucketParity`). + * - `autonumber` — modulates `create()`/`bulkCreate()`: the engine defers + * autonumber generation to the driver when set. + * - `batchSchemaSync` — opt-in for `syncSchemasBatch()` even where a base + * class inherits the method; the engine ANDs it with method presence. + * + * Everything else IS the method: transactions gate on + * `driver.beginTransaction`, aggregate pushdown on + * `typeof driver.aggregate === 'function'`, schema sync on + * `typeof driver.syncSchema === 'function'`, and the REQUIRED CRUD/bulk + * methods are called unconditionally. Do not add a boolean here for behaviour + * that a method's presence — or a caller that does not exist yet — already + * decides; that is how thirty-one dead bits accumulated. */ export const DriverCapabilitiesSchema = lazySchema(() => z.object({ // ============================================================================ - // Basic CRUD Operations + // Live capability bits — each one has a named reader (see the TSDoc above) // ============================================================================ - - /** - * Whether the driver supports create operations. - */ - create: z.boolean().default(true).describe('Supports CREATE operations'), - - /** - * Whether the driver supports read operations. - */ - read: z.boolean().default(true).describe('Supports READ operations'), - - /** - * Whether the driver supports update operations. - */ - update: z.boolean().default(true).describe('Supports UPDATE operations'), - - /** - * Whether the driver supports delete operations. - */ - delete: z.boolean().default(true).describe('Supports DELETE operations'), - - // ============================================================================ - // Bulk Operations - // ============================================================================ - - /** - * Whether the driver supports bulk create operations. - */ - bulkCreate: z.boolean().default(false).describe('Supports bulk CREATE operations'), - - /** - * Whether the driver supports bulk update operations. - */ - bulkUpdate: z.boolean().default(false).describe('Supports bulk UPDATE operations'), - - /** - * Whether the driver supports bulk delete operations. - */ - bulkDelete: z.boolean().default(false).describe('Supports bulk DELETE operations'), - - // ============================================================================ - // Transaction & Connection Management - // ============================================================================ - - /** - * Whether the driver supports database transactions. - * If true, beginTransaction, commit, and rollback must be implemented. - */ - transactions: z.boolean().default(false).describe('Supports ACID transactions'), - - /** - * Whether the driver supports savepoints within transactions. - */ - savepoints: z.boolean().default(false).describe('Supports transaction savepoints'), - - /** - * Supported transaction isolation levels. - */ - isolationLevels: z.array(IsolationLevelEnum).optional().describe('Supported isolation levels'), - - // ============================================================================ - // Query Operations - // ============================================================================ - - /** - * Whether the driver supports WHERE clause filters. - * If false, ObjectQL will fetch all records and filter in memory. - * - * Example: Memory driver might not support complex filter conditions. - */ - queryFilters: z.boolean().default(true).describe('Supports WHERE clause filtering'), - - /** - * Whether the driver supports aggregation functions (COUNT, SUM, AVG, etc.). - * If false, ObjectQL will compute aggregations in memory. - */ - queryAggregations: z.boolean().default(false).describe('Supports GROUP BY and aggregation functions'), /** * Per-granularity native SQL date bucketing support. @@ -215,84 +181,6 @@ export const DriverCapabilitiesSchema = lazySchema(() => z.object({ queryDateGranularity: z.record(DateGranularity, z.boolean()).optional() .describe('Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing.'), - /** - * Whether the driver supports ORDER BY sorting. - * If false, ObjectQL will sort results in memory. - */ - querySorting: z.boolean().default(true).describe('Supports ORDER BY sorting'), - - /** - * Whether the driver supports LIMIT/OFFSET pagination. - * If false, ObjectQL will fetch all records and paginate in memory. - */ - queryPagination: z.boolean().default(true).describe('Supports LIMIT/OFFSET pagination'), - - /** - * Whether the driver supports window functions (ROW_NUMBER, RANK, LAG, LEAD, etc.). - * If false, ObjectQL will compute window functions in memory. - */ - queryWindowFunctions: z.boolean().default(false).describe('Supports window functions with OVER clause'), - - /** - * Whether the driver supports subqueries (nested SELECT statements). - * If false, ObjectQL will execute queries separately and combine results. - */ - querySubqueries: z.boolean().default(false).describe('Supports subqueries'), - - /** - * Whether the driver supports Common Table Expressions (WITH clause). - */ - queryCTE: z.boolean().default(false).describe('Supports Common Table Expressions (WITH clause)'), - - /** - * Whether the driver supports SQL-style joins. - * If false, ObjectQL will fetch related data separately and join in memory. - */ - joins: z.boolean().default(false).describe('Supports SQL joins'), - - // ============================================================================ - // Advanced Features - // ============================================================================ - - /** - * Whether the driver supports full-text search. - * If true, text search queries can be pushed to the database. - */ - fullTextSearch: z.boolean().default(false).describe('Supports full-text search'), - - /** - * Whether the driver supports JSON querying capabilities. - */ - jsonQuery: z.boolean().default(false).describe('Supports JSON field querying'), - - /** - * Whether the driver supports geospatial queries. - */ - geospatialQuery: z.boolean().default(false).describe('Supports geospatial queries'), - - /** - * Whether the driver supports streaming large result sets. - */ - streaming: z.boolean().default(false).describe('Supports result streaming (cursors/iterators)'), - - /** - * Whether the driver supports JSON field types. - * If false, JSON data will be serialized as strings. - */ - jsonFields: z.boolean().default(false).describe('Supports JSON field types'), - - /** - * Whether the driver supports array field types. - * If false, arrays will be stored as JSON strings or in separate tables. - */ - arrayFields: z.boolean().default(false).describe('Supports array field types'), - - /** - * Whether the driver supports vector embeddings and similarity search. - * Required for RAG (Retrieval-Augmented Generation) and AI features. - */ - vectorSearch: z.boolean().default(false).describe('Supports vector embeddings and similarity search'), - /** * Whether the driver natively generates persistent autonumber / sequence * values inside `create()` / `bulkCreate()` / `upsert()` (e.g. a DB-backed @@ -309,51 +197,150 @@ export const DriverCapabilitiesSchema = lazySchema(() => z.object({ */ autonumber: z.boolean().optional().describe('Driver natively generates persistent autonumber/sequence values'), - // ============================================================================ - // Schema Management - // ============================================================================ - - /** - * Whether the driver supports automatic schema synchronization. - */ - schemaSync: z.boolean().default(false).describe('Supports automatic schema synchronization'), - /** * Whether the driver supports batching multiple schema sync operations * into a single (or fewer) round-trips for the DDL phase. When true, * the engine may call `syncSchemasBatch()` instead of calling * `syncSchema()` per object, reducing network round-trips for remote drivers. + * + * This is the one bit the engine ANDs with method presence + * (`typeof driver.syncSchemasBatch === 'function' && supports.batchSchemaSync`): + * a base class can inherit the METHOD while a transport genuinely cannot + * batch, so presence alone cannot carry the signal. + * + * Optional since 17.0.0 (#4634): absence means `false`, exactly as both + * readers (`engine.ts` / `plugin.ts`) already treated it — the previous + * `.default(false)` forced every capability object to spell out a bit whose + * absence says the same thing. */ - batchSchemaSync: z.boolean().default(false).describe('Supports batched schema sync to reduce schema DDL round-trips'), - - /** - * Whether the driver supports database migrations. - */ - migrations: z.boolean().default(false).describe('Supports database migrations'), - - /** - * Whether the driver supports index management. - */ - indexes: z.boolean().default(false).describe('Supports index creation and management'), + batchSchemaSync: z.boolean().optional().describe('Supports batched schema sync to reduce schema DDL round-trips (absence = false)'), // ============================================================================ - // Performance & Optimization + // Retired capability bits (#4634, ADR-0049 enforce-or-remove) — 17.0.0 // ============================================================================ - - /** - * Whether the driver supports connection pooling. - */ - connectionPooling: z.boolean().default(false).describe('Supports connection pooling'), - - /** - * Whether the driver supports prepared statements. - */ - preparedStatements: z.boolean().default(false).describe('Supports prepared statements (SQL injection prevention)'), - - /** - * Whether the driver supports query result caching. - */ - queryCache: z.boolean().default(false).describe('Supports query result caching'), + // + // Tombstoned, not deleted: this schema is not `.strict()`, so a plain delete + // would let Zod silently STRIP the key — an author (or driver vendor) who + // kept writing it would get a clean parse and a bit that never existed, + // which is the very defect the removal closes. `retiredKey()` keeps each + // key authored-unwritable: tsc rejects it at the authoring site (the class + // literal of every driver that `implements IDataDriver`), and a parse + // rejects it with the prescription below. Tombstones age out ~two majors + // after 17 (see shared/retired-key.ts). + + create: retiredKey(capRemoved('create', + 'CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are ' + + 'REQUIRED `IDataDriver` methods and the engine calls them unconditionally.')), + read: retiredKey(capRemoved('read', + 'CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` ' + + 'methods, called unconditionally.')), + update: retiredKey(capRemoved('update', + 'CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, ' + + 'called unconditionally.')), + delete: retiredKey(capRemoved('delete', + 'CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called ' + + 'unconditionally.')), + + bulkCreate: retiredKey(capRemoved('bulkCreate', + 'The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` ' + + 'methods and the engine calls them directly; wire-level batch capability is advertised ' + + 'by REST discovery from the live composition (#3298), never from this record.')), + bulkUpdate: retiredKey(capRemoved('bulkUpdate', + 'The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; ' + + 'wire-level batch capability is advertised by REST discovery from the live composition ' + + '(#3298), never from this record.')), + bulkDelete: retiredKey(capRemoved('bulkDelete', + 'The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; ' + + 'wire-level batch capability is advertised by REST discovery from the live composition ' + + '(#3298), never from this record.')), + + transactions: retiredKey(capRemoved('transactions', + 'Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` ' + + '(`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method ' + + 'gets the non-transactional fallback, whatever this bit claimed. Discovery\'s ' + + '`transactionalBatch` capability is likewise derived from `engine.transaction` plus the ' + + 'mounted batch route, never from this bit.')), + savepoints: retiredKey(capRemoved('savepoints', + 'No savepoint code path exists in the engine — a capability bit for a feature the ' + + 'platform does not call is a false affordance, not documentation.')), + isolationLevels: retiredKey(capRemoved('isolationLevels', + 'Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no ' + + 'planner ever consulted this list to decide anything.')), + + queryFilters: retiredKey(capRemoved('queryFilters', + '`find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST ' + + 'execute all of it — the "ObjectQL will filter in memory" fallback this bit\'s ' + + 'description promised was never built.')), + querySorting: retiredKey(capRemoved('querySorting', + '`find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will ' + + 'sort in memory" fallback this bit\'s description promised was never built.')), + queryPagination: retiredKey(capRemoved('queryPagination', + '`find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will ' + + 'paginate in memory" fallback this bit\'s description promised was never built.')), + queryAggregations: retiredKey(capRemoved('queryAggregations', + 'Aggregate pushdown is decided by `typeof driver.aggregate === \'function\'` plus ' + + '`queryDateGranularity` (engine aggregate dispatch) — never by this bit.')), + queryWindowFunctions: retiredKey(capRemoved('queryWindowFunctions', + 'ObjectQL never plans window functions through a driver, so there was nothing for the ' + + 'bit to switch on.')), + querySubqueries: retiredKey(capRemoved('querySubqueries', + 'ObjectQL never plans subqueries through a driver, so there was nothing for the bit to ' + + 'switch on.')), + queryCTE: retiredKey(capRemoved('queryCTE', + 'ObjectQL never plans Common Table Expressions through a driver, so there was nothing ' + + 'for the bit to switch on.')), + joins: retiredKey(capRemoved('joins', + 'Related data is resolved by the engine (lookup expansion over `find()`), not by ' + + 'driver-side JOIN planning — no code consulted the bit.')), + + fullTextSearch: retiredKey(capRemoved('fullTextSearch', + '`$search` is compiled by the engine into an `$or` of `$contains` predicates over the ' + + 'searchable fields (ADR-0061) and removed from the AST before the driver sees it — no ' + + 'driver-side full-text path exists.')), + jsonQuery: retiredKey(capRemoved('jsonQuery', + 'No engine path ever branched on driver-side JSON querying.')), + geospatialQuery: retiredKey(capRemoved('geospatialQuery', + 'No geospatial query path exists in the platform — declaring the bit advertised a ' + + 'capability nothing delivers.')), + streaming: retiredKey( + '`DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 ' + + 'enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only ' + + 'read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called ' + + 'it, and two of its three implementations materialised the entire result set before ' + + 'yielding. The bit carried the same defect one level up (`SqlDriver` implemented ' + + '`findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a ' + + 'full-table read) — which is what zero readers makes inevitable. Page large reads ' + + 'through `find()` with `limit`/`offset`. Delete the key.'), + jsonFields: retiredKey(capRemoved('jsonFields', + 'Field-type handling is negotiated per object at `syncSchema` time by the driver itself ' + + '(e.g. `SqlDriver`\'s per-object JSON/date column tracking); no engine path consulted ' + + 'the bit.')), + arrayFields: retiredKey(capRemoved('arrayFields', + 'Field-type handling is negotiated per object at `syncSchema` time by the driver itself; ' + + 'no engine path consulted the bit.')), + vectorSearch: retiredKey(capRemoved('vectorSearch', + 'No vector read path routes through `IDataDriver`. When one exists it should arrive ' + + 'WITH its caller and its capability bit together (the honest order under ' + + 'enforce-or-remove), not as a dangling boolean.')), + + schemaSync: retiredKey(capRemoved('schemaSync', + 'Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === \'function\'` ' + + '(engine and ObjectQL plugin init).')), + migrations: retiredKey(capRemoved('migrations', + 'No migration engine ever consulted it.')), + indexes: retiredKey(capRemoved('indexes', + 'Declared indexes are materialised by the driver itself during schema sync ' + + '(`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit.')), + + connectionPooling: retiredKey(capRemoved('connectionPooling', + 'Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is ' + + 'duck-typed where monitoring wants it. Nothing consulted the bit.')), + preparedStatements: retiredKey(capRemoved('preparedStatements', + 'Parameterised execution is an implementation detail of the driver ' + + '(`execute(command, parameters)`); nothing consulted the bit.')), + queryCache: retiredKey(capRemoved('queryCache', + 'No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint ' + + 'to the driver, not a switch on this bit.')), })); /** diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 398f457c8a..f5f4b1ee9c 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -1137,6 +1137,70 @@ const step17: MigrationStep = { + 'The served /openapi.json is byte-identical before and after — the block never ' + 'reached it.', }, + { + id: 'driver-capabilities-inert-bits-removed', + surface: + 'data.DriverCapabilities.create / data.DriverCapabilities.read / ' + + 'data.DriverCapabilities.update / data.DriverCapabilities.delete / ' + + 'data.DriverCapabilities.bulkCreate / data.DriverCapabilities.bulkUpdate / ' + + 'data.DriverCapabilities.bulkDelete / data.DriverCapabilities.transactions / ' + + 'data.DriverCapabilities.savepoints / data.DriverCapabilities.isolationLevels / ' + + 'data.DriverCapabilities.queryFilters / data.DriverCapabilities.queryAggregations / ' + + 'data.DriverCapabilities.querySorting / data.DriverCapabilities.queryPagination / ' + + 'data.DriverCapabilities.queryWindowFunctions / data.DriverCapabilities.querySubqueries / ' + + 'data.DriverCapabilities.queryCTE / data.DriverCapabilities.joins / ' + + 'data.DriverCapabilities.fullTextSearch / data.DriverCapabilities.jsonQuery / ' + + 'data.DriverCapabilities.geospatialQuery / data.DriverCapabilities.streaming / ' + + 'data.DriverCapabilities.jsonFields / data.DriverCapabilities.arrayFields / ' + + 'data.DriverCapabilities.vectorSearch / data.DriverCapabilities.schemaSync / ' + + 'data.DriverCapabilities.migrations / data.DriverCapabilities.indexes / ' + + 'data.DriverCapabilities.connectionPooling / data.DriverCapabilities.preparedStatements / ' + + 'data.DriverCapabilities.queryCache', + replacement: + '(removed — delete the keys. A driver advertises a capability by implementing the ' + + 'corresponding IDataDriver method; the three bits that survive because method ' + + 'presence cannot carry the signal are `queryDateGranularity`, `autonumber` and ' + + '`batchSchemaSync`)', + reason: + 'The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a ' + + 'capability the contract no longer declares, and the follow-up audit (#4634) checked ' + + 'every bit in the record the same way, across objectstack and cloud (objectui ' + + 'confirmed clean): of 34 declared bits, THREE have a decision-making reader — ' + + '`queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), ' + + '`autonumber` (engine defers generation to the driver), `batchSchemaSync` (engine ' + + 'ANDs it with method presence, because a subclass can inherit `syncSchemasBatch` ' + + 'from a base whose transport batches while its own cannot) — and THIRTY-ONE were ' + + 'written by every driver and read by nothing. Their `.describe()` strings promised ' + + 'engine adaptation ("if false, ObjectQL will filter/sort/paginate in memory") that ' + + 'was never built, and zero readers let the values go WRONG unnoticed: SqlDriver ' + + 'declared `streaming: false` while implementing `findStream`; InMemoryDriver ' + + 'declared `streaming: true` over a full-table read (ADR-0078 false affordance, on ' + + 'the capability record itself). The real mechanism everywhere else is METHOD ' + + 'presence: transactions gate on `driver.beginTransaction`, aggregate pushdown on ' + + '`typeof driver.aggregate`, schema sync on `typeof driver.syncSchema`, and the ' + + 'REQUIRED CRUD/bulk methods are called unconditionally. A driver is CODE, never ' + + 'stack metadata — `supports` literals live in driver classes and ' + + '`DriverConfig.capabilities` is plugin TS configuration, neither ever a ' + + '`sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was ' + + 'retired separately in #4583) — so there is no source for the D2 chain to rewrite ' + + 'and this entry is the D3 record. The keys are tombstoned rather than deleted ' + + 'because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed ' + + '(DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): ' + + 'a plain delete would silently strip a vendor\'s authored bit, replacing one ' + + 'silent no-op with another. `batchSchemaSync` also drops its `.default(false)` ' + + 'for `.optional()` — absence already meant false at both readers, and the default ' + + 'forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634.', + acceptanceCriteria: + 'No `supports` literal or `DriverConfig.capabilities` object authors any of the 31 ' + + 'retired bits — a driver class that still writes one fails tsc against ' + + '`IDataDriver.supports` (the bit is `never`), and a parsed config fails with the ' + + 'per-key prescription. The three in-repo drivers (memory / mongodb / sql) declare ' + + 'only live bits; cloud\'s TursoDriver keeps compiling via its `...super.supports` ' + + 'spread (its stale explicit overrides are cleanup, tracked cloud-side). Engine ' + + 'behaviour is byte-identical: every removed bit had zero readers, and the three ' + + 'live bits keep their readers (engine.ts autonumber defer / aggregate dispatch, ' + + 'plugin.ts + engine.ts batched schema sync, verify date-bucket parity).', + }, ], }; From 91130a166e4a8c968d2be9621d1d5cf85547595b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 06:15:02 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(spec):=20merge=20main(=E5=90=AB=20C10?= =?UTF-8?q?=20#4767)=E5=90=8E=E6=8C=89=E8=A7=84=E9=87=8D=E6=95=B4=E7=94=9F?= =?UTF-8?q?=E6=88=90=E7=89=A9=20=E2=80=94=E2=80=94=20=E4=B8=89=E6=95=8F?= =?UTF-8?q?=E6=84=9F=E6=96=87=E4=BB=B6=E5=8F=96=20main=20=E7=89=88?= =?UTF-8?q?=E5=86=8D=E9=87=8D=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit json-schema.manifest.json / authorable-surface.json / dual-source-exports.baseline.json 一律先取 origin/main 版本(避免静默复活 C10 蓄意删除的 system/EnvironmentArtifact* 键),再以合并后源码全量重跑 gen:schema/check:generated:DriverCapabilities 31 行 [RETIRED] 由生成器重新写入,manifest 与 authorable-surface 中 C10 删键保持已删 (实测两文件 system/EnvironmentArtifact 零残留)。8/8 生成物门禁 + 8 项源码审计 全 PASS,spec 292 文件 7374 测试全绿。 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- packages/spec/authorable-surface.json | 46 --------------------------- 1 file changed, 46 deletions(-) diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 14a918c29c..f7e089995c 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -5944,52 +5944,6 @@ "system/EncryptionConfig:keyManagement", "system/EncryptionConfig:scope", "system/EncryptionConfig:searchableEncryption", - "system/EnvironmentArtifact:builtAt", - "system/EnvironmentArtifact:builtWith", - "system/EnvironmentArtifact:checksum", - "system/EnvironmentArtifact:commitId", - "system/EnvironmentArtifact:environmentId", - "system/EnvironmentArtifact:functions", - "system/EnvironmentArtifact:manifest", - "system/EnvironmentArtifact:metadata", - "system/EnvironmentArtifact:payloadRef", - "system/EnvironmentArtifact:schemaVersion", - "system/EnvironmentArtifactChecksum:algorithm", - "system/EnvironmentArtifactChecksum:value", - "system/EnvironmentArtifactFunction:code", - "system/EnvironmentArtifactFunction:hash", - "system/EnvironmentArtifactFunction:language", - "system/EnvironmentArtifactFunction:name", - "system/EnvironmentArtifactFunction:source", - "system/EnvironmentArtifactManifest:drivers", - "system/EnvironmentArtifactManifest:engine", - "system/EnvironmentArtifactManifest:plugins", - "system/EnvironmentArtifactMetadata:actions", - "system/EnvironmentArtifactMetadata:agents", - "system/EnvironmentArtifactMetadata:apis", - "system/EnvironmentArtifactMetadata:apps", - "system/EnvironmentArtifactMetadata:dashboards", - "system/EnvironmentArtifactMetadata:datasets", - "system/EnvironmentArtifactMetadata:datasources", - "system/EnvironmentArtifactMetadata:fields", - "system/EnvironmentArtifactMetadata:flows", - "system/EnvironmentArtifactMetadata:objects", - "system/EnvironmentArtifactMetadata:pages", - "system/EnvironmentArtifactMetadata:permissionSets", - "system/EnvironmentArtifactMetadata:permissions", - "system/EnvironmentArtifactMetadata:positions", - "system/EnvironmentArtifactMetadata:reports", - "system/EnvironmentArtifactMetadata:skills", - "system/EnvironmentArtifactMetadata:tools", - "system/EnvironmentArtifactMetadata:translations", - "system/EnvironmentArtifactMetadata:triggers", - "system/EnvironmentArtifactMetadata:views", - "system/EnvironmentArtifactMetadata:workflows", - "system/EnvironmentArtifactPayloadRef:checksum", - "system/EnvironmentArtifactPayloadRef:expiresAt", - "system/EnvironmentArtifactPayloadRef:url", - "system/EnvironmentArtifactRequirement:id", - "system/EnvironmentArtifactRequirement:version", "system/ExecuteSqlOperation:description", "system/ExecuteSqlOperation:sql", "system/ExecuteSqlOperation:type",