Skip to content

Fix table UDF result block splitting - #18333

Open
ColinLeeo wants to merge 6 commits into
masterfrom
fix/table-udf-result-chunking
Open

Fix table UDF result block splitting#18333
ColinLeeo wants to merge 6 commits into
masterfrom
fix/table-udf-result-chunking

Conversation

@ColinLeeo

@ColinLeeo ColinLeeo commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Description

Problem

A table-model UDF can generate a large result while processing a partition, especially when one device contains many rows or the UDF expands each input row into multiple output rows. Previously, TableFunctionOperator could return the generated TsBlock without applying the configured TsBlock row-count and size targets to the final result.

The final result may also contain pass-through columns. Returning it as one large block increases memory pressure in the query pipeline and the risk that a downstream serialized block exceeds an RPC frame limit.

Design

TableFunctionOperator now splits each final result in buildTsBlock() after pass-through columns have been appended. Splitting at this point ensures that proper columns and pass-through columns consume the same block-size budget and remain aligned in every result fragment.

The implementation estimates the logical in-memory size of each result region using the same per-position accounting model as TsBlockBuilder:

  • the time column and fixed-width values are accounted for directly from their data types;
  • per-position null-state storage is included;
  • variable-width columns include their per-position shallow storage and the retained size of each non-null Binary value.

For blocks containing only fixed-width columns, the maximum position count is calculated directly from the per-position size, so no row scan is needed. For blocks containing variable-width columns, each position in those columns is inspected once because payload sizes can differ by row. This costs O(positionCount * variableWidthColumnCount), but it only reads null states, Binary references, and retained sizes. It does not serialize values or traverse/copy payload bytes.

The selected fragments are created with TsBlock.getRegion(), so they are views over the original columns rather than rebuilt row by row. Fragments are queued in their original order and returned by subsequent next() calls.

This deliberately uses an in-memory size estimate instead of serializing candidate regions. Serializing candidates would write the values into temporary buffers only for the exchange layer to serialize the selected fragments again. Rebuilding the result through a size-tracking TsBlockBuilder would avoid temporary serialization but would turn the UDF's batched column output into row-by-row, column-by-column copies.

The configured size is therefore a logical in-memory target, not an exact serialized-size guarantee. It reduces the risk of oversized serialized blocks without adding serialization work to the UDF execution path.

Configuration and corner cases

This PR introduces no new configuration. It uses:

  • max_tsblock_size_in_bytes
  • max_tsblock_line_number

Both constraints are applied to the final result after pass-through columns are appended. A row is indivisible; if one row alone exceeds max_tsblock_size_in_bytes, it is returned by itself so execution can continue. Result ordering and empty-output behavior are unchanged.

Tests

The unit tests cover:

  • variable-width results with and without pass-through columns;
  • fixed-width and variable-width columns sharing one size budget;
  • the configured row-count limit;
  • the estimated in-memory size of returned blocks;
  • total row count, ordering, values, and pass-through alignment.

The integration test configures a 1 KB TsBlock target and runs a table UDF that expands four input rows into 256 variable-width output rows. It verifies:

  • every expected (time, repeat_index) pair is returned;
  • no result row is duplicated;
  • payload values are complete;
  • pass-through columns, including null values, remain aligned across fragments.

The following targeted tests passed:

mvn test -pl iotdb-core/datanode -am \
  -Dtest=TableFunctionOperatorTest \
  -DfailIfNoTests=false \
  -Dsurefire.failIfNoSpecifiedTests=false

mvn verify -Drat.skip=true -DskipUTs \
  -Dit.test=IoTDBUserDefinedTableFunctionIT#testLargeResultIsSplitWithoutDataLoss \
  -DfailIfNoTests=false \
  -Dfailsafe.failIfNoSpecifiedTests=false \
  -pl integration-test -am \
  -PTableSimpleIT -P with-integration-tests

This PR has:

  • been self-reviewed.
  • added comments explaining non-obvious design decisions.
  • added or updated unit tests.
  • added an integration test.
  • been tested in a test IoTDB cluster.

Key changed/added classes
  • TableFunctionOperator
    • Splits final output blocks by row count and estimated logical in-memory size.
    • Applies splitting after pass-through columns are appended.
    • Uses fixed-width direct calculation and a single scan of variable-width values.
  • TableFunctionOperatorTest
    • Covers size-based result splitting with and without pass-through columns.
    • Verifies that fixed-width and variable-width columns share one size budget.
  • LargeResultTableFunction
    • Generates wide integration-test output.
  • IoTDBUserDefinedTableFunctionIT
    • Verifies end-to-end result completeness and pass-through alignment.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.66667% with 16 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@c13be2c). Learn more about missing BASE report.
⚠️ Report is 49 commits behind head on master.

Files with missing lines Patch % Lines
...erator/process/function/TableFunctionOperator.java 78.66% 16 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##             master   #18333   +/-   ##
=========================================
  Coverage          ?   43.45%           
  Complexity        ?      374           
=========================================
  Files             ?     5366           
  Lines             ?   383022           
  Branches          ?    49823           
=========================================
  Hits              ?   166428           
  Misses            ?   216594           
  Partials          ?        0           

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copilot AI left a comment

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.

Pull request overview

This PR addresses oversized TsBlock outputs from table-model UDFs by enforcing configured TsBlock size/row limits in TableFunctionOperator, preventing large blocks from propagating through the query pipeline and risking RPC frame overflows.

Changes:

  • Add final-result TsBlock splitting in TableFunctionOperator (after pass-through columns are appended) based on configured TsBlock constraints.
  • Add a unit test covering variable-width outputs with/without pass-through columns.
  • Add an integration-test UDF plus an IT validating large UDF results are returned without loss/duplication and with pass-through alignment under a small TsBlock limit.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/tvf/TableFunctionOperatorTest.java Adds unit coverage for splitting variable-width UDF output blocks with optional pass-through columns.
iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/TableFunctionOperator.java Implements final-result TsBlock splitting and adjusts operator completion/return-size reporting.
integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/udf/IoTDBUserDefinedTableFunctionIT.java Adds end-to-end validation that large table-UDF results are split without data loss and keep pass-through columns aligned.
integration-test/src/main/java/org/apache/iotdb/db/query/udf/example/relational/LargeResultTableFunction.java Introduces a table UDF used by the integration test to generate large variable-width outputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 64 to 65
private static final long INSTANCE_SIZE =
RamUsageEstimator.shallowSizeOfInstance(AggregationMergeSortOperator.class);
Comment on lines 423 to 426
@Override
public long calculateMaxReturnSize() {
return Math.max(DEFAULT_MAX_TSBLOCK_SIZE_IN_BYTES, properBlockBuilder.getRetainedSizeInBytes());
return maxTsBlockSizeInBytes;
}
Comment on lines +264 to +277
/**
* Splits the final result using the same logical in-memory size accounting as {@link
* TsBlockBuilder}.
*
* <p>Serializing candidate regions to find their exact sizes would write every value into
* temporary buffers, only for the exchange layer to serialize the selected regions again.
* Rebuilding the result with a size-tracking {@link TsBlockBuilder} would avoid that temporary
* serialization, but it would turn the UDF's batched column output into row-by-row,
* column-by-column copies.
*
* <p>Instead, fixed-width values are accounted for directly from their data types, while only the
* retained sizes of variable-width values are inspected. This deliberately estimates the
* in-memory TsBlock size rather than its serialized size because the two representations are not
* equivalent. The resulting regions are views over the original columns and do not copy their
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants