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

Copy link
Copy Markdown
Collaborator

Description

Problem

A table-model UDF can produce a large result block while processing a partition containing a large device. Previously, TableFunctionOperator could return the generated TsBlock without enforcing the configured row-count and serialized byte-size limits.

This could propagate an oversized block through the query pipeline and eventually exceed an RPC frame limit, such as the 64 MB Thrift frame limit.

Design

TableFunctionOperator now splits each final output block inside buildTsBlock(), after pass-through columns have been appended. Checking the final block is important because pass-through columns contribute to the serialized result size.

For every output fragment, the operator:

  1. Caps the candidate row count using max_tsblock_line_number.
  2. Serializes the candidate region and compares its exact byte size with max_tsblock_size_in_bytes.
  3. Uses binary search to find the largest row prefix that fits when the candidate is oversized.
  4. Queues all fragments in their original order for subsequent next() calls.

Binary search handles variable-width values such as TEXT, STRING, and BLOB without relying on an average row-size estimate. The size check uses TsBlockSerde because the serialized size is the relevant boundary for blocks sent through the query pipeline.

The splitting logic is kept in TableFunctionOperator instead of inheriting the retained-block logic from AbstractOperator. This avoids maintaining two result-fragmentation paths and ensures that proper columns and pass-through columns are always split together.

Configuration and corner cases

This PR introduces no new configuration. It uses:

  • max_tsblock_size_in_bytes
  • max_tsblock_line_number

Rows are indivisible. If a single row is larger than max_tsblock_size_in_bytes, it is returned alone so execution can continue. Result ordering and empty-output behavior are unchanged.

Tests

The unit test covers both output forms:

  • proper columns only
  • proper columns with pass-through columns

It verifies the row-count limit, exact serialized byte-size limit, total row count, ordering, values, and pass-through alignment.

The integration test configures a 1 KB TsBlock limit 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 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 exact serialized size.
    • Applies splitting after pass-through columns are appended.
  • TableFunctionOperatorTest
    • Covers variable-width results with and without pass-through columns.
  • 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.

2 participants