Repository navigation
perf(api): paginate /api/data off cached_result_expr, not a full result.parquet (#90) - #92
Merged
Merged
Conversation
…quet Cheap and expensive entries both write no per-entry result.parquet at build (#74). Serving a paginated read through ensure_result reverses that by dumping the entry's whole result to disk for an O(limit) request — and for an expensive entry it duplicates the baked snapshot under the entry. Both tests assert no result.parquet exists under the entry after a page read (cheap path returns the right page+total; expensive path paginates in order across a deep offset). They fail today because api_data routes through ensure_result. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…result.parquet api_data called ensure_result on every paginated read, which materialised the entry's entire result to disk (cached_result_expr(...).to_parquet) before reading offset+limit rows back and slicing — O(rows) work plus a file write to serve an O(limit) request. For a cheap entry that reversed #74's "materialise nothing"; for an expensive entry it duplicated the baked snapshot under the entry. Serve the page straight off the expression cached_result_expr already returns: total from the manifest's row_count (api_entry_detail reads it the same way), rows from cached_result_expr(...).limit(limit, offset=offset).execute(). The window pushes down (expensive -> windowed read of the baked snapshot, cheap -> limit through to the source read) and writes nothing. ensure_result stays for the callers that need a parquet path (downloads, promote-diff, post-processing). Removes the now-unused _read_head_rows helper and its pyarrow import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
paddymul
added a commit
that referenced
this pull request
Jun 16, 2026
A cheap (unsorted) entry paginates by re-executing the recipe per page (no baked snapshot — only Aggregate/Join/Sort entries bake one), so two pages tile the result with no overlap or gap only if the source scan order is stable across executions. The old path sliced one on-disk result.parquet, stable by construction. Assert two pages equal the matching slices of a single full read. Passes today — a characterization lock on the property #92 introduced, not a bug repro. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
Post-merge reviewReviewed #92 after merge. The change is correct and a real win — it drops an Findings and dispositions:
Fixes for #1 and #4 are in #95 (green on CI: ruff ✓, fast ✓, integration ✓). #3 is tracked in #96. |
This was referenced Jun 16, 2026
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.
Addresses #90.
What
/api/data(app.py:518) paginated a windowed read by first materialising the entry's entire result to disk: it calledensure_resultunconditionally, whichcached_result_expr(...).to_parquet(result_path)s the full result, then readoffset+limitrows back and sliced. AnO(limit)request didO(rows)work and wrote a file — for a cheap entry that reverses #74's "materialise nothing"; for an expensive entry it duplicates the baked snapshot under the entry.This serves the page straight off the expression
cached_result_expralready hands back:totalcomes from the manifest'srow_count(recorded at build;api_entry_detailreads it the same way), so no file is needed to populate it.cached_result_expr(project, content_hash).limit(limit, offset=offset).execute(). The window pushes down — for an expensive entry it lowers to a windowed read of the baked snapshot, for a cheap entry the limit pushes through to the source read. Neither writesresult.parquet.ensure_resultstays for the callers that need a parquet path (downloads, promote-diff, post-processing) — this is the one read path that needs rows, not a path.The now-unused
_read_head_rowshelper (and itspyarrowimport) are removed.Tests
test_api_data_cheap_entry_serves_page_without_materialisingandtest_api_data_expensive_entry_paginates_without_per_entry_parquetassert no per-entryresult.parquetis written when a page is served (cheap path returns the right page+total; expensive path paginates in order across a deep offset). Both are committed first so the regression is visible failing on CI, then the fix lands.Context
Surfaced while reconciling the caching docs with #74. Same
ensure_result-on-read mechanism as #79 (concurrent materialisation race); adjacent to #71 (viewer re-runs the recipe on load).🤖 Generated with Claude Code