Skip to content

Commit 1126c6a

Browse files
timsaucerclaude
andcommitted
Make the tests fail for the implementations they claim to catch
Each of these passed with the defect it existed to catch: - The projection round trip never checked the projection was applied; a decoder that parsed it and passed None through produced the same values, just with extra columns. The schema is now asserted. - Nothing exercised the limit half of the storage descriptor in either direction. A new test executes the restored scan node per partition, because the limit operator above the scan would mask a dropped one. - The bundle-reuse test compared two session codec ids, which differ with no bundle installed at all. It now round-trips a plan through the second session and pins the decode counter. - The decline test offered the codec nothing: a registered name is resolved by the registry before any codec is consulted. The foreign name is now manufactured by a length-preserving edit of the encoded plan, and the declined counter is asserted. - aggregate_by_name and window_by_name, and the whole logical codec, ran in no test. Two new workers decode the aggregate and window function with nothing registered, and a receiver with only the logical codec installed rebuilds the scalar from a logical plan. - The by-value pickle claim was pinned on one side only; the payload now has a size floor to match the by-reference cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 0aa89b4 commit 1126c6a

3 files changed

Lines changed: 159 additions & 10 deletions

File tree

‎examples/distributed/engine-library/python/tests/_test_three_libraries.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ def bucket(prices: pa.Array) -> pa.Array:
145145
assert stage is not None
146146
# The callable itself is in the bytes, under the scalar-UDF family prefix.
147147
assert b"DFPYUDF" in stage.to_bytes(ctx)
148+
# By value means the payload carries bytecode, not a two-word pointer --
149+
# the other half of the size claim pinned at the by-reference test's
150+
# `< 200`. If cloudpickle ever started resolving `<locals>` functions by
151+
# reference, this is the line that would notice.
152+
assert len(cloudpickle.dumps(bucket)) > 500
148153

149154
result = run_distributed(sql, spec, extra_udfs=[price_bucket])
150155
# Prices run from one hundred to eight hundred, so four exceed four hundred.

‎examples/distributed/storage-library/python/tests/_test_portable_codec.py‎

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,52 @@ def test_a_projection_survives_the_round_trip(readings_dir: pathlib.Path) -> Non
131131
plan = ctx.sql("select reading from readings").execution_plan()
132132
restored = ExecutionPlan.from_bytes(ctx, plan.to_bytes(ctx))
133133

134-
rows = [
135-
value
134+
batches = [
135+
batch.to_pyarrow()
136136
for partition in range(restored.partition_count)
137137
for batch in ctx.execute(restored, partition)
138-
for value in batch.to_pyarrow().column("reading").to_pylist()
139138
]
140-
assert sorted(rows) == [1.5, 1.5, 1.5, 2.5, 2.5, 2.5, 3.5, 3.5, 3.5]
139+
# Exactly one column, not merely the right values in it: a decoder that
140+
# parsed the projection and then dropped it would still produce every
141+
# `reading` value below, just alongside the columns that were projected
142+
# away. The schema is where that shows.
143+
assert [batch.column_names for batch in batches] == [["reading"]] * 3
144+
rows = sorted(
145+
value for batch in batches for value in batch.column("reading").to_pylist()
146+
)
147+
assert rows == [1.5, 1.5, 1.5, 2.5, 2.5, 2.5, 3.5, 3.5, 3.5]
148+
149+
150+
def _scan_of(plan: ExecutionPlan) -> ExecutionPlan:
151+
"""Descend to the leaf, which for these plans is this library's scan."""
152+
children = plan.children()
153+
return _scan_of(children[0]) if children else plan
154+
155+
156+
def test_a_limit_survives_the_round_trip(readings_dir: pathlib.Path) -> None:
157+
"""The limit is part of the descriptor too, and it changes what is read.
158+
159+
Asserted on the scan node itself rather than on the whole plan: the
160+
logical `PushDownLimit` rule keeps a limit operator *above* the scan, and
161+
that operator would trim the result to the right count even if the decoder
162+
dropped the scan's own limit. Executing the restored scan directly is what
163+
makes a dropped limit visible -- three rows per file instead of two.
164+
"""
165+
ctx, _ = _configured(readings_dir)
166+
plan = ctx.sql("select reading from readings limit 2").execution_plan()
167+
scan = _scan_of(plan)
168+
blob = scan.to_bytes(ctx)
169+
assert b'"limit":2' in blob
170+
171+
restored = ExecutionPlan.from_bytes(ctx, blob)
172+
per_partition = [
173+
sum(batch.to_pyarrow().num_rows for batch in ctx.execute(restored, partition))
174+
for partition in range(restored.partition_count)
175+
]
176+
# The limit is applied per partition -- each file stops after two of its
177+
# three rows. The trim to two rows overall is the limit operator's job,
178+
# and that operator is deliberately not part of what was serialized here.
179+
assert per_partition == [2, 2, 2]
141180

142181

143182
def test_stock_nodes_never_reach_this_codec(readings_dir: pathlib.Path) -> None:
@@ -343,4 +382,13 @@ def test_the_bundle_is_reusable_across_sessions(readings_dir: pathlib.Path) -> N
343382
== 9
344383
)
345384

