Skip to content

feat(python): give a port that carried no rows its declared columns - #8488

Open
kz930 wants to merge 1 commit into
apache:mainfrom
kz930:upstream-empty-table
Open

feat(python): give a port that carried no rows its declared columns#8488
kz930 wants to merge 1 commit into
apache:mainfrom
kz930:upstream-empty-table

Conversation

@kz930

@kz930 kz930 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this PR?

The runtime now tells an executor what each input port was declared to carry, and TableOperator falls back to that when a port finishes with no rows.

Operator gains an input_schemas mapping, keyed by port index. DataProcessor writes the finishing port's schema into it just before calling on_finish, reading it from the input manager's own port; a source has no input port to ask, so it is left alone. TableOperator.on_finish uses it only when there are no tuples to read column names off, which is the only case where the tuples do not already say the same thing. Table.empty_of builds that frame through Arrow, so each column carries the dtype it would have had with rows in it rather than object.

Any related issues, documentation, discussions?

Found while building #8325, but not part of it: the bug is in the engine, not in
the export. The export's verification is what surfaced it, by running each
operator on a table with no rows, but it reaches Sort, the visualization
operators and any user-written UDFTableOperator in an ordinary run.

Closes #8487, the bug this change is the whole of.

How was this PR tested?

A new case in TestTableOperator, test_on_finish_with_no_rows_keeps_the_declared_columns, gives a port an INTEGER and a STRING column, finishes it with no rows, and asserts the operator receives both columns, an empty frame, and int32 for the integer one. Removing the fallback turns it red on the missing columns.

The rest of amber/src/test/python/core passes unchanged, 1107 tests. The one failure is test_iceberg_rest_catalog_integration, which needs a running catalog and fails the same way without this change.

Was this PR authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Claude Opus 5)

🤖 Generated with Claude Code

A port can finish having carried no rows: an upstream filter that matches
nothing still ends its channel, and DataProcessor calls on_finish either
way. TableOperator then built its table out of no tuples, and since the
column names are read off the tuples, the operator was handed a frame of
no columns at all. Every table operator that names one of its own columns
raised KeyError on it.

The runtime now records what each input port was declared to carry, and
the table falls back to it where the tuples cannot say.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Automated Reviewer Suggestions

Based on the git blame history of the changed files, we recommend the following reviewers:

  • Contributors with relevant context: @aglinxinyuan, @eugenegujing
    You can notify them by mentioning @aglinxinyuan, @eugenegujing in a comment.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.69%. Comparing base (1fbd346) to head (2004373).

Additional details and impacted files
@@            Coverage Diff            @@
##               main    #8488   +/-   ##
=========================================
  Coverage     93.69%   93.69%           
  Complexity     4826     4826           
=========================================
  Files          1209     1209           
  Lines         49871    49890   +19     
  Branches       6099     6101    +2     
=========================================
+ Hits          46727    46746   +19     
  Misses         1652     1652           
  Partials       1492     1492           
Flag Coverage Δ *Carryforward flag
access-control-service 80.18% <ø> (ø) Carriedforward from 1fbd346
agent-service 99.32% <ø> (ø) Carriedforward from 1fbd346
amber 89.91% <ø> (ø) Carriedforward from 1fbd346
computing-unit-managing-service 77.14% <ø> (ø) Carriedforward from 1fbd346
config-service 87.12% <ø> (ø) Carriedforward from 1fbd346
file-service 83.65% <ø> (ø) Carriedforward from 1fbd346
frontend 96.16% <ø> (ø) Carriedforward from 1fbd346
notebook-migration-service 83.73% <ø> (ø) Carriedforward from 1fbd346
pyamber 98.48% <100.00%> (+<0.01%) ⬆️
workflow-compiling-service 77.19% <ø> (ø) Carriedforward from 1fbd346

*This pull request uses carry forward flags. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Benchmark changes need a look

🟢 2 better · 🔴 4 worse · ⚪ 9 noise (<±5%) · 0 without baseline

