Conversation
The positional cursor on cloudsync_payload_chunks stated its resume lower bound only inside (db_version>? OR (db_version=? AND seq>=?)). The two arms carry distinct parameters, so SQLite derives no range from the disjunction and cloudsync_changes' xBestIndex was offered an upper bound and a site filter but no lower bound. Every call re-read the window from the start, evaluating cloudsync_col_value() on each discarded row, which made a full drain quadratic in the number of chunks. State db_version>=? explicitly alongside the disjunction. The term is logically implied, so the same rows are selected; it exists so the constraint loop emits a lower bound into the generated inner SQL and the (db_version) index can seek. Draining a 188-chunk window locally: 6110ms -> 213ms, with per-chunk cost now constant in the window size rather than proportional to it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make chunk-bench times a real positional drain per chunk index, then replays the same resume points straight at cloudsync_changes in three SQL shapes and prints the idxStr each one produces, so whether a lower bound reaches xBestIndex is visible rather than inferred. It asserts every shape selects the identical row at each resume point. Kept off CI: the timings are machine-dependent and the shape of the curve, not the absolute numbers, is the result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The defect
The positional cursor on
cloudsync_payload_chunkswas designed to make each/checkcall an O(1) seek to where the previous call stopped. It does not do that today.The resume lower bound is stated only inside the disjunction
(db_version>? OR (db_version=? AND seq>=?)). SQLite will derive a range term from a disjunction like this — but only when it can see both arms compare against the same value. The two arms here carry distinct anonymous parameters, and SQLite does not assume two parameters hold the same value, so it derives nothing.cloudsync_changesis a virtual table that string-builds its inner SQL from the constraintsxBestIndexreceives, so the clause it actually runs is:An upper bound and a site filter, and nothing saying where to start. Every call re-reads the window from the beginning and discards rows until it reaches the resume point — and each discarded row costs nearly as much as an emitted one, because the generated subquery evaluates
cloudsync_col_value()plus two joins per row. A full drain is therefore quadratic in the number of chunks.In production this took one tenant's change export to ~25 minutes, and before a server-side deadline was raised it never completed at all.
The distinction is invisible in the SQL text, which is why this survived review:
The fix
State the lower bound explicitly alongside the untouched disjunction:
The term is logically implied by the disjunction, so it selects exactly the same rows. It exists only so the constraint loop emits a lower bound into the generated inner SQL and the
(db_version)index can seek.Reusing one parameter across both arms would work identically. I chose the explicit conjunct because it states the bound the code depends on, rather than relying on a planner inference that a later edit could silently undo by splitting the parameter again.
Measurements
make chunk-bench(added here) reproduces the defect and measures the fix locally — no network, no server.Per-chunk latency by decile of chunk index, same run:
There is no single speedup multiplier here, and that is the point: the multiplier is proportional to the window size. Doubling the chunks doubled it (15.8x -> 29.2x). Growth exponents: before 3.86x for 2x the chunks (~N^1.95), after 2.09x (~N^1.06). The invariant is the last column — per-chunk cost stops depending on the window at all. O(N^2) -> O(N).
The benchmark also asserts that all three SQL shapes select the identical row at every resume point (0 differences across 93 points), which is the correctness claim the fix rests on.
Not a PostgreSQL defect
src/postgresql/cloudsync_postgresql.c:1400-1407writes the same disjunction with$3in both arms, so the parameter-identity trap does not apply, and PostgreSQL's planner handles OR'd range bounds natively rather than through a vtab constraint pipeline. No change there.Testing
dist/unit— 156 tests, all OKdist/review_regressions— 0 failuresdist/network_unit— all passedmake chunk-benchat 94 and 188 chunks, curve flat after the fixNotes for reviewers
makefirst.make dist/chunk_benchbuilds the test binary but notdist/cloudsync.dylib, which is what the benchmarkload_extensions — measuring against a stale extension shows the fix doing nothing.CHUNK_BENCH_TXNS=1is the negative control: onedb_versionfor the whole window, where no bound ondb_versioncan narrow anything. Cost there is flat and high (~65 ms/chunk) both before and after, as it should be.cloudsync_sqlite.c:1049and:1342claiming "O(1) seek per chunk" were aspirational; they are now accurate, so they are left alone.🤖 Generated with Claude Code