Skip to content

fix(driver-sql): an Oracle RAW(16) uuid column is filtered and written by the uuid it reads as - #31642

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-py8ur5
Nov 8, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-py8ur5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #31390

Card items 1, 2 and 4 are in this pull request. Item 3, the type-compat pairing for an Oracle RAW(16) column, is carried by #31592 in the spec lane, which waits on this one; nothing under packages/spec is touched here.

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:Oracle 联邦表里 RAW(16) 的 uuid 列(SYS_GUID() 主键及指向它的 lookup),过滤比较值、写入值和按 id 定位的键现在经 HEXTORAW(?) 绑定;读侧不变,仍读成带横线的小写 uuid。
  • 为什么改:读出来是 uuid 文本,写回去也是文本,Oracle 拒绝文本对 RAW,所以这类 lookup 只能看不能筛、不能写、不能按 id 改删。
  • 风险与回滚:列类型取自远端目录(每个对象只读一次 ALL_TAB_COLUMNS),读失败就照旧按文本绑定;非 uuid 文本在发库前被拒(过滤 INVALID_FILTER,写入 VALIDATION_ERROR)。其它方言编译输出逐字节不变,有 pin。回滚即 revert 本 PR。
  • 先看:sql-driver-filter-helpers.ts(比较值绑定与三处编译拒绝点)、sql-driver-sqlite-serialisation.ts(formatInput 写入分支,所有方言共用)、sql-driver-field-mapping.ts(目录读取与缓存)。
  • 两分钟验证:pnpm --filter @objectstack/driver-sql exec vitest run src/sql-driver-oracle-dialect.test.ts,预期 71 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  • Which columns. On the Oracle dialect, the first door that touches a federated object reads which of its remote columns are RAW(16), once per object, from ALL_TAB_COLUMNS. The owner is the declared remote schema, then the OWNER. prefix of the remote name, then CURRENT_SCHEMA. The reading is cached per object and dropped when the object is registered again. The column type decides; the value is never the evidence, so a VARCHAR2 column that holds uuids still binds text.
  • Comparands. A comparison comparand (=, $eq, $ne, $in, $nin, $gt, $gte, $lt, $lte, $between) against such a column binds as HEXTORAW(?) over the uuid's 32 hex digits, upper case. Pattern operators ($contains, $like and the rest) compile exactly as before, because their comparand is a text pattern and is never bound to the column.
  • Writes and keys. A written value for such a column (create, update, bulk) and the by-id key of update, delete, bulkDelete and the post-insert read-back bind the same way. The read-back matches the inserted rows by the read form of the id, so an id written upper case or undashed is found again.
  • Accepted text. Uuid text in the dashed read form, or the same 32 digits undashed, in either case.
  • Refusals, before anything is sent. A filter comparand that is not uuid text is refused with the existing INVALID_FILTER / 400, through the existing withheld-filter seam, so the field is withheld unless the subtree is author-marked. A written value that is not uuid text is refused with the existing VALIDATION_ERROR / 400.
  • Failure mode. A catalog read that throws records nothing: every value binds text as today, and the next door asks again. An empty answer (no RAW(16) column) is recorded and binds text.
  • Facade. The priming runs at the top of each door's facet body (find, create, update, delete, bulkCreate, bulkDelete, updateMany, deleteMany, count, aggregate, distinct, findWithWindowFunctions, analyzeQuery). The catalog read lives in FieldMappingFacet, the binding in FilterHelpersFacet, and SqlDriver keeps one-line forwards only.
  • Other dialects. Every new branch is gated on the Oracle client. Their compiled SQL and bindings are byte-identical, pinned per shared helper (see Tests).

Error codes reused, none new: INVALID_FILTER (filter comparands, through withheldFilterError), VALIDATION_ERROR (written values).

Why

The driver already reads a RAW(16) column as a dashed lowercase uuid (RAWTOHEX plus dashes in the fetch type handler), but bound the same text back as a string, which Oracle refuses against RAW. A lookup keyed by a SYS_GUID() column therefore displayed correctly and could not be filtered, written, updated or deleted by its value. Step 0 on main (6b83ec2d, Oracle client compile) showed every uuid comparand and written value bound as dashed text, malformed values sent as they were, and no HEXTORAW anywhere. The declared field type cannot tell a RAW(16) column from a text one, so the column type has to come from the remote catalog.

Tests

  • sql-driver-oracle-dialect.test.ts, 30 new tests (71 in the file, 41 on main):
    • Comparand binding: eq, $eq on the key, $in with mixed case, undashed 32-hex, $ne null-safe, null as IS NULL.
    • Controls: a VARCHAR2 uuid column binds text (seat pin b), $contains is not judged, a uuid in a text column stays text.
    • INVALID_FILTER with nothing sent: equality, 8 hex digits, $in with one bad member, $ne with a number, 31 hex digits, and through the find door.
    • Writes: insert compile, a null write, create VALIDATION_ERROR with nothing sent, the update, delete and bulkDelete door SQL, and the create read-back for upper-case and undashed ids.
    • Catalog: read once per object (seat pin d); a failed read records nothing and the next door asks again, and an empty answer binds text and is not re-asked (seat pin a); re-registration forgets the reading.
    • Every other dialect (seat pin c): pg, mysql2 and mssql compile pins over coerceFilterValue and formatInput with a poisoned column map, and a sqlite door-level pin capturing the exact statements and bindings of find, create, update, delete, bulkDelete and the read-back.
  • sql-driver-oracle-federation-conformance.test.ts: a live Oracle cell, "RAW(16) uuid keys: filtered, written and looked up by the uuid they read as", covering the SYS_GUID read form, create with an upper-case id, eq and $in, the key, the VARCHAR2 control, a lookup round trip, update and delete by key, INVALID_FILTER and VALIDATION_ERROR. It runs only with OS_TEST_ORACLE_URL.
  • Forced test change: sql-driver-compile-refusal-seam.test.ts gains one DOORS row for assertOracleUuidComparand. That file enumerates every builder that raises through the withheld-filter seam and is red until each has a row; the row proves the policy, author, unmarked and merged-$and provenance cases for the new refusal.

