From af2db4c95480b31e70676c46b2ebccedb6cff113 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 11:02:44 +0000 Subject: [PATCH 1/3] refactor(spec)!: remove never-implemented engine.update() upsert option (#8057, ADR-0049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit options.upsert was declared on both update-options schemas and allowlisted by the engine's unknown-option gate while no engine or driver path ever read it — { upsert: true } was accepted and silently dropped. Removal route per the finding-grading ruling (2026-08-12): retiredKey() tombstones on EngineUpdateOptionsSchema and DataEngineUpdateOptionsSchema sharing one prescription (ENGINE_UPDATE_UPSERT_REMOVED), the key dropped from ENGINE_UPDATE_OPTION_KEYS with the tombstone quoted from ENGINE_RETIRED_OPTION_MESSAGES, ADR-0087 registration for both keys plus the semantic entry engine-update-upsert-retired (no D2 conversion: the option bag is call-time only, the BatchOptions.validateOnly disposition), baselines and reference docs regenerated, pins re-pointed to assert the refusal. Create-if-absent intent is explicit now that the by-id branch throws RECORD_NOT_FOUND per the #7867 not-found gate, which stays as-is. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2 --- .changeset/engine-update-upsert-retired.md | 43 ++++++++++++++++++ content/docs/references/data/data-engine.mdx | 8 ++-- docs/protocol-upgrade-guide.md | 3 ++ .../src/engine-unknown-option.test.ts | 16 ++++++- packages/objectql/src/engine.ts | 15 +++++-- packages/spec/api-surface/data.json | 1 + packages/spec/authorable-defaults/data.json | 2 - packages/spec/authorable-surface/data.json | 4 +- packages/spec/export-origins/data.json | 1 + packages/spec/spec-changes.json | 14 ++++++ packages/spec/src/data/data-engine.test.ts | 37 +++++++++++++--- packages/spec/src/data/data-engine.zod.ts | 30 +++++++++++-- ...7.data__DataEngineUpdateOptions__upsert.ts | 9 ++++ .../17.data__EngineUpdateOptions__upsert.ts | 18 ++++++++ .../17.engine-update-upsert-retired.ts | 25 +++++++++++ packages/spec/src/migrations/registry.ts | 44 +++++++++++++++++++ 16 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 .changeset/engine-update-upsert-retired.md create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.data__DataEngineUpdateOptions__upsert.ts create mode 100644 packages/spec/src/migrations/entries/retired-keys/17.data__EngineUpdateOptions__upsert.ts create mode 100644 packages/spec/src/migrations/entries/semantic/17.engine-update-upsert-retired.ts diff --git a/.changeset/engine-update-upsert-retired.md b/.changeset/engine-update-upsert-retired.md new file mode 100644 index 0000000000..ae9699c30e --- /dev/null +++ b/.changeset/engine-update-upsert-retired.md @@ -0,0 +1,43 @@ +--- +"@objectstack/spec": major +"@objectstack/objectql": patch +--- + +refactor(spec)!: remove the never-implemented `upsert` flag from `engine.update()`'s option surface (#8057, ADR-0049 enforce-or-remove) + +`options.upsert` was declared on both update-options schemas +(`EngineUpdateOptionsSchema` and the deprecated `DataEngineUpdateOptionsSchema`) +and sat on the engine's update allowlist, yet **no engine or driver path ever +read it**: it was not a driver pass-through key and `ObjectQL.update()` never +referenced it. A caller passing `{ upsert: true }` got silence — not a refusal, +not an upsert. The one place a caller would learn the truth was by reading the +engine, and the strict-unknown gate (the mechanism that normally catches a +meaningless option) actively vouched for the key. + +FROM → TO: delete `upsert` from any `engine.update()` / `updateData` option +bag. For create-if-absent intent, express it explicitly — read the row first +(`findOne`) and call `insert` or `update` on what you find. Note the by-id +update branch now throws `RECORD_NOT_FOUND` when the id names no row (#7867's +not-found gate); a future first-class upsert must reconcile with that gate by +design, which is why the flag is removed rather than implemented. + +The retirement kit: + +- `retiredKey()` tombstones on BOTH schemas, one shared prescription + (`ENGINE_UPDATE_UPSERT_REMOVED`): authoring the key is a tsc error and a + parse error carrying the fix. +- The objectql engine drops `upsert` from `ENGINE_UPDATE_OPTION_KEYS` and + quotes the same prescription from its unknown-option gate + (`ENGINE_RETIRED_OPTION_MESSAGES`), so the untyped runtime path is loud too. +- **ADR-0087 registry**: both keys registered in `RETIRED_KEYS_BY_MAJOR[17]` + plus the D3 semantic entry `engine-update-upsert-retired`. **No D2 + conversion**, deliberately: an engine option bag is call-time only — nobody + authors one and nothing persists one (the `BatchOptions.validateOnly` + disposition). +- Baselines (`authorable-surface/data.json` `[RETIRED]` marks, + `authorable-defaults`, `api-surface`, `spec-changes.json`, upgrade guide, + reference docs) regenerated deliberately. + +No runtime behaviour changes for any in-tree caller — zero production call +sites passed the flag (measured in #8057); the only references were the spec's +own schema tests, now re-pointed to assert the refusal. diff --git a/content/docs/references/data/data-engine.mdx b/content/docs/references/data/data-engine.mdx index c4c5d767b5..e8d2a14043 100644 --- a/content/docs/references/data/data-engine.mdx +++ b/content/docs/references/data/data-engine.mdx @@ -291,7 +291,7 @@ This schema accepts one of the following structures: | **object** | `string` | ✅ | | | **data** | `Record` | ✅ | | | **id** | `string \| number` | optional | ID for single update, or use where in options | -| **options** | `{ context?: object; where?: Record \| any; upsert?: boolean; multi?: boolean; … }` | optional | | +| **options** | `{ context?: object; where?: Record \| any; multi?: boolean; returning?: boolean; … }` | optional | | --- @@ -402,7 +402,7 @@ Options for DataEngine.update operations | :--- | :--- | :--- | :--- | | **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | | **filter** | `Record \| any` | optional | Data Engine query filter conditions | -| **upsert** | `boolean` | optional | | +| **upsert** | `never` | optional | [REMOVED] `update.options.upsert` was removed in @objectstack/spec 17 (#8057, ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (#7867's not-found gate) rather than inserting, so read the row first (`findOne`) and call `insert` or `update` on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. | | **multi** | `boolean` | optional | | | **returning** | `boolean` | optional | | @@ -419,7 +419,7 @@ Options for DataEngine.update operations | **object** | `string` | ✅ | | | **data** | `Record` | ✅ | | | **id** | `string \| number` | optional | ID for single update, or use where in options | -| **options** | `{ context?: object; where?: Record \| any; upsert?: boolean; multi?: boolean; … }` | optional | | +| **options** | `{ context?: object; where?: Record \| any; multi?: boolean; returning?: boolean; … }` | optional | | --- @@ -537,7 +537,7 @@ QueryAST-aligned options for DataEngine.update operations | :--- | :--- | :--- | :--- | | **context** | `{ userId?: string; actor?: string; attributedUserId?: string; email?: string; … }` | optional | | | **where** | `Record \| any` | optional | | -| **upsert** | `boolean` | optional | | +| **upsert** | `never` | optional | [REMOVED] `update.options.upsert` was removed in @objectstack/spec 17 (#8057, ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (#7867's not-found gate) rather than inserting, so read the row first (`findOne`) and call `insert` or `update` on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. | | **multi** | `boolean` | optional | | | **returning** | `boolean` | optional | | diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index b8dad38e4d..577e2e2115 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -413,6 +413,9 @@ This is a CODE-path API, not stored metadata, so — like `hook-register-empty-o ONE AUTHOR-REACHABLE SURFACE reaches this indirectly and is why it is not purely a code-side note: a saved report's `query.orderBy` (`sys_saved_report`) is forwarded verbatim into `engine.find` by `plugin-reports`, bypassing the ingress gate. A report authored to sort by a formula field used to run and return rows in an arbitrary order; it now fails loudly, with the remedy in the message. One further path is deliberately NOT a refusal: a nested `expand` sort raises this refusal inside `expandRelatedRecords`, whose pre-existing graceful-degradation `catch` swallows every expand failure and retains the raw foreign keys — so that path moves from silent to OBSERVABLE (a warning naming the field and the fix) rather than refusing. Reversing that backstop is a separate decision on all expand failure modes. #7095, #6994, #6924, #4226, #4256, #3821, ADR-0112. - Done when: No `engine.find` / `engine.findOne` call site sorts by a `formula` field, and no saved report's `query.orderBy` names one — grep your report definitions for an `orderBy` field whose object declares it as a `formula`, and denormalise it onto a stored column written when the source changes. A `summary` / rollup field needs no action: it has a real maintained column and sorts correctly. Reads complete with no `INVALID_SORT` naming a formula field, and no "Failed to expand relationship field" warning whose error text names one. +- **`engine-update-upsert-retired`** — `data.engine.update options.upsert` → (removed — never implemented; express create-if-absent explicitly: `findOne` first, then `insert` or `update` on what you find) + - Why not automatic: The `upsert` flag promised insert-if-absent on `engine.update()` but no engine or driver path ever read it: the key was declared on both update-options schemas and allowlisted by the unknown-option gate, yet `ObjectQL.update()` never referenced it and it was not a driver pass-through key — `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update (ADR-0049 declared-but-unenforced). There is no behaviour to preserve and nothing stored to rewrite (it only ever appeared in a call-time option bag). Any future first-class upsert must reconcile with #7867's not-found gate — a by-id update whose id names no row throws RECORD_NOT_FOUND rather than inserting — which is why the flag is removed rather than implemented here. + - Done when: No caller passes `options.upsert` to `engine.update()`; a call that includes it is refused loudly (the engine gate and both schemas quote the #8057 prescription) instead of succeeding with the option silently ignored. - **`enhanced-api-error-field-errors-renamed`** — `api.enhancedApiError.fieldErrors` → fields - Why not automatic: The wire has always carried `fields` — the validators, import coercion, validation-failure.ts, @objectstack/client and the console's field-error extractor all say `fields`, and nothing ever emitted `fieldErrors`, so a reader keying on it was reading a field no server sent (ADR-0078's silently-inert declaration, on the error envelope). This is a RESPONSE surface: no stack, example or template carries the key, so there is no source for the chain to rewrite — the schema tombstones it via retiredKey() and consumers move their read themselves. ADR-0114 D4, #3977. - Done when: No consumer reads `error.fieldErrors`; per-field validation detail is read from `error.fields`, and constructing an EnhancedApiError with `fieldErrors` fails to parse with the rename prescription instead of silently losing the array. diff --git a/packages/objectql/src/engine-unknown-option.test.ts b/packages/objectql/src/engine-unknown-option.test.ts index c76fe54353..414b147392 100644 --- a/packages/objectql/src/engine-unknown-option.test.ts +++ b/packages/objectql/src/engine-unknown-option.test.ts @@ -142,6 +142,20 @@ describe('unknown engine option keys are rejected (#4371 option 2)', () => { .rejects.toThrow(/#4286, ADR-0049/); }); + it('update `upsert` is rejected with its #8057 tombstone — it was accepted and silently dropped', async () => { + // [#8057] `upsert` sat on the update allowlist while nothing read it + // (declared-but-unenforced, ADR-0049). The refusal quotes the spec's + // prescription: delete the key; create-if-absent is explicit now that + // the by-id branch throws on a missing row (#7867's not-found gate). + await expect(engine.update('task', { title: 'Z' }, { where: { id: a.id }, upsert: true } as any)) + .rejects.toThrow(/`update\.options\.upsert` was removed .*#8057, ADR-0049.*never implemented.*Delete the key.*not-found gate/s); + }); + + it('a null-valued `upsert` stays a withdrawal — no intent a drop could lose', async () => { + const updated = await engine.update('task', { title: 'A4' }, { where: { id: a.id }, upsert: null } as any); + expect(updated).toBeDefined(); + }); + // ── null stays a withdrawal ───────────────────────────────────────── it('a null-valued unknown key is a withdrawal, not a rejection', async () => { @@ -203,7 +217,7 @@ describe('unknown engine option keys are rejected (#4371 option 2)', () => { // ── drift pin: legal sets stay glued to the spec schemas ──────────── it('each legal set covers its schema shape (minus tombstones) and only the documented extras', () => { - const TOMBSTONES = new Set(['cursor', 'distinct']); + const TOMBSTONES = new Set(['cursor', 'distinct', 'upsert']); const PASSTHROUGH = ['transaction', 'tenantId', 'tenantIds', 'timezone', 'bypassTenantAudit', 'preserveAudit']; const expectSetMatches = ( setName: string, diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index f6ea2c9c96..c996b11845 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -17,6 +17,7 @@ import { foldQueryAliasSlots, QUERY_CURSOR_REMOVED, QUERY_DISTINCT_REMOVED, + ENGINE_UPDATE_UPSERT_REMOVED, type QueryAliasSlot, type DroppedFieldsEvent } from '@objectstack/spec/data'; @@ -328,9 +329,9 @@ const ENGINE_DRIVER_PASSTHROUGH_KEYS = [ * wire-alias rejection above. * * Sources, in order: the method's `Engine*OptionsSchema` declared keys (minus - * the `retiredKey` tombstones `cursor`/`distinct`, which get their tombstone - * quoted instead of a generic rejection — the schema keeps them ONLY to carry - * that message, and this runtime path never parses); `searchFields` (read by + * the `retiredKey` tombstones `cursor`/`distinct`/`upsert` (#8057), which get + * their tombstone quoted instead of a generic rejection — the schema keeps them + * ONLY to carry that message, and this runtime path never parses); `searchFields` (read by * `find` at the `$search` expansion, sent by the protocol layer); * `onFieldsDropped` and `strictReadonlyWrites` (`WriteObservabilityOptions` — * contract-declared, deliberately outside the serializable Zod schema: the @@ -350,7 +351,7 @@ const ENGINE_FIND_OPTION_KEYS: ReadonlySet = new Set([ ...ENGINE_DRIVER_PASSTHROUGH_KEYS, ]); const ENGINE_UPDATE_OPTION_KEYS: ReadonlySet = new Set([ - 'context', 'where', 'upsert', 'multi', 'returning', 'onFieldsDropped', 'strictReadonlyWrites', + 'context', 'where', 'multi', 'returning', 'onFieldsDropped', 'strictReadonlyWrites', ...ENGINE_DRIVER_PASSTHROUGH_KEYS, ]); const ENGINE_DELETE_OPTION_KEYS: ReadonlySet = new Set([ @@ -457,6 +458,12 @@ function readStoredAutonumberCounter(value: string, prefix: string, suffix: stri const ENGINE_RETIRED_OPTION_MESSAGES: Record = { cursor: QUERY_CURSOR_REMOVED, distinct: QUERY_DISTINCT_REMOVED, + // [#8057] `update.options.upsert` — declared-but-unenforced (ADR-0049): the + // key sat on the update allowlist while nothing read it, so `{ upsert: true }` + // was accepted and silently dropped. Removed rather than implemented; the + // spec's tombstone carries the create-if-absent prescription (#7867's + // not-found gate is the semantics a caller must reconcile with). + upsert: ENGINE_UPDATE_UPSERT_REMOVED, }; /** diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index e90c801d2d..d71bc6198a 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -201,6 +201,7 @@ "DriverVocabularyEntry (interface)", "DroppedFieldsEvent (type)", "DroppedFieldsEventSchema (const)", + "ENGINE_UPDATE_UPSERT_REMOVED (const)", "ESignatureConfig (type)", "ESignatureConfigParsed (type)", "ESignatureConfigSchema (const)", diff --git a/packages/spec/authorable-defaults/data.json b/packages/spec/authorable-defaults/data.json index 6c32c33cd2..1cf33d2863 100644 --- a/packages/spec/authorable-defaults/data.json +++ b/packages/spec/authorable-defaults/data.json @@ -19,7 +19,6 @@ "data/DataEngineInsertOptions:returning = true", "data/DataEngineUpdateOptions:multi = false", "data/DataEngineUpdateOptions:returning = false", - "data/DataEngineUpdateOptions:upsert = false", "data/DataEngineVectorFindRequest:limit = 5", "data/Datasource:active = true", "data/Datasource:autoConnect = false", @@ -33,7 +32,6 @@ "data/EngineDeleteOptions:multi = false", "data/EngineUpdateOptions:multi = false", "data/EngineUpdateOptions:returning = false", - "data/EngineUpdateOptions:upsert = false", "data/ExternalColumn:primaryKey = false", "data/ExternalDatasourceSettings:allowWrites = false", "data/ExternalDatasourceSettings:queryTimeoutMs = 30000", diff --git a/packages/spec/authorable-surface/data.json b/packages/spec/authorable-surface/data.json index 3798cc954f..ba4da252d7 100644 --- a/packages/spec/authorable-surface/data.json +++ b/packages/spec/authorable-surface/data.json @@ -142,7 +142,7 @@ "data/DataEngineUpdateOptions:filter", "data/DataEngineUpdateOptions:multi", "data/DataEngineUpdateOptions:returning", - "data/DataEngineUpdateOptions:upsert", + "data/DataEngineUpdateOptions:upsert [RETIRED]", "data/DataEngineUpdateRequest:data", "data/DataEngineUpdateRequest:id", "data/DataEngineUpdateRequest:method", @@ -304,7 +304,7 @@ "data/EngineUpdateOptions:context", "data/EngineUpdateOptions:multi", "data/EngineUpdateOptions:returning", - "data/EngineUpdateOptions:upsert", + "data/EngineUpdateOptions:upsert [RETIRED]", "data/EngineUpdateOptions:where", "data/EqualityOperator:$eq", "data/EqualityOperator:$ne", diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index bdc33b0812..cdaf306e36 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -201,6 +201,7 @@ "DriverVocabularyEntry": "src/data/driver/config-registry.zod.ts#DriverVocabularyEntry (interface)", "DroppedFieldsEvent": "src/data/data-engine.zod.ts#DroppedFieldsEvent (type)", "DroppedFieldsEventSchema": "src/data/data-engine.zod.ts#DroppedFieldsEventSchema (const)", + "ENGINE_UPDATE_UPSERT_REMOVED": "src/data/data-engine.zod.ts#ENGINE_UPDATE_UPSERT_REMOVED (const)", "ESignatureConfig": "src/data/document.zod.ts#ESignatureConfig (type)", "ESignatureConfigParsed": "src/data/document.zod.ts#ESignatureConfigParsed (type)", "ESignatureConfigSchema": "src/data/document.zod.ts#ESignatureConfigSchema (const)", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index d70d7692dc..3ef1f8373b 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -698,6 +698,13 @@ "toMajor": 17, "rationale": "#4226 / #4256 / #6994 closed the SORT axis at the REST ingress (`assertSortFieldsExist`, `400 INVALID_SORT`), which covers everything reaching `findData`: the list route, `POST /data/:object/query`, the export route and the RPC dispatcher. A caller reaching `engine.find()` / `engine.findOne()` DIRECTLY passed through none of it, and a `formula` ORDER BY there was dropped in silence. Measured on a real driver: `asc` and `desc` came back BYTE-IDENTICAL, in insertion order, under a success, with the rows carrying the very values they were asked to be ordered by. No column exists to order by (a formula is computed on read, so no driver materialises one), so the ORDER BY reached the driver, found nothing, and the unknown-column backstop returned the rows unordered.\n\nRuled 2026-08-10 on #7095: an ORDER BY the engine cannot apply is a 4xx with guidance prose at the public boundary, never a silent drop — the same direction as the analytics dataset refusal envelope and the #6924 sort-hint prescription. The engine's documented internal-caller tolerance (`assertProjectionFieldsExist`'s docblock) was to survive only behind a pinned internal path, and only if a MEASURED internal call site relied on it. The #7095 sweep of every in-tree `orderBy` reaching the engine directly — hooks, flows, reports, queue/job adapters, sharing, metadata loaders, expand sub-reads — found NONE: every hardcoded internal sort names a real stored column (`created_at`, `updated_at`, `version`, `priority`, `scheduled_for`, `started_at`, `next_run_at`, `recorded_at`, `id`), and no shipped object in the repo declares a `formula` field at all. So no internal path shipped, and there is no flag to opt back into the drop.\n\nThis is a CODE-path API, not stored metadata, so — like `hook-register-empty-object-target-refused` at this step — there is no `sys_metadata` row for the D2 chain to rewrite and the ledger entry is the notification channel. No mechanical rewrite exists in either direction: the platform cannot invent the stored column the remedy prescribes, and it must not sort post-hoc instead — `driver.find` has already applied `limit` / `offset`, so re-sorting after the formulas are evaluated would reorder an ARBITRARY PAGE, which looks correct on small result sets and is wrong the moment pagination is involved.\n\nONE AUTHOR-REACHABLE SURFACE reaches this indirectly and is why it is not purely a code-side note: a saved report's `query.orderBy` (`sys_saved_report`) is forwarded verbatim into `engine.find` by `plugin-reports`, bypassing the ingress gate. A report authored to sort by a formula field used to run and return rows in an arbitrary order; it now fails loudly, with the remedy in the message. One further path is deliberately NOT a refusal: a nested `expand` sort raises this refusal inside `expandRelatedRecords`, whose pre-existing graceful-degradation `catch` swallows every expand failure and retains the raw foreign keys — so that path moves from silent to OBSERVABLE (a warning naming the field and the fix) rather than refusing. Reversing that backstop is a separate decision on all expand failure modes. #7095, #6994, #6924, #4226, #4256, #3821, ADR-0112." }, + { + "surface": "data.engine.update options.upsert", + "replacement": "(removed — never implemented; express create-if-absent explicitly: `findOne` first, then `insert` or `update` on what you find)", + "migrationId": "engine-update-upsert-retired", + "toMajor": 17, + "rationale": "The `upsert` flag promised insert-if-absent on `engine.update()` but no engine or driver path ever read it: the key was declared on both update-options schemas and allowlisted by the unknown-option gate, yet `ObjectQL.update()` never referenced it and it was not a driver pass-through key — `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update (ADR-0049 declared-but-unenforced). There is no behaviour to preserve and nothing stored to rewrite (it only ever appeared in a call-time option bag). Any future first-class upsert must reconcile with #7867's not-found gate — a by-id update whose id names no row throws RECORD_NOT_FOUND rather than inserting — which is why the flag is removed rather than implemented here." + }, { "surface": "api.enhancedApiError.fieldErrors", "replacement": "fields", @@ -1720,6 +1727,13 @@ "toMajor": 17, "rationale": "#4226 / #4256 / #6994 closed the SORT axis at the REST ingress (`assertSortFieldsExist`, `400 INVALID_SORT`), which covers everything reaching `findData`: the list route, `POST /data/:object/query`, the export route and the RPC dispatcher. A caller reaching `engine.find()` / `engine.findOne()` DIRECTLY passed through none of it, and a `formula` ORDER BY there was dropped in silence. Measured on a real driver: `asc` and `desc` came back BYTE-IDENTICAL, in insertion order, under a success, with the rows carrying the very values they were asked to be ordered by. No column exists to order by (a formula is computed on read, so no driver materialises one), so the ORDER BY reached the driver, found nothing, and the unknown-column backstop returned the rows unordered.\n\nRuled 2026-08-10 on #7095: an ORDER BY the engine cannot apply is a 4xx with guidance prose at the public boundary, never a silent drop — the same direction as the analytics dataset refusal envelope and the #6924 sort-hint prescription. The engine's documented internal-caller tolerance (`assertProjectionFieldsExist`'s docblock) was to survive only behind a pinned internal path, and only if a MEASURED internal call site relied on it. The #7095 sweep of every in-tree `orderBy` reaching the engine directly — hooks, flows, reports, queue/job adapters, sharing, metadata loaders, expand sub-reads — found NONE: every hardcoded internal sort names a real stored column (`created_at`, `updated_at`, `version`, `priority`, `scheduled_for`, `started_at`, `next_run_at`, `recorded_at`, `id`), and no shipped object in the repo declares a `formula` field at all. So no internal path shipped, and there is no flag to opt back into the drop.\n\nThis is a CODE-path API, not stored metadata, so — like `hook-register-empty-object-target-refused` at this step — there is no `sys_metadata` row for the D2 chain to rewrite and the ledger entry is the notification channel. No mechanical rewrite exists in either direction: the platform cannot invent the stored column the remedy prescribes, and it must not sort post-hoc instead — `driver.find` has already applied `limit` / `offset`, so re-sorting after the formulas are evaluated would reorder an ARBITRARY PAGE, which looks correct on small result sets and is wrong the moment pagination is involved.\n\nONE AUTHOR-REACHABLE SURFACE reaches this indirectly and is why it is not purely a code-side note: a saved report's `query.orderBy` (`sys_saved_report`) is forwarded verbatim into `engine.find` by `plugin-reports`, bypassing the ingress gate. A report authored to sort by a formula field used to run and return rows in an arbitrary order; it now fails loudly, with the remedy in the message. One further path is deliberately NOT a refusal: a nested `expand` sort raises this refusal inside `expandRelatedRecords`, whose pre-existing graceful-degradation `catch` swallows every expand failure and retains the raw foreign keys — so that path moves from silent to OBSERVABLE (a warning naming the field and the fix) rather than refusing. Reversing that backstop is a separate decision on all expand failure modes. #7095, #6994, #6924, #4226, #4256, #3821, ADR-0112." }, + { + "surface": "data.engine.update options.upsert", + "replacement": "(removed — never implemented; express create-if-absent explicitly: `findOne` first, then `insert` or `update` on what you find)", + "migrationId": "engine-update-upsert-retired", + "toMajor": 17, + "rationale": "The `upsert` flag promised insert-if-absent on `engine.update()` but no engine or driver path ever read it: the key was declared on both update-options schemas and allowlisted by the unknown-option gate, yet `ObjectQL.update()` never referenced it and it was not a driver pass-through key — `{ upsert: true }` was accepted and silently dropped and the update stayed a plain update (ADR-0049 declared-but-unenforced). There is no behaviour to preserve and nothing stored to rewrite (it only ever appeared in a call-time option bag). Any future first-class upsert must reconcile with #7867's not-found gate — a by-id update whose id names no row throws RECORD_NOT_FOUND rather than inserting — which is why the flag is removed rather than implemented here." + }, { "surface": "api.enhancedApiError.fieldErrors", "replacement": "fields", diff --git a/packages/spec/src/data/data-engine.test.ts b/packages/spec/src/data/data-engine.test.ts index 26313ceb43..85e75a2cb7 100644 --- a/packages/spec/src/data/data-engine.test.ts +++ b/packages/spec/src/data/data-engine.test.ts @@ -193,12 +193,21 @@ describe('DataEngineUpdateOptionsSchema', () => { expect(options).toBeDefined(); }); - it('should accept upsert mode', () => { - const options = DataEngineUpdateOptionsSchema.parse({ - upsert: true, - }); - - expect(options.upsert).toBe(true); + it('REJECTS the retired `upsert` flag, with the fix in the message (#8057)', () => { + // Tombstoned, not deleted: the schema is not `.strict()`, so a plain + // deletion would silently strip the key — the exact accepted-and-dropped + // silence #8057 removes. `retiredKey()` makes the removal audible. + expect(() => + DataEngineUpdateOptionsSchema.parse({ + upsert: true, + }), + ).toThrow(/`update\.options\.upsert` was removed .*#8057, ADR-0049.*Delete the key/s); + }); + + it('parses cleanly without the retired flag, and never produces the key', () => { + const options = DataEngineUpdateOptionsSchema.parse({ multi: true }); + expect(options.multi).toBe(true); + expect(options).not.toHaveProperty('upsert'); }); it('should accept multi-update', () => { @@ -449,6 +458,20 @@ describe('EngineUpdateOptionsSchema', () => { expect(options.where).toBeDefined(); expect(options.multi).toBe(true); }); + + it('REJECTS the retired `upsert` flag with the same #8057 tombstone as the legacy schema', () => { + expect(() => + EngineUpdateOptionsSchema.parse({ + where: { status: 'inactive' }, + upsert: true, + }), + ).toThrow(/`update\.options\.upsert` was removed .*#8057, ADR-0049.*Delete the key/s); + }); + + it('never produces the retired key on a clean parse', () => { + const options = EngineUpdateOptionsSchema.parse({ where: { status: 'inactive' } }); + expect(options).not.toHaveProperty('upsert'); + }); }); describe('EngineDeleteOptionsSchema', () => { @@ -1029,7 +1052,7 @@ describe('Integration Tests', () => { it('should accept options without context (backward compatible)', () => { const queryOpts = DataEngineQueryOptionsSchema.parse({ filter: { x: 1 } }); const insertOpts = DataEngineInsertOptionsSchema.parse({ returning: true }); - const updateOpts = DataEngineUpdateOptionsSchema.parse({ upsert: true }); + const updateOpts = DataEngineUpdateOptionsSchema.parse({ multi: true }); const deleteOpts = DataEngineDeleteOptionsSchema.parse({ multi: true }); const countOpts = DataEngineCountOptionsSchema.parse({}); const aggOpts = DataEngineAggregateOptionsSchema.parse({ groupBy: ['a'] }); diff --git a/packages/spec/src/data/data-engine.zod.ts b/packages/spec/src/data/data-engine.zod.ts index f9acad3c81..621c82e863 100644 --- a/packages/spec/src/data/data-engine.zod.ts +++ b/packages/spec/src/data/data-engine.zod.ts @@ -204,11 +204,34 @@ export const DataEngineInsertOptionsSchema = lazySchema(() => BaseEngineOptionsS // 4. method: UPDATE (QueryAST-aligned) // ========================================================================== +/** + * Exported (like {@link QUERY_CURSOR_REMOVED}) because ONE prescription serves + * three rejection sites: both update-options schemas below re-declare the key + * as a tombstone, and the objectql engine's unknown-option gate + * (`ENGINE_RETIRED_OPTION_MESSAGES` in `engine.ts`) quotes the same string at + * the runtime entry point, where the untyped option bag never parses. + * + * No `os migrate meta` sentence, deliberately: an ADR-0087 D2 conversion + * rewrites an authored source or a stored `sys_metadata` row, and this key is + * call-time only — nobody authors an engine option bag and nothing persists + * one. The removal reaches consumers as the protocol-17 semantic migration + * `engine-update-upsert-retired` plus this tombstone (the + * `BatchOptions.validateOnly` / `ListNotificationsRequest.cursor` disposition). + */ +export const ENGINE_UPDATE_UPSERT_REMOVED = + '`update.options.upsert` was removed in @objectstack/spec 17 (#8057, ADR-0049) — it was ' + + 'declared and allowlisted but never implemented: no engine or driver path ever read it, so ' + + '`{ upsert: true }` was accepted and silently dropped and the update stayed a plain update. ' + + 'Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row ' + + "throws RECORD_NOT_FOUND (#7867's not-found gate) rather than inserting, so read the row " + + 'first (`findOne`) and call `insert` or `update` on what you find. A first-class upsert, if ' + + 'ever built, must reconcile with that gate by design rather than through this silent flag.'; + export const EngineUpdateOptionsSchema = lazySchema(() => BaseEngineOptionsSchema.extend({ /** Filter conditions to identify records to update — standard QueryAST `where` */ where: z.union([z.record(z.string(), z.unknown()), FilterConditionSchema]).optional(), - /** Perform an upsert? If true, insert if not found. */ - upsert: z.boolean().default(false).optional(), + /** Upsert flag — REMOVED (#8057): declared-but-unenforced (ADR-0049); the tombstone carries the prescription. */ + upsert: retiredKey(ENGINE_UPDATE_UPSERT_REMOVED), /** Update multiple records? If false, only the first match is updated. Default: false */ multi: z.boolean().default(false).optional(), /** Return the updated record(s)? Default: false (returns update count/status) */ @@ -289,7 +312,8 @@ export const DroppedFieldsEventSchema = lazySchema(() => z.object({ export const DataEngineUpdateOptionsSchema = lazySchema(() => BaseEngineOptionsSchema.extend({ /** @deprecated Use `where` (EngineUpdateOptionsSchema) */ filter: DataEngineFilterSchema.optional(), - upsert: z.boolean().default(false).optional(), + /** Upsert flag — REMOVED (#8057); same tombstone as `EngineUpdateOptionsSchema.upsert`. */ + upsert: retiredKey(ENGINE_UPDATE_UPSERT_REMOVED), multi: z.boolean().default(false).optional(), returning: z.boolean().default(false).optional(), }).describe('Options for DataEngine.update operations')); diff --git a/packages/spec/src/migrations/entries/retired-keys/17.data__DataEngineUpdateOptions__upsert.ts b/packages/spec/src/migrations/entries/retired-keys/17.data__DataEngineUpdateOptions__upsert.ts new file mode 100644 index 0000000000..9ceab8374d --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/17.data__DataEngineUpdateOptions__upsert.ts @@ -0,0 +1,9 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #8057 — the deprecated legacy update-options schema re-declared the +// never-implemented `upsert` flag, so it is tombstoned with the SAME +// prescription as `data/EngineUpdateOptions:upsert` (one string, both +// schemas plus the engine's unknown-option gate): a capability is never +// half-deleted. See that entry's comment for the full disposition; no D2 +// conversion, since an engine option bag is call-time only. +export const entry = 'data/DataEngineUpdateOptions:upsert'; diff --git a/packages/spec/src/migrations/entries/retired-keys/17.data__EngineUpdateOptions__upsert.ts b/packages/spec/src/migrations/entries/retired-keys/17.data__EngineUpdateOptions__upsert.ts new file mode 100644 index 0000000000..915d524e6b --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/17.data__EngineUpdateOptions__upsert.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #8057 — the never-implemented upsert flag on `engine.update()`'s option +// surface, tombstoned on BOTH update-options schemas (this one and the +// deprecated `DataEngineUpdateOptions` sibling) with one prescription: +// `ENGINE_UPDATE_UPSERT_REMOVED` in `data/data-engine.zod.ts`, which the +// objectql engine's unknown-option gate quotes too. Declared-but-unenforced +// (ADR-0049): the key sat on the engine's update allowlist while no code path +// read it, so `{ upsert: true }` was accepted and silently dropped. +// +// Registered here but NOT in `src/conversions/registry.ts`, and that asymmetry +// is the point rather than an omission: a D2 conversion rewrites an authored +// source or a stored `sys_metadata` row, and an engine option bag is call-time +// only — nobody authors one and nothing persists one. The prescription reaches +// consumers as the D3 semantic entry `engine-update-upsert-retired` plus this +// tombstone (the `BatchOptions.validateOnly` / `ListNotificationsRequest:cursor` +// disposition). +export const entry = 'data/EngineUpdateOptions:upsert'; diff --git a/packages/spec/src/migrations/entries/semantic/17.engine-update-upsert-retired.ts b/packages/spec/src/migrations/entries/semantic/17.engine-update-upsert-retired.ts new file mode 100644 index 0000000000..bcc9f5e13e --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/17.engine-update-upsert-retired.ts @@ -0,0 +1,25 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'engine-update-upsert-retired', + surface: 'data.engine.update options.upsert', + replacement: + '(removed — never implemented; express create-if-absent explicitly: `findOne` first, then ' + + '`insert` or `update` on what you find)', + reason: + 'The `upsert` flag promised insert-if-absent on `engine.update()` but no engine or driver ' + + 'path ever read it: the key was declared on both update-options schemas and allowlisted by ' + + 'the unknown-option gate, yet `ObjectQL.update()` never referenced it and it was not a ' + + 'driver pass-through key — `{ upsert: true }` was accepted and silently dropped and the ' + + 'update stayed a plain update (ADR-0049 declared-but-unenforced). There is no behaviour to ' + + 'preserve and nothing stored to rewrite (it only ever appeared in a call-time option bag). ' + + "Any future first-class upsert must reconcile with #7867's not-found gate — a by-id update " + + 'whose id names no row throws RECORD_NOT_FOUND rather than inserting — which is why the ' + + 'flag is removed rather than implemented here.', + acceptanceCriteria: + 'No caller passes `options.upsert` to `engine.update()`; a call that includes it is refused ' + + 'loudly (the engine gate and both schemas quote the #8057 prescription) instead of ' + + 'succeeding with the option silently ignored.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index ca8e8ec26d..a4212ebc91 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -2767,6 +2767,27 @@ const step17: MigrationStep = { + '`INVALID_SORT` naming a formula field, and no "Failed to expand relationship field" ' + 'warning whose error text names one.', }, + { + id: 'engine-update-upsert-retired', + surface: 'data.engine.update options.upsert', + replacement: + '(removed — never implemented; express create-if-absent explicitly: `findOne` first, then ' + + '`insert` or `update` on what you find)', + reason: + 'The `upsert` flag promised insert-if-absent on `engine.update()` but no engine or driver ' + + 'path ever read it: the key was declared on both update-options schemas and allowlisted by ' + + 'the unknown-option gate, yet `ObjectQL.update()` never referenced it and it was not a ' + + 'driver pass-through key — `{ upsert: true }` was accepted and silently dropped and the ' + + 'update stayed a plain update (ADR-0049 declared-but-unenforced). There is no behaviour to ' + + 'preserve and nothing stored to rewrite (it only ever appeared in a call-time option bag). ' + + "Any future first-class upsert must reconcile with #7867's not-found gate — a by-id update " + + 'whose id names no row throws RECORD_NOT_FOUND rather than inserting — which is why the ' + + 'flag is removed rather than implemented here.', + acceptanceCriteria: + 'No caller passes `options.upsert` to `engine.update()`; a call that includes it is refused ' + + 'loudly (the engine gate and both schemas quote the #8057 prescription) instead of ' + + 'succeeding with the option silently ignored.', + }, { id: 'enhanced-api-error-field-errors-renamed', surface: 'api.enhancedApiError.fieldErrors', @@ -4726,6 +4747,29 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // takes (`query-joins-retired` / `query-cursor-retired` / // `query-distinct-retired` / `query-window-functions-retired`, #4286). 'data/AggregationNode:distinct', + // #8057 — the deprecated legacy update-options schema re-declared the + // never-implemented `upsert` flag, so it is tombstoned with the SAME + // prescription as `data/EngineUpdateOptions:upsert` (one string, both + // schemas plus the engine's unknown-option gate): a capability is never + // half-deleted. See that entry's comment for the full disposition; no D2 + // conversion, since an engine option bag is call-time only. + 'data/DataEngineUpdateOptions:upsert', + // #8057 — the never-implemented upsert flag on `engine.update()`'s option + // surface, tombstoned on BOTH update-options schemas (this one and the + // deprecated `DataEngineUpdateOptions` sibling) with one prescription: + // `ENGINE_UPDATE_UPSERT_REMOVED` in `data/data-engine.zod.ts`, which the + // objectql engine's unknown-option gate quotes too. Declared-but-unenforced + // (ADR-0049): the key sat on the engine's update allowlist while no code path + // read it, so `{ upsert: true }` was accepted and silently dropped. + // + // Registered here but NOT in `src/conversions/registry.ts`, and that asymmetry + // is the point rather than an omission: a D2 conversion rewrites an authored + // source or a stored `sys_metadata` row, and an engine option bag is call-time + // only — nobody authors one and nothing persists one. The prescription reaches + // consumers as the D3 semantic entry `engine-update-upsert-retired` plus this + // tombstone (the `BatchOptions.validateOnly` / `ListNotificationsRequest:cursor` + // disposition). + 'data/EngineUpdateOptions:upsert', // #7990 — sibling of `data/PostgresConfig:password`, same ruling, same // disposition: tombstoned inline credential; the secret binder / // `external.credentialsRef` is the mechanism. See that entry for the reasoning From ff8ff521a0de2881045440afd566331551b31769 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 11:28:06 +0000 Subject: [PATCH 2/3] chore: add adr-0087 disposition marker to the #8057 changeset The Check Changeset gate requires the ADR-0087 question answered in writing in the changeset body; the D3 registration itself landed in the previous commit (registered engine-update-upsert-retired). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2 --- .changeset/engine-update-upsert-retired.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/engine-update-upsert-retired.md b/.changeset/engine-update-upsert-retired.md index ae9699c30e..c9db7d9763 100644 --- a/.changeset/engine-update-upsert-retired.md +++ b/.changeset/engine-update-upsert-retired.md @@ -41,3 +41,5 @@ The retirement kit: No runtime behaviour changes for any in-tree caller — zero production call sites passed the flag (measured in #8057); the only references were the spec's own schema tests, now re-pointed to assert the refusal. + + From a34e25a90e5cf6735c6161ede048536672192166 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 12:02:44 +0000 Subject: [PATCH 3/3] chore: regenerate spec artifacts from the merged tree (os-regen-merge relay) Merge commit first, regeneration as its own commit per the sanctioned sequence; api-surface and export-origins regenerated after a fresh spec build, spec-changes/upgrade-guide/docs from the merged registry. Both sides verified present: the #8057 retirement (two [RETIRED] marks, the prescription const, tombstones + engine refusal) and main's #8296/#8369 virtual-formula where-refusal doors. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Euoy6wyfzgiWtgCg4s6JK2 --- packages/spec/api-surface/data.json | 10 ++++++++++ packages/spec/export-origins/data.json | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index d71bc6198a..848f973cce 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -11,6 +11,7 @@ "API_METHOD_ORDER (const)", "API_OPERATION_ORDER (const)", "API_PRIMITIVES (const)", + "AUDIT_FIELD_DEFS (const)", "AUDIT_PROVENANCE_FIELDS (const)", "Address (type)", "AddressSchema (const)", @@ -319,6 +320,7 @@ "ImportFieldMappingParsed (type)", "ImportFieldMappingSchema (const)", "IndexSchema (const)", + "InjectedColumnProvenance (type)", "InjectedSystemColumnPlan (interface)", "InstantValue (type)", "InstantValueSchema (const)", @@ -390,6 +392,8 @@ "NormalizedFilter (type)", "NormalizedFilterSchema (const)", "OBJECT_KEY_GUIDANCE (const)", + "OWNER_FIELD_DEF (const)", + "OWNING_BUSINESS_UNIT_FIELD_DEF (const)", "ObjectAccessConfig (type)", "ObjectAccessConfigParsed (type)", "ObjectAccessConfigSchema (const)", @@ -571,6 +575,7 @@ "TEMPORAL_ROWS (const)", "TEMPORAL_TIME_CASES (const)", "TEMPORAL_TIME_ROWS (const)", + "TENANT_SCOPE_FIELD_DEF (const)", "TITLE_ELIGIBLE (const)", "TITLE_ELIGIBLE_TYPES (const)", "TITLE_INELIGIBLE_TYPES (const)", @@ -644,6 +649,7 @@ "hasDanglingLikeEscape (function)", "hasDynamicTokens (function)", "hookForm (const)", + "injectedSystemColumnDefs (function)", "isAcceptedFilterComparand (function)", "isApiOperationAllowed (function)", "isApiPrimitive (function)", @@ -657,6 +663,7 @@ "isFilterAST (function)", "isGlobalUnique (function)", "isIncoherentAggregate (function)", + "isInjectedColumnDefinition (function)", "isKnownFilterToken (function)", "isLegacyApiMethod (function)", "isMultiValueField (function)", @@ -681,6 +688,7 @@ "parseDateMacroParam (function)", "parseFilterAST (function)", "percentScaleOf (function)", + "platformProvisionsStorage (function)", "provisionPrimary (function)", "readAutonumberCounter (function)", "reduceFilterKeyVerdict (function)", @@ -696,6 +704,7 @@ "resolveDisplayField (function)", "resolveDriverId (function)", "resolveEffectiveApiMethods (function)", + "resolveInjectedColumnProvenance (function)", "resolveInjectedSystemColumns (function)", "resolveRecordDisplayName (function)", "resolveSearchFieldResolution (function)", @@ -704,6 +713,7 @@ "stripLegacyApiMethods (function)", "suggestDefaultValueToken (function)", "suggestFieldTypeForSqlType (function)", + "unprovisionedInjectedColumns (function)", "urlUserinfoPassword (function)", "utcInstantMs (function)", "validateDriverConfig (function)", diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index cdaf306e36..a9c4526341 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -11,6 +11,7 @@ "API_METHOD_ORDER": "src/data/api-derivation.ts#API_METHOD_ORDER (const)", "API_OPERATION_ORDER": "src/data/object.zod.ts#API_OPERATION_ORDER (const)", "API_PRIMITIVES": "src/data/api-derivation.ts#API_PRIMITIVES (const)", + "AUDIT_FIELD_DEFS": "src/data/injected-system-column-provenance.ts#AUDIT_FIELD_DEFS (const)", "AUDIT_PROVENANCE_FIELDS": "src/data/field-group-layout.ts#AUDIT_PROVENANCE_FIELDS (const)", "Address": "src/data/field.zod.ts#Address (type)", "AddressSchema": "src/data/field-value.zod.ts#AddressSchema (const)", @@ -319,6 +320,7 @@ "ImportFieldMappingParsed": "src/data/mapping.zod.ts#ImportFieldMappingParsed (type)", "ImportFieldMappingSchema": "src/data/mapping.zod.ts#ImportFieldMappingSchema (const)", "IndexSchema": "src/data/object.zod.ts#IndexSchema (const)", + "InjectedColumnProvenance": "src/data/injected-system-column-provenance.ts#InjectedColumnProvenance (type)", "InjectedSystemColumnPlan": "src/data/injected-system-columns.ts#InjectedSystemColumnPlan (interface)", "InstantValue": "src/data/field-value.zod.ts#InstantValue (type)", "InstantValueSchema": "src/data/field-value.zod.ts#InstantValueSchema (const)", @@ -390,6 +392,8 @@ "NormalizedFilter": "src/data/filter.zod.ts#NormalizedFilter (type)", "NormalizedFilterSchema": "src/data/filter.zod.ts#NormalizedFilterSchema (const)", "OBJECT_KEY_GUIDANCE": "src/data/authoring-key-lint.ts#OBJECT_KEY_GUIDANCE (const)", + "OWNER_FIELD_DEF": "src/data/injected-system-column-provenance.ts#OWNER_FIELD_DEF (const)", + "OWNING_BUSINESS_UNIT_FIELD_DEF": "src/data/injected-system-column-provenance.ts#OWNING_BUSINESS_UNIT_FIELD_DEF (const)", "ObjectAccessConfig": "src/data/object.zod.ts#ObjectAccessConfig (type)", "ObjectAccessConfigParsed": "src/data/object.zod.ts#ObjectAccessConfigParsed (type)", "ObjectAccessConfigSchema": "src/data/object.zod.ts#ObjectAccessConfigSchema (const)", @@ -571,6 +575,7 @@ "TEMPORAL_ROWS": "src/data/temporal-conformance.ts#TEMPORAL_ROWS (const)", "TEMPORAL_TIME_CASES": "src/data/temporal-conformance.ts#TEMPORAL_TIME_CASES (const)", "TEMPORAL_TIME_ROWS": "src/data/temporal-conformance.ts#TEMPORAL_TIME_ROWS (const)", + "TENANT_SCOPE_FIELD_DEF": "src/data/injected-system-column-provenance.ts#TENANT_SCOPE_FIELD_DEF (const)", "TITLE_ELIGIBLE": "src/data/display-name.ts#TITLE_ELIGIBLE (const)", "TITLE_ELIGIBLE_TYPES": "src/data/display-name.ts#TITLE_ELIGIBLE_TYPES (const)", "TITLE_INELIGIBLE_TYPES": "src/data/display-name.ts#TITLE_INELIGIBLE_TYPES (const)", @@ -644,6 +649,7 @@ "hasDanglingLikeEscape": "src/data/filter.zod.ts#hasDanglingLikeEscape (function)", "hasDynamicTokens": "src/data/autonumber-format.ts#hasDynamicTokens (function)", "hookForm": "src/data/hook.form.ts#hookForm (const)", + "injectedSystemColumnDefs": "src/data/injected-system-column-provenance.ts#injectedSystemColumnDefs (function)", "isAcceptedFilterComparand": "src/data/filter-comparand-type.ts#isAcceptedFilterComparand (function)", "isApiOperationAllowed": "src/data/api-derivation.ts#isApiOperationAllowed (function)", "isApiPrimitive": "src/data/api-derivation.ts#isApiPrimitive (function)", @@ -657,6 +663,7 @@ "isFilterAST": "src/data/filter.zod.ts#isFilterAST (function)", "isGlobalUnique": "src/data/field.zod.ts#isGlobalUnique (function)", "isIncoherentAggregate": "src/data/aggregation-policy.ts#isIncoherentAggregate (function)", + "isInjectedColumnDefinition": "src/data/injected-system-column-provenance.ts#isInjectedColumnDefinition (function)", "isKnownFilterToken": "src/data/context-tokens.zod.ts#isKnownFilterToken (function)", "isLegacyApiMethod": "src/data/api-derivation.ts#isLegacyApiMethod (function)", "isMultiValueField": "src/data/field-value.zod.ts#isMultiValueField (function)", @@ -681,6 +688,7 @@ "parseDateMacroParam": "src/data/date-macros.zod.ts#parseDateMacroParam (function)", "parseFilterAST": "src/data/filter.zod.ts#parseFilterAST (function)", "percentScaleOf": "src/data/percent-scale.ts#percentScaleOf (function)", + "platformProvisionsStorage": "src/data/injected-system-column-provenance.ts#platformProvisionsStorage (function)", "provisionPrimary": "src/data/display-name.ts#provisionPrimary (function)", "readAutonumberCounter": "src/data/autonumber-format.ts#readAutonumberCounter (function)", "reduceFilterKeyVerdict": "src/data/filter-verdict.ts#reduceFilterKeyVerdict (function)", @@ -696,6 +704,7 @@ "resolveDisplayField": "src/data/display-name.ts#resolveDisplayField (function)", "resolveDriverId": "src/data/driver/config-registry.zod.ts#resolveDriverId (function)", "resolveEffectiveApiMethods": "src/data/api-derivation.ts#resolveEffectiveApiMethods (function)", + "resolveInjectedColumnProvenance": "src/data/injected-system-column-provenance.ts#resolveInjectedColumnProvenance (function)", "resolveInjectedSystemColumns": "src/data/injected-system-columns.ts#resolveInjectedSystemColumns (function)", "resolveRecordDisplayName": "src/data/display-name.ts#resolveRecordDisplayName (function)", "resolveSearchFieldResolution": "src/data/search-fields.ts#resolveSearchFieldResolution (function)", @@ -704,6 +713,7 @@ "stripLegacyApiMethods": "src/data/object.zod.ts#stripLegacyApiMethods (function)", "suggestDefaultValueToken": "src/data/default-value-shape.ts#suggestDefaultValueToken (function)", "suggestFieldTypeForSqlType": "src/data/type-compat.ts#suggestFieldTypeForSqlType (function)", + "unprovisionedInjectedColumns": "src/data/injected-system-column-provenance.ts#unprovisionedInjectedColumns (function)", "urlUserinfoPassword": "src/data/driver/common.zod.ts#urlUserinfoPassword (function)", "utcInstantMs": "src/data/calendar-day.ts#utcInstantMs (function)", "validateDriverConfig": "src/data/driver/config-registry.zod.ts#validateDriverConfig (function)",