Compared against main 1fbd346 benchmarked on this same runner, so the delta is largely free of cross-runner hardware noise. The "7d avg" column still reflects the gh-pages dashboard. Treat <±5% as noise unless repeated.

Dashboard · Run

config throughput MB/s latency max Δ latest / 7d
🔴 bs=10 sw=10 sl=64 345 0.21 27,914/38,715/38,715 us 🔴 +20.0% / 🔴 +140.0%
🔴 bs=100 sw=10 sl=64 774 0.472 128,505/153,463/153,463 us 🟢 -7.5% / 🔴 +41.2%
bs=1000 sw=10 sl=64 920 0.561 1,091,359/1,141,539/1,141,539 us ⚪ within ±5% / 🔴 +10.0%
Baseline details

Latest main 1fbd346 from same runner

config metric PR latest main 7d avg Δ latest Δ 7d
bs=10 sw=10 sl=64 throughput 345 tuples/sec 388 tuples/sec 755.28 tuples/sec -11.1% -54.3%
bs=10 sw=10 sl=64 MB/s 0.21 MB/s 0.237 MB/s 0.461 MB/s -11.4% -54.4%
bs=10 sw=10 sl=64 p50 27,914 us 23,270 us 12,957 us +20.0% +115.4%
bs=10 sw=10 sl=64 p95 38,715 us 39,438 us 16,134 us -1.8% +140.0%
bs=10 sw=10 sl=64 p99 38,715 us 39,438 us 20,333 us -1.8% +90.4%
bs=100 sw=10 sl=64 throughput 774 tuples/sec 806 tuples/sec 980.1 tuples/sec -4.0% -21.0%
bs=100 sw=10 sl=64 MB/s 0.472 MB/s 0.492 MB/s 0.598 MB/s -4.1% -21.1%
bs=100 sw=10 sl=64 p50 128,505 us 120,490 us 101,894 us +6.7% +26.1%
bs=100 sw=10 sl=64 p95 153,463 us 165,976 us 108,718 us -7.5% +41.2%
bs=100 sw=10 sl=64 p99 153,463 us 165,976 us 122,482 us -7.5% +25.3%
bs=1000 sw=10 sl=64 throughput 920 tuples/sec 915 tuples/sec 1,011 tuples/sec +0.5% -9.0%
bs=1000 sw=10 sl=64 MB/s 0.561 MB/s 0.558 MB/s 0.617 MB/s +0.5% -9.1%
bs=1000 sw=10 sl=64 p50 1,091,359 us 1,090,198 us 996,422 us +0.1% +9.5%
bs=1000 sw=10 sl=64 p95 1,141,539 us 1,152,241 us 1,037,670 us -0.9% +10.0%
bs=1000 sw=10 sl=64 p99 1,141,539 us 1,152,241 us 1,072,152 us -0.9% +6.5%
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,580.06,200,128000,345,0.210,27914.10,38714.64,38714.64
1,100,10,64,20,2583.98,2000,1280000,774,0.472,128504.72,153463.06,153463.06
2,1000,10,64,20,21742.96,20000,12800000,920,0.561,1091359.37,1141539.18,1141539.18

@kz930

kz930 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@carloea2 this one is a platform bug rather than an export change: a port that finishes having carried no rows hands a table operator a frame with no columns at all, so every operator naming one of its own columns raises KeyError. Would you take a look when you have a moment?

table. Building it through Arrow gives each column the dtype it would
have had with rows in it.
"""
return pa.Table.from_pylist([], schema=schema.as_arrow_schema()).to_pandas()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this return a Table rather than a plain DataFrame? A TableOperator UDF using yield from table.as_tuples() now raises AttributeError when its input is empty and has a declared schema. The same UDF still works with populated input and with the previous empty-input path. I reproduced this against 2004373 with three local pytest cases: those two controls pass, while the declared empty input fails. Wrapping the frame in Table would preserve the existing UDF API while keeping the columns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A table operator handed a port that carried no rows loses every column

3 participants