Skip to content

Commit ec683bb

Browse files
paddymulclaude
andauthored
fix(xorq): use WeakKeyDictionary for _expr_count_cache to prevent id() reuse (#889)
`_expr_count_cache` was keyed by `id(expr)`, which is the CPython memory address. When a GC'd expression's address was reused by a new expression, the cache returned a stale count for the new object. Concretely: `test_filter_pushes_down` cached count=4 for a filtered expr; after GC that address was reused by the 2-row search result in the next test, causing `filtered_rows` to read 4 instead of 2. Switching to `weakref.WeakKeyDictionary` (keyed by the expression object directly) means entries are evicted when the expression is GC'd, eliminating the false hit. Also update the `TestExprCountMemoization` assertions that referenced `id(stub)` directly. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 153ca64 commit ec683bb

2 files changed

Lines changed: 8 additions & 8 deletions

File tree

buckaroo/xorq_buckaroo.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import logging
1414
import traceback
15+
import weakref
1516
from io import BytesIO
1617
from typing import Any
1718

@@ -45,14 +46,13 @@ def _is_pandas(obj: Any) -> bool:
4546
# with one entry per unique expression object observed; entries become
4647
# unreachable when the expression is GC'd, so for a long-running
4748
# session it tracks the live set of expressions naturally.
48-
_expr_count_cache: dict[int, int] = {}
49+
_expr_count_cache: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
4950

5051

5152
def _expr_count(expr_or_df: Any) -> int:
5253
if _is_pandas(expr_or_df):
5354
return len(expr_or_df)
54-
key = id(expr_or_df)
55-
cached = _expr_count_cache.get(key)
55+
cached = _expr_count_cache.get(expr_or_df)
5656
if cached is not None:
5757
return cached
5858
try:
@@ -65,7 +65,7 @@ def _expr_count(expr_or_df: Any) -> int:
6565
# next call retries the backend.
6666
logger.exception("_expr_count: backend count failed; not caching")
6767
return 0
68-
_expr_count_cache[key] = result
68+
_expr_count_cache[expr_or_df] = result
6969
return result
7070

7171

tests/unit/test_xorq_buckaroo_widget.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_expr_count_memoizes_repeat_calls(self):
9595
assert len(xorq_buckaroo._expr_count_cache) == 1, (
9696
f"expected exactly 1 cached entry for 3 calls on the same "
9797
f"expression; got {len(xorq_buckaroo._expr_count_cache)}. "
98-
f"The cache key must be id(expr)."
98+
f"The cache key must be the expression object itself."
9999
)
100100

101101
def test_expr_count_separate_expressions_cache_separately(self):
@@ -159,17 +159,17 @@ def count(self):
159159
stub = _StubExpr()
160160

161161
first = xorq_buckaroo._expr_count(stub)
162-
assert id(stub) not in xorq_buckaroo._expr_count_cache, (
162+
assert stub not in xorq_buckaroo._expr_count_cache, (
163163
"failed _expr_count call must not write to the cache; "
164-
f"found cached value {xorq_buckaroo._expr_count_cache.get(id(stub))!r}"
164+
f"found cached value {xorq_buckaroo._expr_count_cache.get(stub)!r}"
165165
)
166166

167167
second = xorq_buckaroo._expr_count(stub)
168168
assert second == 42, (
169169
f"after backend recovery, _expr_count must return the real "
170170
f"count, not a cached failure sentinel; got {second!r}"
171171
)
172-
assert xorq_buckaroo._expr_count_cache.get(id(stub)) == 42, (
172+
assert xorq_buckaroo._expr_count_cache.get(stub) == 42, (
173173
"successful call following a failure must populate the cache "
174174
"with the real count"
175175
)

0 commit comments

Comments
 (0)