You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 836d408
Browse filesBrowse the repository at this point in the historyBrowse files
fix(driver-sql): an Oracle RAW(16) uuid column is filtered and written by the uuid it reads as (#31642)
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.
<details><summary>Ablations (every negative pin), run with
scripts/ablation-replace.mjs, each restored to blob == HEAD</summary>
| 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.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not applicable.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmQ9H84XNocJ8waPS1P)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(driver-sql): an Oracle `RAW(16)` uuid column (a `SYS_GUID()` key, or a lookup over one) is now filtered and written by the uuid text it reads back as
6
+
7
+
Clause-②: yes (widening)
8
+
Metadata-protocol: none
9
+
10
+
The driver already read a `RAW(16)` column as a lowercase hyphenated uuid, but it bound that same text back as a string, which Oracle refuses against `RAW`. So a lookup keyed by a `SYS_GUID()` column displayed correctly and could not be filtered, written, updated or deleted by its value.
11
+
12
+
The driver now reads, once per federated object, which of its remote columns are `RAW(16)` (`ALL_TAB_COLUMNS`), and binds a comparand, a written value or a by-id key for those columns as `HEXTORAW(?)` over the uuid's 32 hex digits. The column type decides, never the value: a `VARCHAR2` column holding uuids is bound as text, as before. Uuid text is accepted dashed (the read form) or as the 32 digits undashed, in either case. Anything else is refused before the statement is sent: a filter comparand with `INVALID_FILTER` / 400, a written value with `VALIDATION_ERROR` / 400. A failed catalog read changes nothing: values are bound as before and the next read asks again. Other dialects are unchanged.
0 commit comments