Acceptance notes

  • Live Oracle: NOT MEASURED. OS_TEST_ORACLE_URL is not set in this environment and no CI workflow provides it. The live cell skips through the existing testkit.
  • A possible narrowing, required by the card. A comparand or written value of valid hex but another length, or other non-uuid text, reached Oracle before and is now refused before it is sent. Whether Oracle answered any such value through implicit conversion is NOT MEASURED. Undashed 32-hex uuid text is accepted, to keep that form working.
  • create without an id. create() mints a nanoid when no id is given. On an object whose key is a RAW(16) column this is now VALIDATION_ERROR / 400 instead of an Oracle error. A database-default id (DEFAULT SYS_GUID()) is not used by this pull request.
  • textMatchPredicate is not edited; the bind lives in the comparand and write path.

Verification

Head a535d018, merged with origin/main at 133f75aec0. The suites and gates below ran on aed364b6; the one commit after it (a535d018) types the query bags in the new test file, and the oracle dialect file, eslint and the query-options erasure gate were re-run on it.

  • driver-sql: build exit 0, typecheck exit 0, eslint --no-inline-config packages/drivers/driver-sql exit 0.
  • driver-sql full test: 245 files passed, 13 skipped; 4147 tests passed, 222 skipped; verify-lock VERDICT command-exit 0.
  • objectql: 570 files, 11571 tests passed; typecheck exit 0; verify-lock VERDICT command-exit 0.
  • dogfood (closure built under the lock, then pnpm --filter @objectstack/dogfood test): 440 files passed, 3 skipped; 3633 tests passed, 41 skipped; VERDICT command-exit 0.
  • Driver conformance ledger: before (main) 66 covered, 6 DEBT; after 66 covered, 6 DEBT, 0 exempt.
  • Gates: all 71 families dispatch-gates derives for this diff ran and exit 0; --ran reconciliation: 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN. Two first runs exited 3 on a missing prerequisite (the teardown fixture commit absent from the shallow clone, and type-check-debt not getting the verify lock while dogfood held it) and passed on re-run. check:query-options-erasure first went red on 9 new as any query bags in the new test; they are typed in a535d018 and the test surface is back at its 236 ceiling.
Ablations (every negative pin), run with scripts/ablation-replace.mjs, each restored to blob == HEAD
Mutation Red
B1: the comparand refusal returns early 10: the five INVALID_FILTER cases, the find door, and the four seam provenance rows
B2: the write refusal binds the value instead of throwing 1: create VALIDATION_ERROR with nothing sent
C1: the three filter-site Oracle guards and the binding's Oracle guard removed 4: the pg, mysql2, mssql and sqlite other-dialect pins
C2: the formatInput Oracle guard and the binding's Oracle guard removed 4: the same four pins
C3: the binding's Oracle guard alone removed 1: the sqlite door pin
C4: the column decided by the value instead of the catalog 6, among them both controls (VARCHAR2 column, uuid in a text column)
C5: a failed catalog read recorded as empty 1: the failed-read pin
D1: the once-per-object cache check removed 2: once per object, and the empty answer not re-asked
E1: re-registration keeps the stale reading 1: the re-registration pin

The other-dialect pins were also run against the base source: they pass on main, so they pin unchanged behaviour.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not applicable.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…gh HEXTORAW

The catalog says which federated columns are RAW(16); those fields bind
uuid text as HEXTORAW(?) over its hex digits, as comparands, written
values and by-id keys. Text that is not a uuid is refused before the
statement is built. Every other column and dialect binds as before.

Claude-Session: https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P
Co-authored-by: Claude <noreply@anthropic.com>
…ects' unchanged statements

Claude-Session: https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P
Co-authored-by: Claude <noreply@anthropic.com>
… form, and pin the Oracle live cell

Claude-Session: https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P
Co-authored-by: Claude <noreply@anthropic.com>
…ites, through the seam with its subtree

The refusal carries the filter subtree it was raised from, so the
provenance mark decides what the wire names, as for every other
compile refusal. Pattern operators on the column are not judged.

Claude-Session: https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P
Co-authored-by: Claude <noreply@anthropic.com>
…he oracle uuid bind

