Skip to content

Commit a2a2bb2

Browse files
timsaucerclaude
andcommitted
fix: accumulate planner observations instead of overwriting them
foreign_session, foreign_provider, and foreign_plan were written with store, so each one described only the most recent plan. Their accessors are named foreign_*_observed, which asks whether the thing was ever seen, and the tests assert them after running more than one query. The existing tests passed by luck. Reproduced: after scanning a foreign provider and then running SELECT 1, foreign_provider_observed goes from True back to False. Writes them with fetch_or so a later plan cannot retract what an earlier one observed. plan_calls already accumulated, used_fallback only ever stores true so it was already cumulative, and last_max_rows is deliberately last-wins as its name says. Documents that split on the struct, since it is the kind of thing that gets "tidied" back. Confirmed non-vacuous: with store restored, exactly the new test fails and the other 22 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ca3264e commit a2a2bb2

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

examples/datafusion-ffi-query-planner-example/python/tests/_test_three_library_query_planner.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,36 @@ def test_a_session_fallback_delegates_to_its_installed_planner():
398398
assert first.plan_calls() > 0
399399

400400

401+
def test_observations_accumulate_across_queries():
402+
"""A later plain query must not retract what an earlier query observed.
403+
404+
The ``*_observed`` accessors answer "was this ever seen". They are written
405+
with ``fetch_or`` rather than ``store`` so a query that touches no foreign
406+
object cannot clear a flag an earlier one set. Written with ``store``,
407+
``SELECT 1`` here clears ``foreign_provider_observed``, and every other
408+
test asserting these flags after more than one query is a coincidence away
409+
from failing.
410+
"""
411+
ctx, _logical_codec, _physical_codec = configured_context(max_rows=3)
412+
planner = MyQueryPlanner()
413+
ctx = ctx.with_query_planner(planner)
414+
415+
ctx.sql('SELECT "A" FROM numbers ORDER BY "A"').collect()
416+
assert planner.foreign_session_observed()
417+
assert planner.foreign_provider_observed()
418+
assert planner.foreign_plan_observed()
419+
420+
# Touches no table, so this plan has no foreign provider of its own.
421+
ctx.sql("SELECT 1").collect()
422+
assert planner.foreign_session_observed()
423+
assert planner.foreign_provider_observed()
424+
assert planner.foreign_plan_observed()
425+
426+
# `last_max_rows` is deliberately not cumulative; it reports the last plan.
427+
assert planner.last_max_rows() == 3
428+
assert planner.plan_calls() >= 2
429+
430+
401431
def test_second_planner_replaces_the_first():
402432
"""A session holds exactly one planner, so installing another replaces it."""
403433
ctx, _logical_codec, _physical_codec = configured_context(max_rows=2)

examples/datafusion-ffi-query-planner-example/src/planner.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,24 @@ use pyo3::types::PyCapsule;
4141

4242
use crate::config::MyPlannerConfig;
4343

44+
/// What the planner saw, accumulated across every call rather than reset each
45+
/// time.
46+
///
47+
/// Two kinds of field here, and mixing them up is easy. `last_max_rows`
48+
/// reports the most recent value, as its name says. Everything else is
49+
/// cumulative: a count, or a "did this ever happen" flag written with
50+
/// `fetch_or` so a later plan cannot retract an earlier observation. Tests
51+
/// assert after running more than one query, so a flag that only described the
52+
/// most recent plan would be answering a different question than the one its
53+
/// accessor name asks.
4454
#[derive(Default)]
4555
struct PlannerObservations {
4656
plan_calls: AtomicUsize,
4757
last_max_rows: AtomicUsize,
4858
foreign_session: AtomicBool,
4959
foreign_provider: AtomicBool,
5060
foreign_plan: AtomicBool,
61+
/// Only ever set to `true`, so it is already cumulative.
5162
used_fallback: AtomicBool,
5263
}
5364

@@ -156,10 +167,14 @@ impl QueryPlanner for DistributedQueryPlanner {
156167
session: &dyn Session,
157168
) -> datafusion::common::Result<Arc<dyn ExecutionPlan>> {
158169
self.observations.plan_calls.fetch_add(1, Ordering::SeqCst);
170+
// `fetch_or`, not `store`: these answer "was this ever seen", so a
171+
// later plan that happens not to touch a foreign object must not
172+
// retract what an earlier one observed. A bare `SELECT 1` after a
173+
// scan of a foreign provider would otherwise clear the flag.
159174
self.observations
160175
.foreign_session
161-
.store(session.as_any().is::<ForeignSession>(), Ordering::SeqCst);
162-
self.observations.foreign_provider.store(
176+
.fetch_or(session.as_any().is::<ForeignSession>(), Ordering::SeqCst);
177+
self.observations.foreign_provider.fetch_or(
163178
logical_plan_has_foreign_provider(logical_plan),
164179
Ordering::SeqCst,
165180
);
@@ -184,7 +199,7 @@ impl QueryPlanner for DistributedQueryPlanner {
184199
};
185200
self.observations
186201
.foreign_plan
187-
.store(physical_plan_has_foreign_plan(&plan), Ordering::SeqCst);
202+
.fetch_or(physical_plan_has_foreign_plan(&plan), Ordering::SeqCst);
188203

189204
Ok(Arc::new(GlobalLimitExec::new(
190205
plan,

0 commit comments

Comments
 (0)