Repository navigation
Commit a2f77f7
test(drivers): a keyset walk by id visits every organization member once, in the driver's order (#29659)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8RurSHJiXrGwyzL5u3wQjiC)_
Fixes #29398
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:只加测试。五个 driver 包各加一份一致性钉子:按组织成员补全读的同一查询形状(`organization_id` + `id
$gt` 游标、`orderBy id`、`limit`)做 keyset
分页,断言跨页拼起来恰好等于一次不分页的有序读,每个成员恰一次、不混入别的组织。
- 为什么改:卡的第 1 项原本要在解析器里加 JavaScript 的「id 严格递增」校验,已实测证伪(数据库按 collation
排序,ICU en-US 下大小写混排的 id 顺序与码点不同,会让每一页都 503)。席位改判为:解析器不加校验,改由 driver
一致性测试钉住「游标与排序用同一比较」。
- 风险与回滚:不改任何运行时代码,不发布(测试文件不进 `dist`),`skip-changeset`;回滚即 revert。
-
先看:`packages/drivers/driver-sql/src/sql-driver-member-keyset-walk-conformance.test.ts`(按方言单元跑,含
live Postgres/MySQL),再看 `driver-memory` 的同名钉子。
- 两分钟验证:`pnpm --filter @objectstack/driver-sql exec vitest run
src/sql-driver-member-keyset-walk-conformance.test.ts`,预期 sqlite 单元 4
个通过、未配置的 live 单元为具名跳过。
- 需要你做的:无,席位落地。
## What changes
Test files only, one per driver package. Each walks a
`sys_member`-shaped table with the exact query shape of
`readOrganizationMembersToCompletion` in `@objectstack/core`: `where {
organization_id }` then `{ organization_id, id: { $gt: after } }`,
`orderBy id asc`, a `limit`, `fields: ['id', 'user_id']`, resuming after
each full page's last id. For page sizes 1, 3, 4 (an exact multiple of
the 16 rows) and 16, it asserts:
- the pages joined are the same sequence as one unpaged `orderBy id`
read, so the order asserted is the driver's own, never JavaScript's;
- every member id appears once, and no row of the other organization
appears;
- each row carries its `user_id` through the projection;
- the number of reads is the keyset's (`floor(16 / pageSize) + 1`), and
a walk that does not end fails instead of hanging.
The fixture ids are mixed-case nanoid-alphabet values (`AC Ab9 B-2 a-c
aB3 ab b_1 0Zz Z0 z_9 _x -y Mm mN1 n Q-q`), no two equal after case
folding, plus three ids of another organization.
| Package | File | Runs |
|:--|:--|:--|
| `driver-sql` | `sql-driver-member-keyset-walk-conformance.test.ts` |
every `DIALECT_CELLS` cell: sqlite always; live Postgres and MySQL when
`OS_TEST_POSTGRES_URL` / `OS_TEST_MYSQL_URL` are set (CI: `Temporal
Conformance (live PG + MySQL)`, which runs the whole package) |
| `driver-memory` | `memory-member-keyset-walk-conformance.test.ts` |
always |
| `driver-sqlite-wasm` |
`sqlite-wasm-member-keyset-walk-conformance.test.ts` | always |
| `driver-turso` | `turso-member-keyset-walk-conformance.test.ts` |
always (local mode) |
| `driver-mongodb` | `mongodb-member-keyset-walk-conformance.test.ts` |
opt-in, like the package's other live suites
(`OS_TEST_MONGODB_MEMORY_SERVER_ENABLED=1`); a named skip otherwise |
No changeset: no published file changes. Every driver package publishes
only `dist`, `README.md` and `CHANGELOG.md`; after a build,
`member-keyset` and `org_walked` have zero hits under the drivers'
`dist`, and the control (`ORDER` in `driver-sql/dist/index.js`) hits.
The PR carries `skip-changeset`.
## Why
Item 2 of the card landed as #29545. Item 1 asked for a per-page "each
id strictly greater than the last" check in the resolver. That check
compares in JavaScript code-point order, but `driver-sql` orders and
pages by the database collation, so on an ICU `en-US` PostgreSQL
database it would refuse every correctly ordered page (measured in the
report on the card). The seat ruled: no order check in
`resolve-authz-context.ts`; instead, a driver conformance pin that the
keyset walk returns every row once, in the driver's order, across page
boundaries. That is this PR. `resolve-authz-context.ts` and every driver
are untouched.
## Tests
<details><summary>Per driver (head 1647e32)</summary>
- `driver-sql`, sqlite cell: Tests 4 passed, 2 skipped (the
unprovisioned live Postgres and MySQL cells, named).
- `driver-sql` against a local PostgreSQL 16, two databases: `C.UTF-8`
and ICU `en-US` (`CREATE DATABASE ... LOCALE_PROVIDER icu ICU_LOCALE
'en-US'`). Both: Tests 8 passed, 1 skipped (MySQL). The two databases
order the fixture differently, so both a code-point order and a
non-code-point order were walked:
- `C.UTF-8` (same as JavaScript): `-y 0Zz AC Ab9 B-2 Mm Q-q Z0 _x a-c
aB3 ab b_1 mN1 n z_9`
- ICU `en-US`: `_x -y 0Zz a-c ab aB3 Ab9 AC b_1 B-2 Mm mN1 n Q-q z_9 Z0`
- `driver-memory`: Tests 4 passed. `driver-sqlite-wasm`: Tests 4 passed.
`driver-turso`: Tests 4 passed.
- `driver-mongodb`: Tests 4 skipped. NOT MEASURED: no mongod here; the
binary download host is refused by this container's proxy (403). No CI
job sets `OS_TEST_MONGODB_MEMORY_SERVER_ENABLED`, so CI does not run it
either, as for the package's existing pagination suite.
- MySQL: NOT MEASURED locally (no server here); CI's live job runs it
after merge.
- `pnpm typecheck` for the five driver packages: exit 0 (head 3784be1;
the later commit only merges `main`). The four `src` test files are in
their package's `tsc` file list; the mongodb one is compiled by its
`check:test-typecheck` step (OK).
</details>
<details><summary>The pin can fail (one-time mutation, no file
kept)</summary>
A scratch copy of the `driver-sql` pin wrapped `find` so that every
paged read returned its rows reversed (a driver whose pages are not in
its own order). Run on sqlite and on the ICU `en-US` database, then
deleted (`git status` clean afterwards):
- sqlite: Tests 3 failed, 1 passed. Pages of 3, 4 and 16 are red
(`expected [ 'AC', '0Zz', '-y', 'Ab9', …(40) ] to deeply equal [ '-y',
'0Zz', 'AC', 'Ab9', …(12) ]`); pages of 1 stay green, since a one-row
page has no order to lose.
- ICU `en-US` Postgres: Tests 6 failed, 2 passed, the same split in both
cells.
A second scratch copy dropped `orderBy` from every paged read instead.
It stayed green on both, because `driver-sql` adds an `id` order to a
limited read that names none. That is a reading about the driver, not a
gap in the pin.
</details>
## Verification
<details><summary>Gates (head 1647e32)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands` derived 49 commands.
All 49 were run locally with exit codes captured before any pipe, and
all exited 0. `--ran` reconciled them: 49 derived, 49 run, 0
NOT-MEASURED, 0 UNRUN.
- The five artifact-roster families whose roster sits under a changed
directory, run as well: `check:authz-resolver`,
`check:error-code-casing`, `check:filter-alias-parity`,
`check:object-def-param-keys`, `check:tenant-chokepoint`. All exited 0.
- `pnpm lint` exited 0.
- A first pass on an earlier head turned up one real red:
`check:query-options-erasure` counted the test surface growing 236 to
246, because each pin cast its `find` options `as any`. The casts were
removed (the options type-check as written), and the gate now reads 236,
at the ceiling.
- `check-plugin-teardown-shape --self-test` needed `git fetch
--unshallow` (its fixture is pinned to an old commit).
- Not run here: `Temporal Conformance (live PG + MySQL)` skips pull
requests and the merge queue, so the live Postgres and MySQL cells of
the `driver-sql` pin first run in `main`'s push battery after merge. The
local Postgres readings above stand in for the Postgres half.
</details>
## Acceptance notes
- The drivers' shared conformance cases live in `@objectstack/spec/data`
(`PAGINATION_CASES` and its siblings). This change does not edit
`packages/spec`, so each driver package carries its own copy of the
fixture and the walk. Hoisting them into `@objectstack/spec/data` beside
the pagination cases is a later choice, not a gap.
- The pin follows the resolver's query shape by copy. If
`readOrganizationMembersToCompletion` changes its shape, these files do
not change with it.
- MySQL's `utf8mb4_0900_ai_ci` is case-insensitive. Two ids equal after
case folding would collide on a case-insensitive primary key at insert,
so the fixture holds none. That is a different fact from the one pinned
here, and it was not measured.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema 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. (Nothing
publishes: `skip-changeset`.)
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [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.
Card thread session: `session_01JQwxbZ5Pwrph2FpoA9Ucqq`, seat
`domain:server#4`, claim 6590972532.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbZ5Pwrph2FpoA9Ucqq
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbZ5Pwrph2FpoA9Ucqq)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a6a369e commit a2f77f7
5 files changed
Lines changed: 564 additions & 0 deletions
File tree
- packages/drivers
- driver-memory/src
- driver-mongodb/src
- driver-sqlite-wasm/src
- driver-sql/src
- driver-turso/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
0 commit comments