From ba6e174031e4d25b46da608e0671bdec20f6bd45 Mon Sep 17 00:00:00 2001 From: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:32:04 -0400 Subject: [PATCH 1/2] [bug] Keep cache keys stable across processes on Python 3.14 PEP 649 attaches an auto-generated __annotate__ (and __annotate_func__) to every annotated class. ccflow's behavior token hashes class method bytecode, so it picked up the __annotate__ closure, whose cells are PYTHONHASHSEED-sensitive. That made the behavior token, and therefore cache keys, differ between processes. Skip __annotate__/__annotate_func__ in the behavior token. Annotations are already captured through other paths, so this is lossless for the cache contract. Add a regression test that injects __annotate__ so it runs on pre-3.14 interpreters. Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com> --- ccflow/tests/utils/test_tokenize.py | 19 +++++++++++++++++++ ccflow/utils/tokenize.py | 5 +++++ 2 files changed, 24 insertions(+) diff --git a/ccflow/tests/utils/test_tokenize.py b/ccflow/tests/utils/test_tokenize.py index 1719c2a..bd52889 100644 --- a/ccflow/tests/utils/test_tokenize.py +++ b/ccflow/tests/utils/test_tokenize.py @@ -265,6 +265,25 @@ def f(self): assert compute_behavior_token(A) == compute_behavior_token(B) + def test_skips_annotate_methods(self): + """PEP 649 __annotate__/__annotate_func__ are skipped (injected here to run pre-3.14).""" + + def _annotate(fmt): + return {"x": int} + + class A: + __annotate__ = _annotate + __annotate_func__ = _annotate + + def f(self): + return 1 + + class B: + def f(self): + return 1 + + assert compute_behavior_token(A) == compute_behavior_token(B) + def _helper_add(x): return x + 1 diff --git a/ccflow/utils/tokenize.py b/ccflow/utils/tokenize.py index 3381a7f..ca82f24 100644 --- a/ccflow/utils/tokenize.py +++ b/ccflow/utils/tokenize.py @@ -433,6 +433,11 @@ def compute_cache_token(*, data_values: Iterable[Any] = (), behavior_classes: It "__get_pydantic_core_schema__", "__get_pydantic_json_schema__", "model_post_init", + # PEP 649: the auto-generated __annotate__ closure is PYTHONHASHSEED-sensitive, + # which would make the behavior token differ between processes. Annotations are + # already captured through other paths, so skipping these is lossless. + "__annotate__", + "__annotate_func__", } ) From ef7a0195ce3f8c7a0ea58e26424ea0e7cd8eb230 Mon Sep 17 00:00:00 2001 From: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:50:25 -0400 Subject: [PATCH 2/2] [bug] Fix TPC-H answer lookup on duckdb 2.0 duckdb 2.0 removed fetchone() from the relation returned by Connection.query(). Switch to relation.fetchall() (duckdb's recommended replacement), which exists on both old and new duckdb and fully drains the result, so the shared connection stays usable for the next query. Validated against duckdb 2.0.0 and 1.4.0. Signed-off-by: Pascal Tomecek <40371786+ptomecek@users.noreply.github.com> --- ccflow/examples/tpch/data_generators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ccflow/examples/tpch/data_generators.py b/ccflow/examples/tpch/data_generators.py index ff49b55..ad19857 100644 --- a/ccflow/examples/tpch/data_generators.py +++ b/ccflow/examples/tpch/data_generators.py @@ -103,10 +103,10 @@ def get_table(self, table: TPCHTable) -> pl.DataFrame: return pl.from_arrow(tbl_arrow) def get_answer(self, query_id: int) -> pa.Table: - row = self._conn.query(f"SELECT answer FROM tpch_answers() WHERE scale_factor={self.scale_factor} AND query_nr={query_id}").fetchone() - if not row: + rows = self._conn.query(f"SELECT answer FROM tpch_answers() WHERE scale_factor={self.scale_factor} AND query_nr={query_id}").fetchall() + if not rows: raise ValueError(f"No TPC-H answer found for scale_factor={self.scale_factor}, query_nr={query_id}") - return pc.read_csv(io.BytesIO(row[0].encode("utf-8")), parse_options=pc.ParseOptions(delimiter="|")) + return pc.read_csv(io.BytesIO(rows[0][0].encode("utf-8")), parse_options=pc.ParseOptions(delimiter="|")) class TPCHTableProvider(CallableModel):