The RAW(16) catalog priming now runs at the top of each door facet body
(find, create, update, delete, bulk, aggregate, distinct, window functions,
analyze), the catalog read and the uuid binding live in FieldMappingFacet and
FilterHelpersFacet, and registerExternalObject forgets a stale reading in its
schema-sync body. SqlDriver keeps one-line forwards only.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P
The find and count calls pass a plain where bag that the driver's query type
already accepts, so the any-casts only grew the query-options erasure test
surface.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql, touching 140 documentable anchor(s).

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 06060c61c572f833f19497a6e84838c52a923166.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 107 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 06060c61c572f833f19497a6e84838c52a923166 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2bd010e8db3def7961ff5bb55f14a00e29061414 — the merge of head ee1c0183fa49234f4617984d219614a811b05878 into base 06060c61c572f833f19497a6e84838c52a923166, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2bd010e8db3def7961ff5bb55f14a00e29061414 && git checkout 2bd010e8db3def7961ff5bb55f14a00e29061414
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 06060c61c572f833f19497a6e84838c52a923166 ee1c0183fa49234f4617984d219614a811b05878 && git checkout -B drift-repro 06060c61c572f833f19497a6e84838c52a923166 && git merge --no-ff ee1c0183fa49234f4617984d219614a811b05878

node scripts/docs-audit/affected-docs.mjs --json 06060c61c572f833f19497a6e84838c52a923166

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 06060c61c572f833f19497a6e84838c52a923166 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

Contract-review-claim: session_01ADHkmP8JJo4zF2sjTBRwnX · head a535d01 · 2026-11-08T18:17Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a535d01808057b7c921d5b50a4b0567aa7ad391a
Local-runs: none
Scope: packages/drivers/driver-sql/src/sql-driver-oracle-dialect.ts, packages/drivers/driver-sql/src/sql-driver-field-mapping.ts, packages/drivers/driver-sql/src/sql-driver-filter-helpers.ts, packages/drivers/driver-sql/src/sql-driver-sqlite-serialisation.ts, packages/drivers/driver-sql/src/sql-driver-crud.ts, packages/drivers/driver-sql/src/sql-driver-sequence-semantics.ts, packages/drivers/driver-sql/src/sql-driver-bulk-operations.ts, packages/drivers/driver-sql/src/sql-driver-aggregation.ts, packages/drivers/driver-sql/src/sql-driver-distinct.ts, packages/drivers/driver-sql/src/sql-driver-window-functions.ts, packages/drivers/driver-sql/src/sql-driver-query-plan.ts, packages/drivers/driver-sql/src/sql-driver-schema-sync.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/sql-driver-oracle-dialect.test.ts, packages/drivers/driver-sql/src/sql-driver-oracle-federation-conformance.test.ts, packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts + packages/drivers/driver-sql/src/sql-driver-top-level.ts (withheldFilterError, the VALIDATION_ERROR write-refusal family), packages/drivers/driver-sql/src/sql-driver-oracle-introspection.ts (rowsOf, the measured knex.raw row shape on oracledb), packages/drivers/driver-sql/src/sql-driver-internal-helpers.ts (readableIdsSubquery), packages/drivers/driver-sql/src/sql-driver-predicate-emitters.ts (applyNormalizedComparison), packages/spec/src/data/type-compat.ts on main (the oracle / uuid demoteExact rule the narrowing is judged against)

Face: reviewFace returns no hits on this file list (no published schema, no governed path); the PR declares Clause-②: yes (widening), and the dev's own open question says some inputs that reached Oracle on main are now refused before they are sent. Reviewed on the narrowing face at the seat's request: every non-prose path of the diff is in scope, the cited code is read, never reviewed. The .changeset file is prose and is judged in ② only. Inputs: card #31390 (body and all four comments, the os-dev-report 6725220789 included), PR #31642 (body, file list, net diff against main), and the check-runs on the head. The tree was judged, not the PR body's account of it.

Check-runs on a535d01808057b7c921d5b50a4b0567aa7ad391a, read 2026-11-08T18:26Z and again at 18:31Z before posting: every completed run is success (Build Core, Lint & Repo Gates and its seven scoped lint jobs, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Check Changeset, Check PR Size, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate, Test UI, Live E2E UI, Auto Label, Flag docs, Plan Test Core shards, Test Core (4/8), filter); the UI, docs, temporal-conformance and opt-in jobs are skipped by path filter. Test Core (1/8, 2/8, 3/8, 5/8, 6/8, 7/8, 8/8) and Type Check · workspace were still in_progress at the second read; their conclusions are the gate verdicts for those families and this record does not stand in for them.

① Derived judgments

The surface is @objectstack/driver-sql on the Oracle dialect only. index.ts re-exports nothing from sql-driver-oracle-dialect.ts, so the package's public surface gains only three protected members on SqlDriver (primeOracleRawUuidColumns, readOracleRawUuidColumns, oracleUuidBinding) and one protected map — additive, subclass-visible, patch-level. Every other dialect's compiled SQL and bindings are byte-identical: each priming call, oracleUuidBinding, the coerceFilterValue arm, the three refusal sites and the formatInput arm are gated on isOracle; readBackInsertedRows collapses to main's String(row.id) keying exactly when the binding returns the id itself (strict equality), which is every non-Oracle call. Right, and pinned with a poisoned registry on pg, mysql2, mssql and sqlite.

