Repository navigation
Conversation
The token's checksum covered only the cursor, so a client could change its `filter` between pages and keep its `page_token`: the server paged on from that position under a predicate the token was never cut for, returning pages that matched neither filter, without an error. AIP-158 requires every field but `page_token` and `page_size` to stay the same between pages, and `aip-go`'s `CalculateRequestChecksum` enforces it. The token now carries a digest of the request's fields -- whatever the caller passes, typically parent, filter and order_by -- next to the cursor. `parse` recomputes it from the request in hand and returns `CursorError::RequestMismatch` when they differ. The digest is kept in its own field so a changed request is told apart from a corrupted token, and the checksum covers it, so rewriting it is still `TokenChecksumMismatch`. Fields are length-prefixed, so moving text across a field boundary is a different request. The empty token is still the first page for any request. `CURSOR_TOKEN_CHECKSUM_MASK` is bumped, so tokens issued before this are rejected cleanly rather than misread. Closes #13 BREAKING CHANGE: `Cursor::encode` and `Cursor::parse` take the request's fields, `&[&str]`, in the same order on both sides. Tokens issued by earlier versions no longer parse.
Replaces the request digest in the page token. The cursor now carries the filter it was issued under, set with `Cursor::with_filter`, and `compose()` checks the `push_where` clauses against it the same way it checks an explicit `order_by`: a changed, added or dropped filter is `Error::CursorMismatch`. Comparing the decoded `WhereClause` is a stronger guarantee than a hash, and `encode`/`parse` keep their signatures. `with_filter` accumulates with `and`, as `push_where` does, so the same clauses in the same order combine to the same clause on both sides. `CURSOR_TOKEN_CHECKSUM_MASK` is bumped, so tokens issued before this are rejected cleanly rather than misread. Closes #13 BREAKING CHANGE: a cursor used with `push_where` must carry the same filter: build the pager's cursor with `Cursor::new(order_by) .with_filter(filter)`, or `compose()` returns `Error::CursorMismatch`. Tokens issued by earlier versions no longer parse.
The cursor never needs the filter itself, only to tell whether the next page's is the same, so a checksum is enough: tokens no longer grow with the filter, and its SQL no longer travels to the client. A checksum can't be ANDed onto, so `with_filter` chains one CRC32 per clause, and `compose()` folds the same chain over its `push_where` clauses -- the same clauses in the same order match, empty ones counting for nothing. `Cursor::filter()` goes, since there is no clause to return. The order_by keys stay in the token as they were: the cursor needs them to build its comparison and to supply the ORDER BY, and `compose()` already compares them directly.
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.
Closes #13.
Problem
The page token carried only the
order_byand the boundary values. A client could change itsfilterbetween pages and keep itspage_token; the server then continued from that cursor under a different predicate and returned pages that matched neither filter, with no error. AIP-158 requires the filter to stay the same between pages.Change
The cursor now carries a checksum of the filter, and the composer checks it alongside
order_by.Cursor::with_filter(impl Into<WhereClause>)adds a filter clause, once perpush_wherecall, in the same order.push_where.compose()computes the same chain over its ownpush_whereclauses. A changed, added or dropped filter returnsError::CursorMismatch, whose message and docs now cover bothorder_byand the filter.order_bykeys stay in the token as before: the cursor needs them to build its comparison and to supply theORDER BY, andcompose()already compares them directly, so a checksum of them would add nothing.encode()/parse(token)keep their signatures.CURSOR_TOKEN_CHECKSUM_MASKis bumped (0x5eed_c0de→0x5eed_c0df), so tokens issued before this change are rejected cleanly instead of misread.CursorOrderByMismatchreference corrected toError::CursorMismatch.The commits show how the design changed: a request digest first, then the full filter in the token, then the filter checksum. Squash on merge.
For review
push_wherenow has to carry the same filter (Cursor::new(order_by).with_filter(filter)), orcompose()fails. Callers who don't attach it getCursorMismatchon page 2 instead of the old silent behaviour.push_where(a.and(b))doesn't matchwith_filter(a).with_filter(b). The docs ask for the same clauses in the same order.bind_value(e.g.tenant_id = $1) isn't part of the filter, so it isn't checked. Scope added throughpush_whereis.Error::CursorMismatch. Splitting them into separate variants would be a small change if you want to tell clients which one changed.Testing
make lint,make testandmake doc-checkpass locally, andcargo check --no-default-featurespasses for both crates.push_where) and rejects a changed, added or dropped filter, and a missing tenant scope.Pagerand a token, then shows a changed filter is refused.