Repository navigation
Reload invalidated cache rows under negated and grouped primary-key conditions, and sort them by their reloaded value - #3923
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request optimizes and fixes the handling of invalidated or evicted cache rows during primary-key filtered, negated, grouped, and sorted reads. It introduces a prepareForSort mechanism to reload stubs before sorting, uses a unified keyPredicate helper for filtering, and adds comprehensive tests to verify these behaviors. The review feedback recommends a safer check for thenable objects and avoiding variable shadowing in Table.ts.
kriszyp
marked this pull request as ready for review
November 4, 2026 15:36
kriszyp
force-pushed
the
fix/invalidated-stub-negated-key-and-sort
branch
from
November 4, 2026 17:24
af653b1 to
ccba7db
Compare
kriszyp
force-pushed
the
fix/invalidated-stub-negated-key-and-sort
branch
from
November 4, 2026 22:04
ccba7db to
5d21c97
Compare
…ary-key conditions, and sort them by their reloaded value A negated primary-key condition (not_equals, not_in, not_between, not_starts_with) and an AND/OR group of primary-key conditions carried no idFilter, so transformToEntries dropped a null INVALIDATED stub instead of letting the materializer reload it. They now carry a key-only keyFilter, read through keyPredicate wherever a stub's admission is decided (transformToEntries and joinFrom); idFilter, which relationship id mapping reads, is unchanged. A post-sort read a stub's sort keys before the materializer reloaded it, so the reloaded row sorted by its missing value and limit/offset cut the wrong order. The materializer now exposes prepareForSort, which transformToOrderedSelect calls on a stub before reading its sort keys when the ordering reads record content: it reloads the row (one source load, reused by the materializer), keeps a stale-while-revalidate row stale and defers its reload to page time, and skips a row deleted at the source. A primary-key sort clause reads a stub's key. Fixes #3783, #3786 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
…e source prepareForSort can skip every row of an index-ordered group, and the ordered iterator treated an empty group as the end of the scan. The closure tests for sorted reads now cover a close during the pre-sort reload and, with a key-led sort, during materialization of a later row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
…ry-key clause, and read a one-element key path from the stub - prepareForSort treats a load that resolves with no record as absent, so the materializer does not load the row a second time, and starts no load once the selection has closed. - An ordering stops at its first primary-key clause: a primary key never ties, and a later clause would be resolved on a stub that was not reloaded. - getAttributeValue reads a stub's key for a one-element primary-key path, as it does for the bare name. - The stale-while-revalidate resource helper is built once per table, not per query. - The sorted-read closure tests cover a close during materialization of the first row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
…-row integration loop Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
…echeck closure after a synchronous preparation - prepareForSort hands on the loaded entry (or a recordless one) for a row the source no longer has when the read does not skip absent rows, instead of the flagged stub the materializer would send to the source again. - collect rechecks that the read is open after a synchronous prepareForSort, since a stale-while-revalidate hook is application code. - A stub resolves a one-element path (`['id']`, `['$updatedtime']`) as the bare name, and sortReadsRecord treats a one-element metadata path as metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
…n a stub only for the key or entry metadata Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
…main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: harper-invalidated-stub-pk-sort
kriszyp
force-pushed
the
fix/invalidated-stub-negated-key-and-sort
branch
from
November 5, 2026 01:56
5d21c97 to
7056332
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A caching-table query keeps a row the source must reload under negated or grouped key filters, and reloads it before sorting.
⊙ Problem
An invalidated row should reload from the source when queried (#3772). Two cases broke that:
id=not_in=(b),(id=a|id=c)) dropped the row (#3783).limit/offsetcut the wrong page (#3786).🤔 Is the requirement right?
Yes. The owner accepted reloading every invalidated row in a sorted range.
💡 Solution
keyFilteron negation, on group), read viakeyPredicatewhere rows are kept or dropped. Look hardest at the group: every member needskeyFilter.prepareForSort, called in sort) and reuse the fresh row.With
cinvalidated,GET /T/?id=ne=b&sort(+body)&limit(1)rankscby its reloaded body.🤔 Reloads before a sort run one at a time
500 invalidated rows mean 500 serial source calls; parallel loading is tracked in #3908.
🤔 Not covered here
Nested relationship selections and non-leading key conditions (#3907). Index-supplied order, expired rows and stale-while-revalidate rows off the page keep cached values (#3909). Index-covered
['id']selects are unchanged.⚖️ Alternatives
Three approaches set aside
idFilter. Relationship id mapping readsidFilter, sochildren.id=not_in=…would match a dangling foreign key.getAttributeValue. That skips the materializer's stale-while-revalidate, only-if-cached and resident-load rules, and loads twice.🔧 Changes
resources/search.ts:keyFilteron primary-key comparators, negations and all-key groups;keyPredicate, whichjoinFromalso reads.resources/Table.ts:transformToEntriesreadskeyPredicate.transformEntryForSelectexposesprepareForSort: rows kept by the stale-while-revalidate hook use stale sort values; rows deleted at the source are skipped.transformToOrderedSelectcalls it, stops at the first primary-key clause, and moves past an emptied group.getAttributeValuereads a stub's key.resources/DESIGN.md: the stub rule.✅ Verification
id=ne=b&sort(+body)&limit(1)returns the right row; 3 integration cases fail on main, 8/8 pass.Commands and results
invalidatedStubKeyAndSortReads.test.js(36 tests):not_in,not_between,not_starts_with, AND/OR groups, limit/offset/descending, metadata and primary-key sorts, deleted at the source, source rejection, a close during a reload, only-if-cached, stale-while-revalidate, row filters. Against main'sTable.ts/search.ts, 16 tests fail per engine, including the negated-join cases added tosortedKeyInvalidationReads.test.js. The sorted-read closure tests inresidencyReads.test.jsnow close during the pre-sort reload, or, with a key-led sort, during materialization.pk-filter-invalidated-reads.test.ts:limit(1)andlimit(1,2)pages,id=not_in=, a grouped condition.test:unit:resources:core:a-n2,706 andcore:o-z2,961 passing, 0 failing.:indexes: 761 pass and 4 derived-index durability tests fail; those 4 pass alone (160/160) and are green on main CI.resources/**: 277/278; the rate-limiter smoke that failed passes alone.test:unit:maindid not finish: lock-timing tests incomponentsstalled under host load (load average about 26). Left to CI.🤖 Generated by Claude Opus 5.5 (Anthropic); posted via @kriszyp.
Framing-Verdict: chosen-approach-sound (eb0a6ae932c1)
Release target: v5.4 — follows up #3772 (milestoned v5.4, not on v5.3); a query-correctness fix on main, no backport requested
Related PRs: #2155 overlaps (same joinFrom joined-filter loop), #3772 overlaps (merged; this follows it up), #3927 overlaps (merged; relationship conditions now existential, this rebased onto it), 7 others independent
Complexity: complicated
Deferred findings
b3de6d8a86f2ce34major performance —resources/Table.ts— pre-sort stub reloads run serially and scale with the matched set, not the page — filed Sorted reads over a caching table reload invalidated rows one at a time before sorting #39088497d5e0218cb0f2major correctness —resources/Table.ts— nested relationship sort never receives prepareForSort — filed Nested relationship selections and non-leading relationship conditions drop or misorder invalidated cache rows #390762cdde79c28b147dminor correctness —resources/Table.ts— nested relationship predicate drops null invalidated child before predicate; no-predicate path returns deleted child as null — filed Nested relationship selections and non-leading relationship conditions drop or misorder invalidated cache rows #3907ea39a25260c849eaminor correctness —resources/Table.ts— null stub under an index-ordered leading sort groups by missing value — filed Index-supplied sort order and expired rows still sort caching-table rows by cached values #39095c73c13b5de51a26minor correctness —resources/Table.ts— onlyIfCached scalar computed sort over null stub invokes resolver with null record — filed Index-supplied sort order and expired rows still sort caching-table rows by cached values #39099349bf2a4a40387eminor correctness —resources/Table.ts— SWR-kept stub sorted outside the page is never revalidated — NOT FILED9f80f430282297a9minor correctness —resources/Table.ts— expired rows without INVALIDATED/EVICTED flags are sorted by stale value then shown fresh — NOT FILEDfefbf67b18f168ffminor correctness —resources/search.ts— relationship-group compileConditions combiner omits keyFilter, dropping parents whose only child is a null stub — filed Nested relationship selections and non-leading relationship conditions drop or misorder invalidated cache rows #3907🤔 defer this PR's major performance finding
b3de6d8a86f2ce34(pre-sort stub reloads run serially and scale with the matched set, not the page) to #3908 instead of fixing it here?Bundle — this PR covers 1 ref.
Closes #3783
Origin — the dispatch brief this PR was written from
Invalidated cache rows: keep them for negated/grouped key filters and sort them after reload
Fix #3786 and #3783 in one PR. Both follow up merged #3772 in the caching-table INVALIDATED-stub keep/reload path. Owner decision (2026-11-02): a query may call the source to reload an invalidated row it would return; that cost is accepted.
not_equals,not_in,not_between,not_starts_with, …) and an AND/OR group of primary-key conditions carry noidFilter, sotransformToEntries(resources/Table.ts,processEntry/applyFilters) drops a null INVALIDATED stub and never reloads it.limit/offsetapply to the wrong order.Order of work:
@indexedattribute, rows a/b/c with distinctbody, invalidatec(_writeInvalidate), source returns{ body: 'fresh ' + id }; RocksDB and LMDB. Reproduce both issues' tables and show them failing on origin/main sources (forced rebuild).filterByType(resources/search.ts, "negated conditions are filter-only") and AND/OR groups of primary-key conditions inmapConditionsToFiltersanidFilter(negated key test, group of key tests), so the stub is kept and reloaded; assert the source is called once per kept stub. A record guard or non-key condition must still drop the stub.limit/offsetapply to the corrected order; coverid in (...)andid ge a, with and withoutlimit: 1. A sorted read with no stubs must cost nothing extra; measure it.resources/DESIGN.md.Out of scope: computed-resolver retirement (harper#3782), relationship joins (
joinTocannot apply the key rule), eviction semantics. Coordinate with the in-flight index-covered select work (task harper-covering-index-select-projection, branch perf/covering-index-select), which serves['id']/['id', attr]selects from the index without loading records: if a covered query reaches an invalidated row, note the interaction in the PR body rather than changing that branch. Release target v5.4 (main).Acceptance
Unit tests for every row in the #3783 and #3786 issue tables fail on origin/main and pass, on both storage engines; the source is called once per kept stub; an HTTP integration test on a caching table (invalidate a row, then
GET /T/?id=ne=b&sort(+body)&limit(1)) returns the reloaded row in its correct position.Dispatch: task
harper-invalidated-stub-pk-sort· queued by kris-session · ran by claude/opus/high · worker kzyp-xps-1Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=13; full=7 @ 7056332
Review-Attention: deep ~30m (critical: Table.ts, search.ts; decisions: keyfilter-separate-from-idfilter, reload-before-sort-vs-sort-stale, do-less-alternative, deferred-b3de6d8a) @ 7056332