Uh oh!
There was an error while loading. Please reload this page.
feat(driver-mongodb): index lookup joins off the canonical reference key - #13633
Conversation
…` key `syncCollectionSchema`'s field-level join-index arm gated on `field.reference_to` — a rejected alias this driver's own schema door refuses outright. The `lookup` conjunct was therefore unreachable for every possible input, and no authored lookup had ever been indexed on MongoDB. The arm now reads `reference`, the only relationship spelling the spec declares. Verified as a complete case split over the key's value domain, not a sample: every `reference_to` value except `undefined` is refused at the door, and `undefined` is falsy, so the old conjunct could not be satisfied at all. The `user` disjunct needs no relationship key and is unchanged — which is why the feature looked healthy. Re-measured on this tree: 65 lookup fields carrying `reference` across the 52 exported platform objects gain their only join-index mechanism. The refusal door is unchanged — predicate, envelope, placement and instruction. Only the tail of its runtime message moved: it told the reader that renaming the key would not by itself get the field an index, true when written and false now. Three pins flip together, each per the direction it pre-wrote: the recorder pin in mongodb-schema-declared-indexes.test.ts, the real-server twin in mongodb-driver.test.ts, and the no-change control in the part (1) suite. The shipped README and the types.mdx callout both asserted the old behaviour and are corrected to describe the new one. Graded minor: this is a boot-time behaviour change for existing deployments. The changeset carries the operations note — cost, magnitude, and the hybrid-build statement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3jdziLbAPGeceVNmSox5L
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4edc15785ffc05749243b293bd1b152f6014410b && git checkout 4edc15785ffc05749243b293bd1b152f6014410b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin eb64351f092071aeccfb4eb7fb8492e8d08c4362 7ac0aa664356d68d909b91d684656b936e77733e && git checkout -B drift-repro eb64351f092071aeccfb4eb7fb8492e8d08c4362 && git merge --no-ff 7ac0aa664356d68d909b91d684656b936e77733e
node scripts/docs-audit/affected-docs.mjs --json eb64351f092071aeccfb4eb7fb8492e8d08c4362 |
zhuangjianguo
commented
Aug 31, 2026
PM review — ACCEPT, with one deviation adjudicated and surfaced. Release held on full CI green.
⚖️ The declared deviation — I am accepting it, and flagging it for the maintainerThe ruling says 「改谓词、不动门( Verified from the diff — the door's verdict machinery is untouched. The only non-comment changes in Accepting, for three reasons:
The pin was TIGHTENED, not relaxedThe door's own test file changed (+61/−28), which is exactly where a message change could have been accommodated by weakening an assertion. It was not. Of the assertion-bearing lines, the three removed are comments (part (1)'s notes that the case was "expected to flip"), and the added assertion is A1 — this is a proof, not a spot checkThe reachability answer is the report's best work. Rather than sampling, it observed that the arm required ⇒ The arm was dead for every possible input, and the blast radius is exactly index creation — which is what the ruling assumed. A2 — the count moved, and the consequence is bigger than the number65, not 57, across 52 exported platform objects rather than 44. The ruling's figure was right when taken and has grown. ⭐ A source grep would have undercounted badly — fields are declared through the
The ops note clears the hard gate on all three partsCost ✓ (full collection scan + external sort + storage + write amplification). Magnitude ✓ (65 as a measured floor, plugin objects enumerated, per-index order-of-magnitude table explicitly labelled not a benchmark). Build feature ✓ — and derived rather than asserted: ⭐ The line I would not have thought to require: the builds are serialized, so the times ADD — startup extends by the sum, not the slowest. That is the figure an operator actually plans a window around, and it was not asked for. Release conditionsCI at review time: 22 success, 1 skipped, 7 running, 0 red. The opt-in real-server suite ran ( Clause ②: I concur it does not fire, and the seat checked the contract face directly rather than by path — Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#13222
Part (2) of the 2026-08-31 ruling (第 5 场总监席决裁批 #10, verbatim「同意」, option A). Part (1) landed as #13481; this takes the other half.
syncCollectionSchema's field-level join-index arm gated onfield.reference_to— a rejected alias that part (1)'s door now refuses outright. The arm now readsreference, the only relationship spelling@objectstack/specdeclares.A1 — is the arm reachable today? No, and the proof is a complete case split
The ruling calls the arm 现不可达. Verified by execution rather than by reading, over the whole value domain of the key rather than a sample. The arm needs
field.reference_toTRUTHY; the door refuses everything!== undefined. Those two predicates partition the domain, so the enumeration below is exhaustive:reference_tovalueidx_company_id_lookup'company',1,true,{},[],null,'',0,falseVALIDATION_ERRORundefinedreference: 'company'(before this change)userfieldidx_owner_id_lookuppresentThe
usercontrol proves the arm executed and the harness was wired to something, so the zeros are real zeros. Verdict: the arm was unreachable for every possible input. Repointing it makes it live for the first time, so the blast radius is exactly index creation and nothing else — which is what the ruling assumed.A2 — re-measured: 65, not 57
Measured at runtime over the built
@objectstack/platform-objectsexport surface, not by grep (fields are declared throughField.lookup(...), so a source grep undercounts badly):reference_to, a spelling the spec rejects — so no authored lookup is ever indexed #12251)lookup, all 65 carrying canonicalreference, 0 carryingreference_tomaster_detail,treeoruserSo the ruling's 57/44 was correct when #12251 measured it and has since grown; the population is unchanged in kind, only larger. The ops note carries 65, not 57. Worth noting for reviewers: because no platform object uses
type: 'user', a platform deployment on MongoDB builds zeroidx_FIELD_lookupindexes today — theuserdisjunct that made the feature look healthy is not exercised by platform metadata at all.Clause ② — my reading agrees with the PM's: NOT firing
No
packages/spec/src/**path. More to the point, I checked the contract face directly rather than by path:FieldSchema's accept/reject answer is untouched by this PR,referencewas already the canonical declared key, and no authorable surface widens — the driver reads one more key it was always supposed to read. Creating an index changes physical schema, not contract. The one thing that did change shape near a contract is the refusal message tail, and that is prose, not a verdict.⛔ Deviation from "byte-identical", declared
The ruling says 改谓词、不动门 and the dispatch says leave
refuseRejectedReferenceAliasbyte-identical. The door's behaviour is byte-identical — predicate!== undefined,VALIDATION_ERROR/400 envelope, placement ahead ofcreateCollection, call site, and the rename instruction are all unchanged, verified by diffing the extracted function against HEAD.One thing did change inside it, and I am flagging it rather than burying it: the tail of the runtime message told the caller that
which was true when written and is false after this PR — renaming is now exactly what gets the index. Shipping a refusal that hands operators a stale claim about what the fix achieves seemed worse than the deviation, and the same principle the ruling applies to the
types.mdxcallout (随本变更改为如实描述新行为) applies here. Part (1)'s own comment pre-wrote this handover: "Whoever takes that ruling owns the arm, its comment, and the #12252 pin ... in one stroke." None of the five substrings the part (1) suite asserts on the message live in the changed sentence. Revert it if the reading is wrong — nothing else depends on it.What else changed, and why it is not scope creep
mongodb-schema-declared-indexes.test.ts, its real-server twin inmongodb-driver.test.ts, and the no-change control in the part (1) suite. Inverted in place, never re-baselined; no test skipped, disabled, quarantined or deleted.README.mdsaid "Lookup fields are not indexed today" and its// Creates:line omitted the index. This README ships to npm (it is infiles), so leaving it would publish the opposite of the behaviour this PR ships. The new// Creates:list is measured against the README's own fixture, not written from memory.types.mdx:734callout rewritten per the ruling's tail, including what alookupwith noreferencegets and whatmaster_detail/treeget.lookupdeclaring no target is NOT indexed. Measured onFieldSchema:{ type: 'lookup' }and{ type: 'lookup', reference: '' }both parse successfully, so this is a real authorable shape rather than a hypothetical. Filed as [spec]FieldSchemaaccepts alookup/master_detailwith noreferencetarget, though its own TSDoc calls the key required #13632 — the spec's prose callsreferencerequired and the schema does not enforce it. Out of scope here..changeset/mongodb-lookup-index-canonical-reference.md, gradedminor, covers all three named parts:syncSchemaafter upgrade builds these across collections that already hold data; full scan plus external sort per index; later boots are no-ops.createIndexawaited per index,syncSchemaawaited per object), so startup is extended by the sum, not the max. That is the figure to size a maintenance window on.@objectstack/driver-mongodbdepends onmongodb@^7.5.0, whose own README states "The driver currently supports 4.2+ servers", and 4.2 is exactly the release that made index builds hybrid. Every server this driver can connect to takes the exclusive lock only briefly at each end and serves reads and writes throughout — not a write stall. Both remaining caveats stated.It also tells an operator how to move the cost out of the boot window entirely.
Verification
Run at
7ac0aa6643, the final commit.pnpm --filter @objectstack/driver-mongodb testwithOS_TEST_MONGODB_MEMORY_SERVER_ENABLED=1— 29 files, 686 tests, 686 passed, 0 skipped. The real-server suite ran, so the flippedtoContainpin is verified against actual MongoDB rather than assumed.pnpm lint(eslint . --no-inline-config), whole repo, exit 0 — no narrowing claimed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived on the final commit: all green. Three needed the built workspace closure first and were green after it (check:type-check-debt --re-measure: "29 ledger entries re-measured, 1542 raw tsc errors total, none above its recorded number").check-test-completenessexits 3 = PREREQUISITE NOT MET by its own words — it grades a saved turbo log, so NOT MEASURED locally, not a red.node packages/lint/scripts/check-reference-carrier-shape.mjs— the always-runs gate most specific to this change: "OK — 5554 file(s) scanned, 516referencesite(s)".reference_toon the committed implementation. Mutation proven on disk (marker grep 1, deleted-text grep 0, bloba03821f4→fc118f73); result exactly the two flipped recorder pins red, 14 others green, so the mutation was surgical and the pins really depend on this change. Restored undertrap ... EXIT INT TERMwith an absolute repo root, and the restore proven by observation rather than exit code: post-restore hasha03821f4matches the HEAD blob exactly, marker residue 0, whole-treegit status --porcelainempty.driver-mongodb'stsconfig.jsonexcludes**/*.test.ts, sopnpm typecheck(exit 0) says nothing about the three test files this PR edits — confirmed withtsc --listFiles, 0 hits. Read it as NOT MEASURED for the test layer, not as green.check:type-check-coveragestructural half is green and this package carries no test-debt ledger entry, so nothing regressed there either.Generated by Claude Code
Generated by Claude Code