346-
assert first.__datafusion_codec_id__ != second.__datafusion_codec_id__
385+
# The second install has to carry live codecs of its own, not a component
386+
# cached from the first -- so the proof is a round trip through `second`
387+
# specifically, not a property of the session objects.
388+
assert bundle.decode_calls() == 0
389+
blob = (
390+
second.sql("select sensor_id from readings").execution_plan().to_bytes(second)
391+
)
392+
restored = ExecutionPlan.from_bytes(second, blob)
393+
assert restored.partition_count == 3
394+
assert bundle.decode_calls() == 1

‎examples/distributed/udf-library/python/tests/_test_udfs.py‎

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import pytest
2929
from datafusion import SessionConfig, SessionContext, udaf, udf, udwf
30-
from datafusion.plan import ExecutionPlan
30+
from datafusion.plan import ExecutionPlan, LogicalPlan
3131
from dfx_udfs import (
3232
CodecObservations,
3333
NetRevenueUDF,
@@ -184,6 +184,10 @@ def test_the_payload_carries_no_bytes(lineitem: pathlib.Path) -> None:
184184
)
185185

186186

187+
def _decoded(stdout: str) -> int:
188+
return int(re.search(r"decoded=(\d+)", stdout).group(1))
189+
190+
187191
def _run_worker(
188192
tmp_path: pathlib.Path, blob: bytes, mode: str, data: pathlib.Path
189193
) -> subprocess.CompletedProcess[str]:
@@ -252,23 +256,115 @@ def test_a_worker_with_neither_names_the_function_it_cannot_find(
252256
assert "dfx_net_revenue" in result.stderr
253257

254258

259+
def test_a_worker_with_only_the_codec_rebuilds_the_aggregate(
260+
lineitem: pathlib.Path, tmp_path: pathlib.Path
261+
) -> None:
262+
"""The aggregate travels by name too, through `aggregate_by_name`.
263+
264+
Worth its own worker: the scalar tests exercise `scalar_by_name` and
265+
nothing else, so a typo in the aggregate's decode arm would only ever
266+
surface here.
267+
"""
268+
ctx, _ = _session(lineitem)
269+
blob = (
270+
ctx.sql("select dfx_weighted_avg(l_extendedprice, l_quantity) from lineitem")
271+
.execution_plan()
272+
.to_bytes(ctx)
273+
)
274+
275+
result = _run_worker(tmp_path, blob, "codec", lineitem)
276+
277+
assert result.returncode == 0, result.stderr
278+
# (100*1 + 200*3 + 400*4) / (1 + 3 + 4), same as the in-process test.
279+
assert "total=287.5" in result.stdout
280+
# More than one decode is fine -- the plan holds the function once per
281+
# aggregate phase -- but zero would mean the registry answered, and this
282+
# worker has nothing registered.
283+
assert _decoded(result.stdout) >= 1
284+
285+
286+
def test_a_worker_with_only_the_codec_rebuilds_the_window_function(
287+
lineitem: pathlib.Path, tmp_path: pathlib.Path
288+
) -> None:
289+
"""And the window function, through `window_by_name`."""
290+
ctx, _ = _session(lineitem)
291+
blob = (
292+
ctx.sql(
293+
"select dfx_revenue_rank() over (order by l_extendedprice desc) as rnk "
294+
"from lineitem"
295+
)
296+
.execution_plan()
297+
.to_bytes(ctx)
298+
)
299+
300+
result = _run_worker(tmp_path, blob, "codec", lineitem)
301+
302+
assert result.returncode == 0, result.stderr
303+
# Ranks 1 + 2 + 3 over the three rows.
304+
assert "total=6.0" in result.stdout
305+
assert _decoded(result.stdout) >= 1
306+
307+
255308
def test_the_codec_declines_names_it_does_not_own(lineitem: pathlib.Path) -> None:
256309
"""A name-only payload reaches every codec, so declining matters.
257310
258311
With no bytes there is no codec id to route on. A codec that answered for
259312
any name it was handed would hijack another library's functions.
313+
314+
The foreign name is manufactured by editing the encoded plan: the session's
315+
own registry is consulted before any codec, so a name that is actually
316+
registered anywhere -- a built-in, say -- never reaches a codec at all.
317+
The replacement keeps the byte length, because the name sits inside
318+
length-delimited protobuf fields.
260319
"""
261320
ctx, observations = _session(lineitem, with_codecs=True)
262-
# `abs` is a built-in, so the plan references a name this library does not
263-
# own; decoding offers it around.
264321
blob = (
265-
ctx.sql("select abs(l_discount) from lineitem").execution_plan().to_bytes(ctx)
322+
ctx.sql(
323+
"select dfx_net_revenue(l_extendedprice, l_discount, l_tax) from lineitem"
324+
)
325+
.execution_plan()
326+
.to_bytes(ctx)
266327
)
267-
ExecutionPlan.from_bytes(ctx, blob)
328+
mangled = blob.replace(b"dfx_net_revenue", b"dfx_not_revenue")
329+
assert len(mangled) == len(blob), "the edit has to preserve the protobuf framing"
268330

331+
with pytest.raises(Exception, match="dfx_not_revenue"):
332+
ExecutionPlan.from_bytes(ctx, mangled)
333+
334+
# The codec was offered the foreign name and said no; it never rebuilt
335+
# anything. A codec that hijacked the name would show up as the mirror
336+
# image -- a decode, no decline, and a query that "works".
337+
assert observations.declined_calls() >= 1
269338
assert observations.decode_calls() == 0
270339

271340

341+
def test_the_logical_codec_rebuilds_functions_by_name(lineitem: pathlib.Path) -> None:
342+
"""The logical half is a peer, not a passenger.
343+
344+
`LogicalPlan.to_bytes` is the layer an engine that ships *logical* plans
345+
exercises, and nothing about the physical tests touches it. The receiving
346+
session installs only the logical codec and registers nothing, so the
347+
rebuild below can only have come through `try_decode_udf` on this codec.
348+
"""
349+
ctx, _ = _session(lineitem)
350+
blob = (
351+
ctx.sql(
352+
"select dfx_net_revenue(l_extendedprice, l_discount, l_tax) from lineitem"
353+
)
354+
.logical_plan()
355+
.to_bytes(ctx)
356+
)
357+
358+
receiver_observations = CodecObservations()
359+
receiver = SessionContext().with_logical_extension_codec(
360+
receiver_observations.logical_codec()
361+
)
362+
restored = LogicalPlan.from_bytes(receiver, blob)
363+
364+
assert receiver_observations.decode_calls() == 1
365+
assert "dfx_net_revenue" in restored.display_indent()
366+
367+
272368
def test_the_codec_ids_are_pinned() -> None:
273369
"""Renaming the exporting class must not invalidate written plans."""
274370
observations = CodecObservations()

0 commit comments

Comments
 (0)