The accept set on an Oracle column the catalog reports as RAW(16), input class by input class (comparand under =, $eq, $ne, $in, $nin, $gt, $gte, $lt, $lte, $between; a written value; a by-id key), main against the head:

  • Dashed uuid text, either case (the form the column reads back as): main bound it as a VARCHAR2 and Oracle's implicit character-to-RAW conversion refuses the hyphens (ORA-01465) — the request FAILED. Head binds HEXTORAW(?) over the 32 upper-case digits and it works. This is the card. Widening, right.
  • The 32 digits undashed, either case (the RAWTOHEX form): main's implicit conversion accepted it, so a filter matched and a write stored the right bytes — SUCCEEDED. Head accepts it, binds the same 16 bytes, and the create read-back maps an upper-case or undashed written id to the read form. No request lost. Right.
  • Even-length hex of another length ('0f8fad5b'): main converted it to a shorter RAW, so a comparand compared false and answered zero rows, and a written value landed in the RAW(16) column as a 4-byte value the read side cannot present as the field (oracleRaw16AsUuid leaves a non-16-byte RAW as a Buffer). Both SUCCEEDED on main; both are now refused before anything is sent (INVALID_FILTER / VALIDATION_ERROR, 400). This is the one input class where a request that completed on main now fails. It never named a row by the uuid the column reads as, and the stored case put a value in a uuid field that the field's own type cannot carry. Card item 2 orders exactly this refusal. Judged against the published spec on main: type-compat.ts holds the oracle / uuid pairing at 'lossy' through demoteExact, whose JSDoc says the field "displays correctly and cannot yet be filtered or written by its value" — no declared capability covered these inputs, so the refusal makes the field type's own refusal real rather than taking a declared capability away. Not a Clause-② narrowing.
  • An empty string written to such a field: Oracle stores a bound '' as NULL, so on main { account: '' } cleared the lookup — SUCCEEDED. Head refuses it with VALIDATION_ERROR; null still clears it (the v === null arm is kept). Same judgment as the class above: '' is not an id of any row and the driver already refuses '' as an Oracle comparand on these operators (assertOracleComparandNotEmptyString) prescribing $null. Recorded as finding 1 (nit) so the loss is on the record; it does not change the declaration.
  • Non-hex text ('not-a-uuid', 'acct-17', a nanoid), a number: ORA-01465 on main — FAILED. Head answers a typed 400 with the accepted forms named. Not a narrowing; a better error. Odd-length hex: NOT MEASURED on either side; it cannot name a row by the uuid the column reads as, so the same judgment holds.
  • null as comparand or written value: IS NULL and a null bind, unchanged. Right.
  • Pattern operators ($contains, $like, and the rest) on the same column: compiled from the text pattern exactly as on main, unjudged — their comparand is never bound to the column as a value. Right, and the set they are excluded by (ORACLE_EMPTY_STRING_SENSITIVE_OPERATORS) is exactly the set of operators whose coerced comparand reaches the column as a bind in applyFilterCondition's switch, checked arm by arm.
  • A VARCHAR2 column holding uuids, a uuid-shaped value in a text column: bound as text, unchanged, because the catalog decides and the value never does. Right, pinned (the two controls).
  • A failed catalog read: nothing recorded, every value binds text as on main, the next door asks again. An empty answer is recorded and binds text. Right; see finding 2.
  • Reads: the fetch type handler and oracleRaw16AsUuid are untouched; a RAW(16) still reads as lowercase hyphenated uuid text. Right.

Clause-②: yes (widening) on the PR body and on the changeset is the right line: the only requests that stop completing on main are ones the published pairing never promised and the field type refuses by definition; the request the card exists for could not complete on main at all.

② Semver level

.changeset/31390-oracle-raw16-uuid-hextoraw.md: '@objectstack/driver-sql': patch, with Clause-②: yes (widening) and Metadata-protocol: none line-initial, matching the PR body's two lines. Metadata-protocol: none is right: nothing an author can write changes — packages/spec is untouched (the claim forbade a type-compat value change; that is #31592), no key, accepted value or default moves. A none kind takes patch, and a bug fix in a released package takes patch; the diff publishes from no other released package. The public-surface delta (①, protected members only) is additive. Right.

