Repository navigation
feat(server): stats_request, stats_update and df_meta.stats on deferred sessions (rows-first s3) - #1024
Merged
paddymul merged 10 commits intoOct 6, 2026
Conversation
Contributor
📦 TestPyPI package publishedpip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37513303397or with uv: uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo==0.15.9.dev37513303397MCP server for Claude Codeclaude mcp add buckaroo-table -- uvx --from "buckaroo[mcp]==0.15.9.dev37513303397" --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ buckaroo-table📖 Docs preview🎨 Storybook preview |
This was referenced Oct 4, 2026
Merged
Closed
Draft
paddymul
changed the base branch from
main
to
adr-002-rows-first-stats-delivery
October 6, 2026 14:40
paddymul
added a commit
that referenced
this pull request
Oct 6, 2026
…irst p33) /load_expr and /reload_expr accept stats_tier auto, full, scalar or schema, stored with the pair and kept out of has_config, so the warm short-circuit holds. When the dataflow is built at the schema tier the handler resolves the policy (resolve_stats_policy) from the count it already has, stores it on the session and starts the stats generation from it: a target below full is not_computed with the policy's reason. df_meta.stats reports the policy as tier_target and estimate, plus auto_request, requestable, omitted_keys, approx_keys and demand_columns where they differ from their documented defaults. It is applied at WebSocket open only for a client that sends ?caps=stats_update,stats_ondemand. A client with stats_update only is told the session is pending and pulls the stats, and a client with no caps gets them at connect, as for any deferred session. A tier the host named (scalar, schema) reaches every client as before. A session on an explicit stats_tier full within the ceiling sends the message it always has. /load keeps resolving to full: it does not read the field and clears a policy left on the session. Rebased onto #1024 without the unit PRs (#1026, #1028): stats_request is the whole run of #1024, so handle_stats_request now takes the client to decide stats_to_pull, and the incremental and display-config parts are gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
paddymul
force-pushed
the
feat/rowsfirst-s3-stats-wire-server
branch
from
October 6, 2026 18:21
f5810de to
ecb0088
Compare
paddymul
force-pushed
the
adr-002-rows-first-stats-delivery
branch
from
October 6, 2026 18:27
c500780 to
f61df3d
Compare
…tats_request (rows-first s3)
On a deferred /load_expr session a stats_request {stats_gen, scope} should
return a stats_update with the matching stats_gen whose inline wide payload
equals the all_stats an inline session sends, a stale stats_gen should get
stats_aborted and run no query, and /load_expr and /reload_expr should bump the
generation. df_meta.stats should be injected on every frame and survive a
dataflow-field change, which returns the session to the schema tier. With a
caps client and a legacy client on one session, the legacy client should keep
getting complete messages through the websocket broadcast, the /load_expr,
/reload_expr, /load and /load_compare pushes and the highlight overlay, while
the caps client gets a stats-free frame and then pulls a stats_update. A spy
telemetry sink should see a stats.request span.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eration edge cases (rows-first s3) Four more cases for the stats wire format, kept in their own commit so each is seen failing on CI before the implementation lands. Returning to a state whose stats were completed once is answered from summary_stats_cache with no query. Completing the stats keeps the session's component_config on the refreshed display config. A warm /load_expr, which rebuilds nothing, leaves stats_gen alone. A stats_request on a session with no data is answered with stats_aborted. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed sessions (rows-first s3)
A client that advertises ?caps=stats_update gets a stats-free initial_state on a
deferred /load_expr session (df_meta.stats.status "pending") and pulls the stats
with stats_request {stats_gen, scope}. The reply is a stats_update carrying the
dataflow's all_stats as an inline wide envelope, or stats_aborted when the
generation is stale. The request is the whole run: one synchronous call that
computes the full stats, writes the full-tier summary_stats_cache entry, assigns
summary_sd and refreshes the session snapshot through one helper.
stats_gen is a server-owned counter bumped by every load handler and by a state
change that touches a dataflow field, which also returns a deferred session to
the schema tier. df_meta.stats is injected by build_state_message from the
session, since the dataflow rebuilds df_meta wholesale. Every send site goes
through build_state_message_for, so a client without the capability still gets
complete messages (its missing stats run synchronously first); broadcast_state
replaces the five copies of the send loop and sends to capable clients first.
The stats_request branch binds the session's telemetry sink and emits a
stats.request span.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…y dedupe key, span sinks and a failed state change (rows-first s3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…; stats.complete span; failed state change restores the tier (rows-first s3) - A summary_stats_cache hit no longer reports a clean run: the errs of the run that filled an entry are kept beside it (_summary_errs_cache) and set_stats_tier reads them back. errs is assigned before summary_sd so _populate_sd_cache files them with the sd. - set_stats_tier puts the tier and the summary key back when the switch raises. - complete_stats times the run as stats.complete rather than firstpull.stats_total, which also fired for completions long after the load, and binds the session's telemetry sink only when it has one so an outer sink is not replaced by None. - A buckaroo_state_change that raises puts the dataflow's tier back, so it matches the snapshot the session still describes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ats.complete span (rows-first s3) The legacy overlay test calls _send_client_state, which replaced _send_highlight_overlay on main, the span test expects stats.complete, and the sink test only rules out firstpull.stats_total (the xorq dataflow emits its own firstpull.summary_stats). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…(rows-first s3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…/load_expr (rows-first s3) The page renders its rows and the schema-tier dtype row, and the stats rows stay empty: no client sends stats_request after the first frame (the scheduler PR #1027 is closed) and the server pushes nothing (ADR-002 D2 is unimplemented). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
paddymul
force-pushed
the
feat/rowsfirst-s3-stats-wire-server
branch
from
October 6, 2026 18:31
ecb0088 to
1b85959
Compare
…first row reply (rows-first s3) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ion (rows-first s3, ADR-002 D2) A client that advertises ?caps=stats_update and is sent a pending frame is owed that generation's stats. The handler registers a continuation on the write future of the client's first row reply; it sends the stats_update once the rows are on the socket, if the connection is still open and the generation is still current. The stats run once per session, so a later connection's push is answered from the session. The stats.push span records the gap since the rows. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This branch was successfully deployed
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.
Stacked on #1022 (
feat/rowsfirst-s2-schema-tier-pandas-polars), which is stacked on #1021 (feat/rowsfirst-s1-stats-tier-core). This PR is based onmainso the repo's Checks workflow runs on it (itspull_requesttrigger usesbranches: "*", which does not match a base branch containing a slash). The diff therefore includes the commits of #1021 and #1022 until they merge. The commits of this phase ared7ba7bd9and6bd63ed7(failing tests) and9bfebfb9(implementation); read only those three.Problem
#1021 lets a host load a xorq expression with
stats_delivery="deferred", which publishes a schema-tier dataflow and stops there: the stats never arrive, and nothing on the wire says so. A client cannot tell a session that is waiting for stats from one that will never have them, cannot ask for them, and cannot tell a reply for the state it is looking at from one for a state it has left (#998, for stats).DataStreamHandler.on_messagedrops unknown message types, so astats_requestis ignored today. The dataflow also rebuildsdf_metawholesale on each state change, so any flag the dataflow wrote there would be lost at the next change.A deferred session must also keep serving clients that know nothing about this protocol (older embeds, MCP and Python callers, tallyman on
buckaroo-js-core0.15.8). The connection is seen only inopen(), but the session snapshot is pushed from five other sites and the highlight overlay, so a check atopen()protects the first message only.Phase and plan references
Rows-first s3, from
buckaroo2-reports/plans/: plan 1 (01-rows-first-stats-separate-plumbing.md) section 9 "Phase 2", server half (the jest half is a separate client phase), with sections 3, 4.0, 5, 6 and 8; plan 2 (02-rows-first-xorq-and-lazy-polars.md) sections 3 and 4.1 for the final assignment and the snapshot refresh; plan 3 (03-no-summary-stats-for-large-files.md) section 3.2 for the field names (reason,elapsed_ms,not_computed).Approach
SessionStategainsstats_gen,stats_statusandstats_reason.begin_stats_generation(session)bumps the counter and restarts the status from the session's policy pair:pendingfor a deferred session headed for the full tier,not_computed(reasonhost) for one headed for the schema tier,completeotherwise. It is called by/load,/load_expr,/load_compare,/reload_exprand by abuckaroo_state_changethat changes a_DATAFLOW_FIELDSfield. It is independent of fix(server): sequence token on buckaroo_state_change so a stale initial_state is dropped (#998) #1014'sstate_seq.build_state_messageinjectsdf_meta.stats = {status, tier, gen, reason?}into a copy ofdf_meta, because the dataflow rebuildsdf_metawholesale.tieris the tier reached so far, so it isschemauntil the session is complete. A session on the default policy (inline delivery, full tier, complete) gets nostatskey and sends the message it always has; a client reads a missingstatsas complete.open()records?caps=a,basself.caps. A new module,buckaroo/server/stats_wire.py, holds the rest.build_state_message_for(session, client, metadata=None)is the one builder every send site uses: for a client withoutstats_updateon a pending deferred session it first runs the missing stats synchronously (today's cost, paid on the loop), and a capable client gets the snapshot as it is, stats-free.broadcast_state(session, metadata=None, reset_search=False)replaces the four copies of the per-client send loop (LoadHandler._push_state_to_clients,/load_expr,/load_compare,/reload_expr) and the one in the WebSocket state-change handler, and sends to capable clients first: a legacy client's message completes the session, and a message built after that would carry the stats, so the capable client would never get the pending frame that describes its own state.stats_request {stats_gen, scope, columns?}is a synchronous branch inon_message. A request for another generation getsstats_aborted {stats_gen, current_gen, scope, reason: "stale"}before any query runs. Otherwise the request is the whole run:complete_stats(session)computes the full stats in one call and applies the final assignment. It writes the full-tier entry intosummary_stats_cache(or finds the one an earlier visit to the same state left there), assignssummary_sd, refreshes the session snapshot throughrefresh_session_snapshotand sets the status tocompletein the same step. The reply isstats_update {type, stats_gen, scope, tier, final: true, payload, elapsed_ms}withpayloadthe dataflow's ownall_stats, an inlineparquet_b64envelope withlayout: "wide", so binary pairing stays single-slot. A repeated request on a complete session is answered from the dataflow with no query.stats_genand broadcasts. The stats then follow as requests, or run synchronously for a legacy client.stats_requestbranch enterstelemetry_contextwithsession.tele_sink, andhandle_stats_requestwraps each request in astats.requestspan (stats_gen,scope,columnsas a count,tier,outcome).outcomeisupdateor the abort reason, so updates sent, requests dropped as stale and errors are counted from the span records.complete_statsadds afirstpull.stats_totalspan per completed run.What changes
buckaroo/server/session.py:stats_gen,stats_status,stats_reason,STATS_STATUSES,initial_stats_status,begin_stats_generation,stats_meta;build_state_messageinjectsdf_meta.stats.buckaroo/server/stats_wire.py(new):parse_caps,client_has_cap,session_dataflow,refresh_session_snapshot,assign_full_stats,complete_stats,build_state_message_for,broadcast_state,handle_stats_request.buckaroo/server/websocket_handler.py: caps atopen(), thestats_requestbranch, the tier reset andbegin_stats_generationin_handle_buckaroo_state_change,refresh_session_snapshotandbroadcast_statein place of the copies, the overlay built throughbuild_state_message_for.buckaroo/server/handlers.py:begin_stats_generationin the four load handlers,broadcast_statein place of the copies,refresh_session_snapshotin/reload_expr;/loadand/load_comparereset the stored stats policy to the defaults.Tests
Three commits: failing tests (
d7ba7bd9), a second batch of failing tests (6bd63ed7), then the implementation (9bfebfb9). The second batch is four cases I found untested after the first was seen failing on CI, so they got their own commit and their own failing run. All new tests are intests/unit/server/test_load_expr.py, in a classTestStatsWirenext toTestLoadExprStatsPolicy, which already holds the deferred-session tests. They load a 5-row table whose float column has a 1e9 maximum, so full stats change itsminWidthand a stats-free message differs from a complete one there.test_stats_request_returns_a_stats_update_that_completes_the_stats: the reply carries the matchingstats_gen, an inline wideparquet_b64payload, and rows equal to theall_statsan inline session of the same build sends.test_rows_are_served_before_and_after_the_stats_requestchecks that no stray binary frame follows it.test_a_stale_stats_gen_gets_stats_aborted_and_runs_nothing(a spy onXorqStatPipeline._executesees no query),test_an_unsupported_scope_gets_stats_aborted.test_load_expr_and_reload_expr_bump_stats_gen,test_df_meta_stats_survives_a_dataflow_field_change(the frame after a search carriesdf_meta.statswith the next generation next to the rebuiltdf_meta, runs no stat query, and a request for the old generation is stale).test_a_legacy_client_stays_complete_while_a_caps_client_gets_a_stats_free_frame: two caps clients (one with?caps=other,stats_update) and two legacy clients (one with?caps=unknown) on one session. A legacy client'spost_processingchange gives both legacy clients a message equal to the inline session's apart fromdf_meta.stats, gives both caps clients a frame with the schema tier only, and a caps client'sstats_requestis then answered with no further query.test_load_expr_push_keeps_a_legacy_client_complete,test_reload_expr_push_keeps_a_legacy_client_complete,test_load_push_leaves_both_clients_complete,test_load_compare_push_leaves_both_clients_complete, andtest_highlight_overlay_is_complete_for_a_legacy_client_and_stats_free_for_a_caps_client.test_overlay_for_a_legacy_client_is_built_after_its_stats_are_completedcalls_send_highlight_overlaydirectly, because every send completes a session that has a legacy client connected, so the overlay never meets a pending session through the socket.test_a_client_connecting_after_completion_gets_the_complete_state,test_a_legacy_client_connecting_to_a_pending_session_completes_it: stats computed once are not computed again for a later client.test_a_session_targeting_the_schema_tier_is_not_computed,test_a_failed_stats_run_reports_error_until_the_next_generation.test_stats_request_emits_a_stats_request_span: a spy sink seesstats.requestfor a stale and a served request, andfirstpull.stats_total.test_returning_to_a_completed_state_is_answered_from_the_cache(search, complete, clear the search, and the original state's request runs no query and returns the original rows),test_completing_the_stats_keeps_component_config,test_a_warm_load_expr_keeps_the_generation,test_a_stats_request_with_no_data_loaded_is_aborted.test_inline_sessions_send_no_df_meta_stats,test_load_push_leaves_both_clients_completeandtest_load_compare_push_leaves_both_clients_completepass on the base branch by design: they pin that the default policy sends the message it always has, and that/loadand/load_compareleave no deferred policy behind.On the first tests commit 16 of its 19 tests fail locally (15 on
initial_state carries no df_meta.stats, one on the session having nostats_status) and the three above pass; with the second batch 20 of 23 fail. I checked the tests against seven deliberate regressions of the implementation (no capable-first order, no tier reset on a state change,/loadkeeping the stored policy, the overlay copying the display config before the message is built, nosummary_stats_cachelookup, a snapshot refresh that dropscomponent_config, a warm/load_exprthat bumps the generation); each makes at least one test fail.CI on the tests commits: on
d7ba7bd9and again on6bd63ed7(both without the implementation) six of the ninePython / Testjobs fail (3.11, 3.12 and 3.13, and Max Versions 3.11, 3.12 and 3.13); the rest of the checks pass. The other three (3.14, Max Versions 3.14, Windows) pass becausexorqis gated to Python below 3.14 andtest_load_expr.pyis skipped on Windows, so they never ran the new tests. CI's annotation only says the step exited 1 and the REST log API is rate-limited for this account, so the reason for the failures rests on the local run of the same commit.CI on the implementation commit
9bfebfb9: all 27 check runs completed, 26 succeeded anddeploywas skipped. That includes all ninePython / Testjobs,Python / Lint, the JS job, the wheel build and the Playwright jobs. Locally the full unit suite (pytest ./tests/unit -m "not slow") gives 1268 passed and 5 skipped.Why default behaviour is unchanged
stats_delivery="deferred"orstats_tier="schema"only from a/load_expror/reload_exprbody field (feat(xorq): stats tier machinery and the xorq schema tier (rows-first s1) #1021). On the default policystats_metareturnsNone, sodf_metais the dataflow's own object andinitial_stateis byte-for-byte what it was.build_state_message_forruns stats only for a deferred session whose status is pending. Every other session takes the pathbuild_state_messagealways took, with the client's ownsearch_string.broadcast_stateis the old loop in one place. The only change for a session on the default policy is the send order (capable clients first), which has no effect when no client has a capability.stats_genis bumped on every session, but it is read only throughdf_meta.stats, which a default session does not send.Deviations from the plan
stats_wire.py, holds the builder, broadcast and request handling.session.pystays free ofdata_loadingimports, and the plan's three files would each have needed to import the others.broadcast_statereplaces the five send loops outright, where the plan routes each loop throughbuild_state_message_for. Capable-first ordering needs one place to live.stats_genis also bumped by/load_exprand/load_compare, not only/load,/reload_exprand state changes: all of them replace the data the stats describe./loadand/load_comparereset the session's stored stats policy tofullandinline. feat(xorq): stats tier machinery and the xorq schema tier (rows-first s1) #1021 noted that/loadleft the pair in place. That is harmless while nothing reads it, but a staledeferredwould now make a pandas session look pending and send it throughcomplete_stats.df_meta.statsis also sent for a session headed for the schema tier with inline delivery (not_computed, reasonhost), not only for deferred ones. The status follows from the policy pair, so the rule is the same for both.stats_ondemandcapability decides this later.stats_updatecarrieselapsed_ms(plan 3 section 3.2), which the scope's message shape does not list.stats_abortedis{type, stats_gen, current_gen, scope, reason}, withstats_genthe request's so a client can pair them; reasons arestale,unsupported_scope,not_requestable,errorandno_data.scope: "raw"is accepted. Filtered stats exist only through a dataflow-field change, which bumps the generation (plan 1 section 6).columnsis read for the span and otherwise ignored by a whole-run request.error, reasonstats_failed) and is not retried by the next request. The plan does not say.refresh_session_snapshotreplaces two of the three copies named in plan 2 section 4.1 (websocket_handler.pystate change,/reload_expr). The/load_exprcopy sets more fields and reads the dataflow's attributes directly, so it stays.outcomeattribute of thestats.requestspan, not separate counters.Not in this PR
StatRun, the time budget and per-unit spans: the next phases generalizecomplete_stats.stats_updatehas noremainingfield yet.final: the final reply does not carry a rebuiltdf_display_args, so a caps client keeps its schema-tier display config (a float column'sminWidth, for one) until its nextinitial_state. A client that connects after completion gets the refreshed config.?caps=stats_ondemand, the policy fields of plan 3 and any default flip./load(pandas and polars): the fields exist on/load_expronly.🤖 Generated with Claude Code