③ Boundary flags

  1. open_questions 1 — a comparand or written value that is not uuid text reached Oracle on main and is now refused: option A (keep the refusal). Answered in ①: the two input classes that completed on main (other-length even hex; '' on write) never named a row by the uuid the column reads as, the pairing was published 'lossy' with no filter/write promise, and the card orders the refusal.
  2. open_questions 2 — create() without an id mints a nanoid, now VALIDATION_ERROR / 400 on a RAW(16)-keyed object instead of an Oracle error: not a narrowing (a nanoid never converted on main; ORA-01465 both before and after, now typed). Option A is right and is the seat's: a database-default id (DEFAULT SYS_GUID(), reading the key back) is new behaviour outside the card's three items and is filed as its own card by the seat if wanted; the acceptance note already records it.
  3. deviation — no second merge of origin/main before the PR: read live. git log 133f75ae..origin/main -- packages/drivers/driver-sql is empty at origin/main 8784ccbc, so for every path in scope the head's tree is main plus this diff. driver-sql (mssql): substring operators on a uniqueidentifier column compare SQL Server's upper-case text form while reads answer lower-case, so $contains / $startsWith with a lower-case GUID fragment misses #31395 has not landed (only feat(driver-sql): SQL Server as a federation-only dialect #31381 did, before the merge base); the claim's "merge again after driver-sql (mssql): substring operators on a uniqueidentifier column compare SQL Server's upper-case text form while reads answer lower-case, so $contains / $startsWith with a lower-case GUID fragment misses #31395 lands, the later lander resolves" stays owed to the seat, not to this review.
  4. Live Oracle NOT MEASURED: the live cell is written (card item 4's three pins — filter eq/$in, write, lookup round-trip — plus key, control, update/delete by key and both refusals) and skips without OS_TEST_ORACLE_URL, which no workflow sets. What the live cell would measure rests on two things read here: Oracle's documented implicit character-to-RAW conversion and HEXTORAW, and the knex.raw row shape on oracledb, which oracleRawUuidColumns reads exactly as rowsOf in sql-driver-oracle-introspection.ts does (Array.isArray(result)), the reading driver-sql: Oracle as a federation dialect — an external datasource over Oracle Database reads (and, double opt-in, writes) live, with type-compat and the predicate emitters modelling oracle #31328 measured on 23ai. Stays a measurement debt on the card, not a finding on the tree.
  5. Forced test change — one DOORS row for assertOracleUuidComparand in sql-driver-compile-refusal-seam.test.ts: right; that file enumerates every builder that raises through the withheld-filter seam and is red until each has a row.
  6. textMatchPredicate untouched, packages/spec untouched: right (patterns are never bound; item 3 is spec(type-compat): lift the oracle RAW(16) → lookup / master_detail pairing from lossy once the driver binds uuid through HEXTORAW #31592).
  7. Check-runs still in_progress at posting (18:31Z): Test Core (1/8, 2/8, 3/8, 5/8, 6/8, 7/8, 8/8) and Type Check · workspace. Their conclusions are read off the head by the seat before any landing act; this record does not stand in for them.

④ Design judgment

Contract read sentence by sentence against the tree (card items 1, 2, 4; the seat's claim's file surface).

"A comparand or written value for a RAW(16) column typed as uuid binds through HEXTORAW(?) with dashes stripped." sql-driver-oracle-dialect.ts oracleUuidRawBinding: knex.raw('HEXTORAW(?)', [hex stripped, upper-cased]); UUID_TEXT accepts 8-4-4-4-12 or 32 hex digits, either case. Comparands: sql-driver-filter-helpers.ts coerceFilterValue routes a uuid-text comparand on a catalogued field to the binding ahead of the temporal arm, maps arrays member by member, and stays total (a non-uuid value is returned as it is, so the $ne emitter's reliance on a total coercion holds). Written values: sql-driver-sqlite-serialisation.ts formatInput's Oracle arm, keyed by logical field before applyWriteColumnMap, copy-on-write (the #24835 shape: the caller's data is never mutated, so the knex Raw never reaches a DataEvent). By-id keys: update binds the key once and uses it for the statement and the read-back; delete, bulkDelete (each id) and readBackInsertedRows (each written id, matched back by the read form) bind the same way; bulkUpdate delegates to update per item. Agrees.

"The read side stays RAWTOHEX dashed." No hunk touches the fetch type handler or oracleRaw16AsUuid; oracleUuidReadForm reproduces its exact form for the read-back match. Agrees.

"The column type must be known at bind time; read it from the introspected/declared column type, never guess it from the value." sql-driver-field-mapping.ts primeOracleRawUuidColumns reads ALL_TAB_COLUMNS (DATA_TYPE = 'RAW' AND DATA_LENGTH = 16) for the object's remote table, owner resolved remoteSchema, then the OWNER. prefix of the remote name, then CURRENT_SCHEMA — the same resolution the statements get (physicalTableByObject is the builder's table name, physicalSchemaByObject its schema). Columns map back to fields through columnFieldByObject, falling back to the column name. The value is consulted nowhere for the decision: the two controls (a VARCHAR2 uuid column, a uuid in a text column) pin that, and ablation C4 shows them red when the value decides. The priming runs at the top of all 13 door bodies that compile a WHERE or a write for an object (find/findOne through findRows, create, update, delete, bulkCreate, bulkDelete, updateMany, deleteMany, count, aggregate, distinct, findWithWindowFunctions, analyzeQuery — explain goes through analyzeQuery; upsert is refused by name on Oracle before any statement, so the upsert-family where(idColumn, …) sites it alone reaches are rightly untouched). Agrees for every door's own object; see finding 3 for the one filter that compiles a second object's predicate.

"Malformed uuid text is refused with a typed INVALID_FILTER, not sent." assertOracleUuidComparand at the three sites that bind a comparison comparand (compileFilters bare =; applyFilterCondition operator path, inside the existing if (this.isOracle) block; applyFilterCondition bare =), through withheldFilterError with the refusing subtree, so the field is withheld unless author-marked — the same seam the empty-string refusal uses; a by-key door's id has no subtree and is withheld. INVALID_FILTER / 400 reused, no new code. Written values take VALIDATION_ERROR / 400, the family sql-driver-top-level.ts already refuses an unwritable value with. Nothing is sent before either: the find-door and create-door tests assert an empty statement log, and in create the refusal is raised by formatInput ahead of the INSERT (the autonumber reservation that precedes it does not apply to a federated object). Agrees.

"Catalog read once per object; failure modes." has(object) short-circuits; a non-federated object has no physicalTableByObject entry (its only writer is registerExternalObject) and returns without a read; a thrown read records nothing and the next door asks again (pinned); an empty answer is recorded (pinned); registerExternalObject deletes the recording so a re-pointed object is read again (pinned). Concurrency: two first doors on one object in flight together both read and both set the same value — benign; see finding 4 for the in-flight re-registration edge. Agrees.

Other dialects byte-identical. Judged in ①; the four pins run with the registry deliberately poisoned so only the dialect guard keeps the arm out, and they pass on main's source too. Agrees.

Facade. sql-driver.ts adds one-line forwards and host installs only; the catalog read is split into readOracleRawUuidColumns so a test can stub it without a connection — right, and Lint & Repo Gates (which carries the facade and keyed-text gates) is green on the head.

Tests. sql-driver-oracle-dialect.test.ts: 30 tests over a probe that answers the catalog from a fixture and captures _query — compile pins for every comparison shape the card names, the two controls, five refusal shapes plus the door, insert/null/update/delete/bulkDelete/read-back SQL, the four catalog pins, the four other-dialect pins. sql-driver-oracle-federation-conformance.test.ts: the live cell (③.4). Both do what they claim; the negative pins were ablated one by one in the report.

Findings, graded:

  1. nit — formatInput Oracle arm (sql-driver-sqlite-serialisation.ts): an empty string written to a catalogued RAW(16) field is VALIDATION_ERROR where main stored NULL. Keep: null is the spelling that means no value and is kept, and the comparand side already refuses '' prescribing $null. On the record for ①.
  2. nit — primeOracleRawUuidColumns swallows a failed catalog read with no log line. The next statement then fails visibly at Oracle (ORA-01465 on the text bind), so this is a functional degradation and warn is its level under AGENTS.md's rule; a one-time warn naming the object and that uuid binding fell back to text would let an operator connect the two. Same shape as primeMssqlPagingKey.
  3. nit — readableIdsSubquery (sql-driver-internal-helpers.ts, read) applies a permission scope's filter on a parent object's builder without priming that object; if the parent is an Oracle federated object no door has touched in this driver, a RAW(16) comparand in the scope binds text — main's behaviour, never worse — until the parent's first door primes it. The function is synchronous, so the fix belongs to the doors (prime the objects named by composed-readable-ids leaves in where). Narrow enough to note; the seat files it if wanted.
  4. nit — primeOracleRawUuidColumns is check-then-await-then-set: a registerExternalObject for the same object during the in-flight read is overwritten by the now-stale set when the read resolves. Same shape as primeMssqlPagingKey; a re-check of has after the await, or keying the recording by remote table, closes it.
  5. nit — the predicate this.oracleRawUuidFieldsByObject.get(table ?? '')?.has(field) === true is spelled at four sites in sql-driver-filter-helpers.ts; one named helper would make the three refusal sites and the coercion arm read as one rule.
  6. nit — no Oracle compile pin for $between or the ordered comparisons over a RAW(16) column; knex's parameter() unwraps a Raw for whereBetween and where(col, op, raw) by the same path whereIn uses, which is pinned, so correctness is not in doubt.
  7. nit — a remote name that is a synonym is absent from ALL_TAB_COLUMNS, so the answer is empty, cached, and the field binds text as on main (a view is listed and works). Worth a sentence in oracleRawUuidColumns's docblock.

No must-fix, no should-fix. Nits alone end the round.

Implemented-by: claude/project-thread-py8ur5
Reviewed-by: session_01ADHkmP8JJo4zF2sjTBRwnX

VERDICT: PASS

…-py8ur5

# Conflicts:
#	packages/drivers/driver-sql/src/sql-driver-aggregation.ts
#	packages/drivers/driver-sql/src/sql-driver-bulk-operations.ts
#	packages/drivers/driver-sql/src/sql-driver-crud.ts
#	packages/drivers/driver-sql/src/sql-driver-distinct.ts
#	packages/drivers/driver-sql/src/sql-driver-filter-helpers.ts
#	packages/drivers/driver-sql/src/sql-driver-query-plan.ts
#	packages/drivers/driver-sql/src/sql-driver-window-functions.ts
#	packages/drivers/driver-sql/src/sql-driver.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

Contract-review-claim: session_01ADHkmP8JJo4zF2sjTBRwnX · head ee1c018 · 2026-11-08T21:04Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ee1c0183fa49234f4617984d219614a811b05878
Local-runs: none
Scope: packages/drivers/driver-sql/src/sql-driver-oracle-dialect.ts, packages/drivers/driver-sql/src/sql-driver-field-mapping.ts, packages/drivers/driver-sql/src/sql-driver-filter-helpers.ts, packages/drivers/driver-sql/src/sql-driver-sqlite-serialisation.ts, packages/drivers/driver-sql/src/sql-driver-crud.ts, packages/drivers/driver-sql/src/sql-driver-sequence-semantics.ts, packages/drivers/driver-sql/src/sql-driver-bulk-operations.ts, packages/drivers/driver-sql/src/sql-driver-aggregation.ts, packages/drivers/driver-sql/src/sql-driver-distinct.ts, packages/drivers/driver-sql/src/sql-driver-window-functions.ts, packages/drivers/driver-sql/src/sql-driver-query-plan.ts, packages/drivers/driver-sql/src/sql-driver-schema-sync.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/sql-driver-oracle-dialect.test.ts, packages/drivers/driver-sql/src/sql-driver-oracle-federation-conformance.test.ts, packages/drivers/driver-sql/src/sql-driver-compile-refusal-seam.test.ts + packages/drivers/driver-sql/src/sql-driver-top-level.ts (withheldFilterError, the VALIDATION_ERROR write-refusal family), packages/drivers/driver-sql/src/sql-driver-oracle-introspection.ts (rowsOf, the measured knex.raw row shape on oracledb), packages/drivers/driver-sql/src/sql-driver-internal-helpers.ts (readableIdsSubquery), packages/drivers/driver-sql/src/sql-driver-predicate-emitters.ts (applyNormalizedComparison; applyLike as #31623 left it on main), packages/spec/src/data/type-compat.ts on main (the oracle / uuid demoteExact rule the narrowing is judged against)

Round 2. The head is the merge of origin/main at 06060c61 into the PASS head a535d018 (record 6725408036); the dev has no other commit. The merge resolved content conflicts in eight files inside Scope (aggregation, bulk-operations, crud, distinct, filter-helpers, query-plan, window-functions, sql-driver.ts), so this is a full same-form record on the current head; the round-1 judgments carry forward wherever the tree is unchanged and are restated where the merge touched them. Face as in round 1: reviewFace returns no hits; reviewed on the narrowing face at the seat's request. Inputs: card #31390 (body and comments), PR #31642 (body — Clause-②: yes (widening) unchanged — file list, net diff git diff 06060c61 ee1c0183), the remerge diff (git show --remerge-diff ee1c0183), the check-runs on the head, and the tree at ee1c0183 read directly. The dev's reported counts on this head (driver-sql 4166 passed / 222 skipped, objectql 11616/11616, dogfood 3652 / 41 skipped, 71 gate families exit 0, conformance 66 covered / 6 DEBT, 11 ablations red and restored) are the dev's claims and are not adopted here; the check-runs are.

Check-runs on ee1c0183fa49234f4617984d219614a811b05878, read 2026-11-08T21:05Z: 48 runs, every non-skipped run completed / success — Build Core, Lint & Repo Gates and its seven scoped lint jobs (ESLint, PM dispatch-gates, CI/release/engine gates, docs & code guards, repo & tooling gates, query-options erasure, slot lookup & corpus, tooling self-tests), TypeScript Type Check and its four jobs (source gates, debt ledger, consumer gates, workspace), Test Core and all eight shards, Plan Test Core shards, Check Changeset, Check PR Size, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate, Test UI, Live E2E UI, Auto Label, Flag docs, filter; the UI, docs, temporal-conformance and opt-in jobs skipped by path filter. Nothing is in progress. Those conclusions are the gate verdicts for their families.

① Derived judgments

What the merge changed, judged from the remerge diff and from a line-level comparison of the two net diffs (a535d018 against its main 133f75ae; ee1c0183 against 06060c61): the added and removed lines are identical except for nine Pick unions (aggregate, updateMany, deleteMany, count, findRows, distinct, applyFilterCondition, analyzeQuery, findWithWindowFunctions), each now naming isMssql / primeMssqlGuidColumns (or mssqlGuidColumnsByObject) from main alongside isOracle / primeOracleRawUuidColumns (or oracleRawUuidFieldsByObject) from this PR. No line main brought in #31623 (card #31395) or #31625 was dropped — a dropped line would show as a removal in the new net diff that the old one lacks, and there is none — and no line beyond the unions was added. The .changeset, both declaration lines, and every Oracle hunk in sql-driver-oracle-dialect.ts, sql-driver-field-mapping.ts, sql-driver-sqlite-serialisation.ts, sql-driver-sequence-semantics.ts, sql-driver-schema-sync.ts and the three test files are byte-identical to round 1.

Door tops, read in the tree: each of the nine shared doors now runs if (this.isMssql) await this.primeMssqlGuidColumns(object, where) then if (this.isOracle) await this.primeOracleRawUuidColumns(object), each prime gated on its own dialect, with both provenance comments kept; findRows runs the Oracle prime, then the two SQL Server primes (paging key, GUID columns), as main ordered the latter two. The four Oracle-only doors (create, update, delete, bulkCreate, bulkDelete) are untouched by the merge; main added no SQL Server prime there, rightly, since #31623 primes before a text operator compiles and those doors compile none. The two dialects are mutually exclusive on one driver, so the order of the two primes within a door carries no meaning. Right.

Hosts, read in sql-driver.ts: every facet host that gained a member from either side carries the union (aggregation, bulk-operations, crud, distinct, query-plan, window-functions; filter-helpers carries mssqlGuidColumnsByObject, oracleRawUuidFieldsByObject, oracleUuidBinding and knex); the two registries (oracleRawUuidFieldsByObject, mssqlGuidColumnsByObject) and the two docblocked method groups are separated into their own blocks, the conflict's shared } and /** restored. The Pick lists are exactly what each body reads, and both type-check families are green on this head. Right.

Interaction between the two features: none. primeMssqlGuidColumns reads introspectColumns into mssqlGuidColumnsByObject; primeOracleRawUuidColumns reads ALL_TAB_COLUMNS into oracleRawUuidFieldsByObject; neither reads the other's registry. In applyFilterCondition, #31623's guidColumn is this.isMssql && … computed once per field, and reaches only the pattern-operator arms ($contains, $icontains, $notContains, $startsWith, $endsWith, $like, $ilike) as a trailing applyLike / applyLikePattern argument; this PR's refusal sits inside the if (this.isOracle) block and its coercion in coerceFilterValue, both on the comparison operators only. On Oracle guidColumn is false and the pattern arms compile as on main — the round-1 control ($contains on the RAW(16) column) pins that and is green. On SQL Server the Oracle arms are inert by their guard — the round-1 mssql pin (poisoned Oracle registry) is green. This PR does not touch sql-driver-predicate-emitters.ts, where #31623's applyLike body changed, so nothing of #31623's intent is altered by this diff.

The accept-set judgment of round 1 carries forward unchanged: the only inputs whose requests completed on main and are now refused (even-length hex of another length; '' on a write) never named a row by the uuid the column reads as and were never a declared capability — type-compat.ts on main publishes the pairing 'lossy' with the words "cannot yet be filtered or written by its value" — so Clause-②: yes (widening) on the PR body and on the changeset is right. Other dialects compile byte-identical to main apart from #31623's own change, which is main's and not this diff's.

② Semver level

Unchanged from round 1 and re-read: .changeset/31390-oracle-raw16-uuid-hextoraw.md is byte-identical, '@objectstack/driver-sql': patch, Clause-②: yes (widening), Metadata-protocol: none; packages/spec untouched; the public surface gains three protected SqlDriver members and one protected map, additive. Right.

③ Boundary flags

  1. open_questions 1 (keep the refusal of non-uuid text): A, as in round 1; the merge changes nothing it rests on.
  2. open_questions 2 (create() without an id → VALIDATION_ERROR instead of an Oracle error): A, the seat's to file as its own card; not a narrowing.
  3. The claim's serial constraint — "merge origin/main again after driver-sql (mssql): substring operators on a uniqueidentifier column compare SQL Server's upper-case text form while reads answer lower-case, so $contains / $startsWith with a lower-case GUID fragment misses #31395 lands; the later lander resolves" — is now discharged: driver-sql (mssql): substring operators on a uniqueidentifier column compare SQL Server's upper-case text form while reads answer lower-case, so $contains / $startsWith with a lower-case GUID fragment misses #31395's PR (fix(driver-sql): match SQL Server uniqueidentifier columns as their lower-case text in the text operators #31623) is in 06060c61 and this head is that merge, resolved by this PR's dev as the later lander. git log 06060c61..origin/main -- packages/drivers/driver-sql is empty at the time of reading (origin/main is 06060c61), so the head's tree is main plus this diff for every path in Scope.
  4. Live Oracle NOT MEASURED — unchanged; the live cell is unchanged and skips without OS_TEST_ORACLE_URL. A card debt, not a tree finding.
  5. The forced seam DOORS row, textMatchPredicate untouched, packages/spec untouched: unchanged, right.
  6. fix(driver-sql): match SQL Server uniqueidentifier columns as their lower-case text in the text operators #31623 keeps mssqlGuidColumnsByObject across a registerExternalObject (no delete beside this PR's oracleRawUuidFieldsByObject.delete(key)); that is main's own code, outside this diff — read, not reviewed, and left for the SQL Server lane to judge.

④ Design judgment

The contract is agreed sentence by sentence as in round 1 — HEXTORAW bind over the stripped digits for comparands, written values and by-id keys; the read side untouched; the column type from ALL_TAB_COLUMNS, mapped through columnFieldByObject, never from the value; malformed text refused before any statement with INVALID_FILTER (filters, through the withheld seam) and VALIDATION_ERROR (writes); the catalog read once per object with its three failure modes; other dialects byte-identical; facade kept to one-line forwards — none of those hunks moved. What the merge added to judge is the eight resolutions, read above: each is the union of both sides with nothing dropped, each prime gated on its dialect, each Pick list and host install complete (type-checked green), and the two features share no state and no code path. The one place the two sit in the same function body (applyFilterCondition) keeps them on disjoint operator sets behind disjoint dialect guards.

Findings, graded — the seven round-1 nits re-read on this head; no new finding:

  1. nit — unchanged: '' written to a catalogued RAW(16) field is VALIDATION_ERROR where main stored NULL; null still clears it. Keep; on the record for ①.
  2. nit — unchanged: primeOracleRawUuidColumns swallows a failed catalog read without a warn; primeMssqlGuidColumns on main has the same shape (catch { continue; }).
  3. nit — stands, now with a precedent on main: readableIdsSubquery compiles a permission scope on a second object without priming it (binds text as main did until that object's first door). fix(driver-sql): match SQL Server uniqueidentifier columns as their lower-case text in the text operators #31623's primeMssqlGuidColumns walks the where with collectTextMatchObjects to prime every object the filter names — the same walk would close this gap for the Oracle registry. Still narrow and never worse than main; the seat files it if wanted.
  4. nit — unchanged: check-then-await-then-set in primeOracleRawUuidColumns; a re-registration during the in-flight read is overwritten by the stale set.
  5. nit — stands, sharpened by the merge: the predicate this.oracleRawUuidFieldsByObject.get(table ?? '')?.has(field) === true is spelled at four sites in sql-driver-filter-helpers.ts, while fix(driver-sql): match SQL Server uniqueidentifier columns as their lower-case text in the text operators #31623 computes its guidColumn once per field at the top of the operator loop; the Oracle refusal could take the same shape (one rawUuidColumn constant beside guidColumn).
  6. nit — unchanged: no Oracle compile pin for $between or the ordered comparisons over a RAW(16) column.
  7. nit — unchanged: a synonym remote name is absent from ALL_TAB_COLUMNS, so the answer is empty, cached, and binds text as on main.

No must-fix, no should-fix. Nits alone end the round.

Implemented-by: claude/project-thread-py8ur5
Reviewed-by: session_01ADHkmP8JJo4zF2sjTBRwnX

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-steve · session_01ADHkmP8JJo4zF2sjTBRwnX

ACCEPT · PR #31642 · #31390 · 2026-11-08T21:10Z

Head-sha: ee1c0183fa49234f4617984d219614a811b05878

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant