This is an automated email from the ASF dual-hosted git repository.

ColinLeeo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 9fe55fce9a9 Fix table UDF result block splitting (#18333)
9fe55fce9a9 is described below

commit 9fe55fce9a933c9d4ff25d3ce10f5d8271107ca4
Author: Colin Lee <[email protected]>
AuthorDate: Thu Aug 6 11:11:05 2026 +0800

    Fix table UDF result block splitting (#18333)
    
    * Fix table UDF result block splitting
    
    * Fix table UDF result splitting after pass-through
    
    * Optimize table UDF result block splitting
    
    * Estimate table UDF result blocks by memory size
    
    * Clarify table UDF result size accounting
    
    * Remove redundant table UDF split condition
    
    * Reuse AbstractOperator for table UDF result splitting
    
    * Harden table UDF result splitting lifecycle
    
    * Remove redundant table UDF type list copy
    
    * Clear partition cache on close
---
 .../relational/LargeResultTableFunction.java       | 136 +++++++++
 .../db/it/udf/IoTDBUserDefinedTableFunctionIT.java |  80 +++++
 .../process/function/TableFunctionOperator.java    |  51 ++--
 .../process/function/partition/PartitionCache.java |   2 +-
 .../process/tvf/TableFunctionOperatorTest.java     | 331 +++++++++++++++++++++
 5 files changed, 576 insertions(+), 24 deletions(-)

diff --git 
a/integration-test/src/main/java/org/apache/iotdb/db/query/udf/example/relational/LargeResultTableFunction.java
 
b/integration-test/src/main/java/org/apache/iotdb/db/query/udf/example/relational/LargeResultTableFunction.java
new file mode 100644
index 00000000000..4a032af5901
--- /dev/null
+++ 
b/integration-test/src/main/java/org/apache/iotdb/db/query/udf/example/relational/LargeResultTableFunction.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.db.query.udf.example.relational;
+
+import org.apache.iotdb.udf.api.exception.UDFException;
+import org.apache.iotdb.udf.api.relational.TableFunction;
+import org.apache.iotdb.udf.api.relational.access.Record;
+import org.apache.iotdb.udf.api.relational.table.MapTableFunctionHandle;
+import org.apache.iotdb.udf.api.relational.table.TableFunctionAnalysis;
+import org.apache.iotdb.udf.api.relational.table.TableFunctionHandle;
+import 
org.apache.iotdb.udf.api.relational.table.TableFunctionProcessorProvider;
+import org.apache.iotdb.udf.api.relational.table.argument.Argument;
+import org.apache.iotdb.udf.api.relational.table.argument.DescribedSchema;
+import org.apache.iotdb.udf.api.relational.table.argument.ScalarArgument;
+import 
org.apache.iotdb.udf.api.relational.table.processor.TableFunctionDataProcessor;
+import 
org.apache.iotdb.udf.api.relational.table.specification.ParameterSpecification;
+import 
org.apache.iotdb.udf.api.relational.table.specification.ScalarParameterSpecification;
+import 
org.apache.iotdb.udf.api.relational.table.specification.TableParameterSpecification;
+import org.apache.iotdb.udf.api.type.Type;
+
+import org.apache.tsfile.block.column.ColumnBuilder;
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.utils.Binary;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class LargeResultTableFunction implements TableFunction {
+
+  private static final String TABLE_PARAMETER_NAME = "DATA";
+  private static final String REPEAT_COUNT_PARAMETER_NAME = "REPEAT_COUNT";
+  private static final String PAYLOAD_SIZE_PARAMETER_NAME = "PAYLOAD_SIZE";
+
+  @Override
+  public List<ParameterSpecification> getArgumentsSpecifications() {
+    return Arrays.asList(
+        TableParameterSpecification.builder()
+            .name(TABLE_PARAMETER_NAME)
+            .rowSemantics()
+            .passThroughColumns()
+            .build(),
+        ScalarParameterSpecification.builder()
+            .name(REPEAT_COUNT_PARAMETER_NAME)
+            .type(Type.INT32)
+            .build(),
+        ScalarParameterSpecification.builder()
+            .name(PAYLOAD_SIZE_PARAMETER_NAME)
+            .type(Type.INT32)
+            .build());
+  }
+
+  @Override
+  public TableFunctionAnalysis analyze(Map<String, Argument> arguments) throws 
UDFException {
+    MapTableFunctionHandle handle =
+        new MapTableFunctionHandle.Builder()
+            .addProperty(
+                REPEAT_COUNT_PARAMETER_NAME,
+                ((ScalarArgument) 
arguments.get(REPEAT_COUNT_PARAMETER_NAME)).getValue())
+            .addProperty(
+                PAYLOAD_SIZE_PARAMETER_NAME,
+                ((ScalarArgument) 
arguments.get(PAYLOAD_SIZE_PARAMETER_NAME)).getValue())
+            .build();
+    return TableFunctionAnalysis.builder()
+        .properColumnSchema(
+            DescribedSchema.builder()
+                .addField("repeat_index", Type.INT32)
+                .addField("payload", Type.STRING)
+                .build())
+        .requiredColumns(TABLE_PARAMETER_NAME, Collections.singletonList(0))
+        .handle(handle)
+        .build();
+  }
+
+  @Override
+  public TableFunctionHandle createTableFunctionHandle() {
+    return new MapTableFunctionHandle();
+  }
+
+  @Override
+  public TableFunctionProcessorProvider getProcessorProvider(
+      TableFunctionHandle tableFunctionHandle) {
+    return new TableFunctionProcessorProvider() {
+      @Override
+      public TableFunctionDataProcessor getDataProcessor() {
+        return new TableFunctionDataProcessor() {
+          private final int repeatCount =
+              (int)
+                  ((MapTableFunctionHandle) tableFunctionHandle)
+                      .getProperty(REPEAT_COUNT_PARAMETER_NAME);
+          private final String payloadSuffix =
+              "x"
+                  .repeat(
+                      (int)
+                          ((MapTableFunctionHandle) tableFunctionHandle)
+                              .getProperty(PAYLOAD_SIZE_PARAMETER_NAME));
+          private long recordIndex;
+
+          @Override
+          public void process(
+              Record input,
+              List<ColumnBuilder> properColumnBuilders,
+              ColumnBuilder passThroughIndexBuilder) {
+            for (int repeatIndex = 0; repeatIndex < repeatCount; 
repeatIndex++) {
+              properColumnBuilders.get(0).writeInt(repeatIndex);
+              properColumnBuilders
+                  .get(1)
+                  .writeBinary(
+                      new Binary(repeatIndex + ":" + payloadSuffix, 
TSFileConfig.STRING_CHARSET));
+              passThroughIndexBuilder.writeLong(recordIndex);
+            }
+            recordIndex++;
+          }
+        };
+      }
+    };
+  }
+}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/udf/IoTDBUserDefinedTableFunctionIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/udf/IoTDBUserDefinedTableFunctionIT.java
index 7fe6648fe70..9db3acb13e0 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/udf/IoTDBUserDefinedTableFunctionIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/relational/it/db/it/udf/IoTDBUserDefinedTableFunctionIT.java
@@ -26,13 +26,18 @@ import 
org.apache.iotdb.itbase.category.TableLocalStandaloneIT;
 
 import org.junit.After;
 import org.junit.AfterClass;
+import org.junit.Assert;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.experimental.categories.Category;
 import org.junit.runner.RunWith;
 
 import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.Statement;
+import java.util.HashSet;
+import java.util.Set;
 
 import static org.apache.iotdb.db.it.utils.TestUtils.tableAssertTestFail;
 import static org.apache.iotdb.db.it.utils.TestUtils.tableResultSetEqualTest;
@@ -42,6 +47,9 @@ import static org.junit.Assert.fail;
 @Category({TableLocalStandaloneIT.class, TableClusterIT.class})
 public class IoTDBUserDefinedTableFunctionIT {
   private static final String DATABASE_NAME = "test";
+  private static final int MAX_TSBLOCK_SIZE_IN_BYTES = 1024;
+  private static final int LARGE_RESULT_REPEAT_COUNT = 64;
+  private static final int LARGE_RESULT_PAYLOAD_SIZE = 128;
   private static final String[] sqls =
       new String[] {
         "CREATE DATABASE " + DATABASE_NAME,
@@ -57,6 +65,10 @@ public class IoTDBUserDefinedTableFunctionIT {
 
   @BeforeClass
   public static void setUp() throws Exception {
+    EnvFactory.getEnv()
+        .getConfig()
+        .getDataNodeCommonConfig()
+        .setMaxTsBlockSizeInByte(MAX_TSBLOCK_SIZE_IN_BYTES);
     EnvFactory.getEnv().initClusterEnvironment();
     insertData();
   }
@@ -182,6 +194,74 @@ public class IoTDBUserDefinedTableFunctionIT {
         DATABASE_NAME);
   }
 
+  @Test
+  public void testLargeResultIsSplitWithoutDataLoss() throws Exception {
+    SQLFunctionUtils.createUDF(
+        "large_result",
+        
"org.apache.iotdb.db.query.udf.example.relational.LargeResultTableFunction");
+
+    Set<String> returnedRows = new HashSet<>();
+    try (Connection connection = EnvFactory.getEnv().getTableConnection();
+        Statement statement = connection.createStatement()) {
+      statement.execute("USE " + DATABASE_NAME);
+      try (ResultSet resultSet =
+          statement.executeQuery(
+              "SELECT * FROM large_result(vehicle, "
+                  + LARGE_RESULT_REPEAT_COUNT
+                  + ", "
+                  + LARGE_RESULT_PAYLOAD_SIZE
+                  + ")")) {
+        while (resultSet.next()) {
+          int repeatIndex = resultSet.getInt("repeat_index");
+          long time = resultSet.getLong("time");
+          Assert.assertTrue(repeatIndex >= 0 && repeatIndex < 
LARGE_RESULT_REPEAT_COUNT);
+          Assert.assertEquals(
+              repeatIndex + ":" + "x".repeat(LARGE_RESULT_PAYLOAD_SIZE),
+              resultSet.getString("payload"));
+          assertPassThroughColumns(resultSet, time);
+          Assert.assertTrue(
+              "Duplicate result row for time " + time + " and repeat index " + 
repeatIndex,
+              returnedRows.add(time + ":" + repeatIndex));
+        }
+      }
+    }
+
+    long[] inputTimes = new long[] {1, 2, 3, 5};
+    Assert.assertEquals(inputTimes.length * LARGE_RESULT_REPEAT_COUNT, 
returnedRows.size());
+    for (long time : inputTimes) {
+      for (int repeatIndex = 0; repeatIndex < LARGE_RESULT_REPEAT_COUNT; 
repeatIndex++) {
+        Assert.assertTrue(returnedRows.contains(time + ":" + repeatIndex));
+      }
+    }
+  }
+
+  private static void assertPassThroughColumns(ResultSet resultSet, long time) 
throws SQLException {
+    switch ((int) time) {
+      case 1:
+        Assert.assertEquals("d0", resultSet.getString("device_id"));
+        Assert.assertEquals(1, resultSet.getInt("s1"));
+        Assert.assertEquals(1, resultSet.getLong("s2"));
+        break;
+      case 2:
+        Assert.assertEquals("d0", resultSet.getString("device_id"));
+        Assert.assertNull(resultSet.getObject("s1"));
+        Assert.assertEquals(2, resultSet.getLong("s2"));
+        break;
+      case 3:
+        Assert.assertEquals("d0", resultSet.getString("device_id"));
+        Assert.assertEquals(3, resultSet.getInt("s1"));
+        Assert.assertEquals(3, resultSet.getLong("s2"));
+        break;
+      case 5:
+        Assert.assertEquals("d1", resultSet.getString("device_id"));
+        Assert.assertEquals(4, resultSet.getInt("s1"));
+        Assert.assertNull(resultSet.getObject("s2"));
+        break;
+      default:
+        Assert.fail("Unexpected pass-through time: " + time);
+    }
+  }
+
   @Test
   public void testHybrid() {
     SQLFunctionUtils.createUDF(
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/TableFunctionOperator.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/TableFunctionOperator.java
index d4bc8a0da4c..fb9b90c9f60 100644
--- 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/TableFunctionOperator.java
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/TableFunctionOperator.java
@@ -19,9 +19,9 @@
 
 package org.apache.iotdb.calc.execution.operator.process.function;
 
+import org.apache.iotdb.calc.execution.operator.AbstractOperator;
 import org.apache.iotdb.calc.execution.operator.CommonOperatorContext;
 import org.apache.iotdb.calc.execution.operator.Operator;
-import 
org.apache.iotdb.calc.execution.operator.process.AggregationMergeSortOperator;
 import org.apache.iotdb.calc.execution.operator.process.ProcessOperator;
 import 
org.apache.iotdb.calc.execution.operator.process.function.partition.PartitionCache;
 import 
org.apache.iotdb.calc.execution.operator.process.function.partition.PartitionState;
@@ -36,7 +36,6 @@ import 
org.apache.iotdb.udf.api.relational.table.processor.TableFunctionDataProc
 import com.google.common.util.concurrent.ListenableFuture;
 import org.apache.tsfile.block.column.Column;
 import org.apache.tsfile.block.column.ColumnBuilder;
-import org.apache.tsfile.common.conf.TSFileDescriptor;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.read.common.block.TsBlock;
 import org.apache.tsfile.read.common.block.TsBlockBuilder;
@@ -57,15 +56,11 @@ import java.util.Queue;
 import static com.google.common.base.Preconditions.checkArgument;
 
 // only one input source is supported now
-public class TableFunctionOperator implements ProcessOperator {
+public class TableFunctionOperator extends AbstractOperator implements 
ProcessOperator {
 
   private static final long INSTANCE_SIZE =
-      
RamUsageEstimator.shallowSizeOfInstance(AggregationMergeSortOperator.class);
+      RamUsageEstimator.shallowSizeOfInstance(TableFunctionOperator.class);
 
-  private static final int DEFAULT_MAX_TSBLOCK_SIZE_IN_BYTES =
-      TSFileDescriptor.getInstance().getConfig().getMaxTsBlockSizeInBytes();
-
-  private final CommonOperatorContext operatorContext;
   private final Operator inputOperator;
   private final TableFunctionProcessorProvider processorProvider;
   private final PartitionRecognizer partitionRecognizer;
@@ -115,11 +110,6 @@ public class TableFunctionOperator implements 
ProcessOperator {
     this.ioTDBLocal = ioTDBLocal;
   }
 
-  @Override
-  public CommonOperatorContext getOperatorContext() {
-    return this.operatorContext;
-  }
-
   @Override
   public ListenableFuture<?> isBlocked() {
     if (isBlocked == null) {
@@ -148,8 +138,8 @@ public class TableFunctionOperator implements 
ProcessOperator {
 
   @Override
   public TsBlock next() throws Exception {
-    if (!resultTsBlocks.isEmpty()) {
-      return resultTsBlocks.poll();
+    if (retainedTsBlock != null || !resultTsBlocks.isEmpty()) {
+      return getNextResultTsBlock();
     }
     if (partitionState == null) {
       partitionState = partitionRecognizer.nextState();
@@ -172,7 +162,7 @@ public class TableFunctionOperator implements 
ProcessOperator {
         resultTsBlocks.addAll(buildTsBlock(properColumnBuilders, 
passThroughIndexBuilder));
         partitionCache.clear();
         consumeCurrentPartitionState();
-        return resultTsBlocks.poll();
+        return getNextResultTsBlock();
       }
       if (stateType == PartitionState.StateType.NEW_PARTITION) {
         if (processor != null) {
@@ -182,7 +172,7 @@ public class TableFunctionOperator implements 
ProcessOperator {
           partitionCache.clear();
           destroyProcessor(processor);
           processor = null;
-          return resultTsBlocks.poll();
+          return getNextResultTsBlock();
         } else {
           processor = processorProvider.getDataProcessor();
           processor.beforeStart(ioTDBLocal);
@@ -196,8 +186,21 @@ public class TableFunctionOperator implements 
ProcessOperator {
       }
       consumeCurrentPartitionState();
       resultTsBlocks.addAll(buildTsBlock(properColumnBuilders, 
passThroughIndexBuilder));
-      return resultTsBlocks.poll();
+      return getNextResultTsBlock();
+    }
+  }
+
+  /**
+   * Applies {@link AbstractOperator}'s low-cost row-count splitting after 
pass-through columns have
+   * been appended. The configured byte size is a logical target rather than 
an exact serialized
+   * limit; in particular, a single oversized row and highly variable binary 
payloads may exceed it.
+   */
+  private TsBlock getNextResultTsBlock() {
+    if (retainedTsBlock != null) {
+      return getResultFromRetainedTsBlock();
     }
+    resultTsBlock = resultTsBlocks.poll();
+    return resultTsBlock == null ? null : checkTsBlockSizeAndGetResult();
   }
 
   private List<ColumnBuilder> getProperColumnBuilders() {
@@ -244,7 +247,6 @@ public class TableFunctionOperator implements 
ProcessOperator {
         result.add(subProperBlock.appendValueColumns(passThroughColumns));
       }
     } else {
-      // split the proper block into smaller blocks
       result.add(properBlock);
     }
     properBlockBuilder.reset();
@@ -265,12 +267,15 @@ public class TableFunctionOperator implements 
ProcessOperator {
 
   @Override
   public boolean hasNext() throws Exception {
-    return !finished || !resultTsBlocks.isEmpty();
+    return !finished || retainedTsBlock != null || !resultTsBlocks.isEmpty();
   }
 
   @Override
   public void close() throws Exception {
     partitionCache.close();
+    resultTsBlocks.clear();
+    resultTsBlock = null;
+    retainedTsBlock = null;
     inputOperator.close();
     if (processor != null) {
       destroyProcessor(processor);
@@ -281,18 +286,18 @@ public class TableFunctionOperator implements 
ProcessOperator {
 
   @Override
   public boolean isFinished() throws Exception {
-    return finished;
+    return finished && retainedTsBlock == null && resultTsBlocks.isEmpty();
   }
 
   @Override
   public long calculateMaxPeekMemory() {
     return inputOperator.calculateMaxPeekMemory()
-        + Math.max(DEFAULT_MAX_TSBLOCK_SIZE_IN_BYTES, 
properBlockBuilder.getRetainedSizeInBytes());
+        + Math.max(maxReturnSize, properBlockBuilder.getRetainedSizeInBytes());
   }
 
   @Override
   public long calculateMaxReturnSize() {
-    return Math.max(DEFAULT_MAX_TSBLOCK_SIZE_IN_BYTES, 
properBlockBuilder.getRetainedSizeInBytes());
+    return maxReturnSize;
   }
 
   @Override
diff --git 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/partition/PartitionCache.java
 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/partition/PartitionCache.java
index a33113d8f48..c0f6d64415c 100644
--- 
a/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/partition/PartitionCache.java
+++ 
b/iotdb-core/calc-commons/src/main/java/org/apache/iotdb/calc/execution/operator/process/function/partition/PartitionCache.java
@@ -117,6 +117,6 @@ public class PartitionCache {
   }
 
   public void close() {
-    // do nothing
+    clear();
   }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/tvf/TableFunctionOperatorTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/tvf/TableFunctionOperatorTest.java
index bcd17aa665c..cf1117de40a 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/tvf/TableFunctionOperatorTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/operator/process/tvf/TableFunctionOperatorTest.java
@@ -21,9 +21,11 @@ package 
org.apache.iotdb.db.queryengine.execution.operator.process.tvf;
 
 import org.apache.iotdb.calc.execution.operator.Operator;
 import 
org.apache.iotdb.calc.execution.operator.process.function.PartitionRecognizer;
+import 
org.apache.iotdb.calc.execution.operator.process.function.TableFunctionOperator;
 import 
org.apache.iotdb.calc.execution.operator.process.function.partition.PartitionState;
 import 
org.apache.iotdb.calc.execution.operator.process.function.partition.Slice;
 import org.apache.iotdb.commons.concurrent.IoTDBThreadPoolFactory;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
 import org.apache.iotdb.db.queryengine.common.FragmentInstanceId;
 import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
 import org.apache.iotdb.db.queryengine.common.QueryId;
@@ -31,9 +33,14 @@ import 
org.apache.iotdb.db.queryengine.execution.driver.DriverContext;
 import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext;
 import 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceStateMachine;
 import org.apache.iotdb.db.queryengine.execution.operator.OperatorContext;
+import org.apache.iotdb.udf.api.IoTDBLocal;
 import org.apache.iotdb.udf.api.relational.access.Record;
+import 
org.apache.iotdb.udf.api.relational.table.TableFunctionProcessorProvider;
+import 
org.apache.iotdb.udf.api.relational.table.processor.TableFunctionDataProcessor;
 
+import org.apache.tsfile.block.column.ColumnBuilder;
 import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.common.conf.TSFileDescriptor;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.read.common.block.TsBlock;
 import org.apache.tsfile.read.common.block.TsBlockBuilder;
@@ -48,11 +55,15 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import static 
org.apache.iotdb.calc.plan.planner.CommonOperatorUtils.TIME_COLUMN_TEMPLATE;
 import static 
org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext.createFragmentInstanceContext;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 
 public class TableFunctionOperatorTest {
   private static final ExecutorService instanceNotificationExecutor =
@@ -287,6 +298,326 @@ public class TableFunctionOperatorTest {
     }
   }
 
+  @Test
+  public void testResultTsBlockUsesAbstractOperatorSplitting() throws 
Exception {
+    assertResultTsBlockUsesAbstractOperatorSplitting(false, false);
+    assertResultTsBlockUsesAbstractOperatorSplitting(true, false);
+  }
+
+  @Test
+  public void testFinishResultUsesAbstractOperatorSplitting() throws Exception 
{
+    assertResultTsBlockUsesAbstractOperatorSplitting(false, true);
+    assertResultTsBlockUsesAbstractOperatorSplitting(true, true);
+  }
+
+  private void assertResultTsBlockUsesAbstractOperatorSplitting(
+      boolean withPassThrough, boolean outputInFinish) throws Exception {
+    int originalMaxBlockSize =
+        TSFileDescriptor.getInstance().getConfig().getMaxTsBlockSizeInBytes();
+    int maxBlockSize = 128;
+    // AbstractOperator uses at least one byte as the estimated size of each 
row, so producing more
+    // rows than the configured byte limit guarantees that the result must be 
split.
+    int outputRowCount = maxBlockSize * 2;
+    try {
+      OperatorContext operatorContext =
+          createOperatorContext(
+              "abstract_operator_split_" + withPassThrough + "_" + 
outputInFinish);
+      TableFunctionProcessorProvider provider =
+          new TableFunctionProcessorProvider() {
+            @Override
+            public TableFunctionDataProcessor getDataProcessor() {
+              return new TableFunctionDataProcessor() {
+                @Override
+                public void process(
+                    Record input,
+                    List<ColumnBuilder> properColumnBuilders,
+                    ColumnBuilder passThroughIndexBuilder) {
+                  if (!outputInFinish) {
+                    appendRows(properColumnBuilders, passThroughIndexBuilder);
+                  }
+                }
+
+                @Override
+                public void finish(
+                    List<ColumnBuilder> properColumnBuilders,
+                    ColumnBuilder passThroughIndexBuilder) {
+                  if (outputInFinish) {
+                    appendRows(properColumnBuilders, passThroughIndexBuilder);
+                  }
+                }
+
+                private void appendRows(
+                    List<ColumnBuilder> properColumnBuilders,
+                    ColumnBuilder passThroughIndexBuilder) {
+                  for (int i = 0; i < outputRowCount; i++) {
+                    properColumnBuilders.get(0).writeLong(i);
+                    if (passThroughIndexBuilder != null) {
+                      passThroughIndexBuilder.writeLong(0);
+                    }
+                  }
+                }
+              };
+            }
+          };
+
+      Operator singleRowChild = constructLongChildOperator(operatorContext, 
new long[] {1});
+
+      // FragmentInstanceContext initialization reloads the TsFile 
configuration, so apply the test
+      // limit immediately before constructing the operator that captures it.
+      
TSFileDescriptor.getInstance().getConfig().setMaxTsBlockSizeInBytes(maxBlockSize);
+      int returnedRows = 0;
+      int returnedBlocks = 0;
+      try (TableFunctionOperator operator =
+          new TableFunctionOperator(
+              operatorContext,
+              provider,
+              singleRowChild,
+              Collections.singletonList(TSDataType.INT64),
+              withPassThrough
+                  ? Arrays.asList(TSDataType.INT64, TSDataType.INT64)
+                  : Collections.singletonList(TSDataType.INT64),
+              1,
+              Collections.singletonList(0),
+              withPassThrough ? Collections.singletonList(0) : 
Collections.emptyList(),
+              withPassThrough,
+              Collections.emptyList(),
+              false,
+              mock(IoTDBLocal.class))) {
+        assertEquals(maxBlockSize, operator.calculateMaxReturnSize());
+        while (!operator.isFinished()) {
+          operator.isBlocked();
+          TsBlock block = operator.next();
+          if (block == null) {
+            continue;
+          }
+          returnedBlocks++;
+          for (int i = 0; i < block.getPositionCount(); i++) {
+            assertEquals(returnedRows, block.getColumn(0).getLong(i));
+            if (withPassThrough) {
+              assertEquals(1, block.getColumn(1).getLong(i));
+            }
+            returnedRows++;
+          }
+        }
+      }
+
+      assertEquals(outputRowCount, returnedRows);
+      Assert.assertTrue("Returned block count: " + returnedBlocks, 
returnedBlocks > 1);
+    } finally {
+      
TSFileDescriptor.getInstance().getConfig().setMaxTsBlockSizeInBytes(originalMaxBlockSize);
+    }
+  }
+
+  @Test
+  public void testPartitionResultsAreDrainedBeforeProcessingNextPartition() 
throws Exception {
+    int originalMaxBlockSize =
+        TSFileDescriptor.getInstance().getConfig().getMaxTsBlockSizeInBytes();
+    int maxBlockSize = 128;
+    int outputRowCount = maxBlockSize * 2;
+    try {
+      OperatorContext operatorContext = 
createOperatorContext("abstract_operator_split_partitions");
+      AtomicInteger processorCount = new AtomicInteger();
+      TableFunctionProcessorProvider provider =
+          new TableFunctionProcessorProvider() {
+            @Override
+            public TableFunctionDataProcessor getDataProcessor() {
+              int resultOffset = processorCount.getAndIncrement() * 
outputRowCount;
+              return new TableFunctionDataProcessor() {
+                @Override
+                public void process(
+                    Record input,
+                    List<ColumnBuilder> properColumnBuilders,
+                    ColumnBuilder passThroughIndexBuilder) {
+                  for (int i = 0; i < outputRowCount; i++) {
+                    properColumnBuilders.get(0).writeLong(resultOffset + i);
+                  }
+                }
+              };
+            }
+          };
+      Operator twoPartitionChild = constructLongChildOperator(operatorContext, 
new long[] {1, 2});
+
+      
TSFileDescriptor.getInstance().getConfig().setMaxTsBlockSizeInBytes(maxBlockSize);
+      int returnedRows = 0;
+      int returnedBlocks = 0;
+      try (TableFunctionOperator operator =
+          new TableFunctionOperator(
+              operatorContext,
+              provider,
+              twoPartitionChild,
+              Collections.singletonList(TSDataType.INT64),
+              Collections.singletonList(TSDataType.INT64),
+              1,
+              Collections.singletonList(0),
+              Collections.emptyList(),
+              false,
+              Collections.singletonList(0),
+              false,
+              mock(IoTDBLocal.class))) {
+        while (!operator.isFinished()) {
+          operator.isBlocked();
+          TsBlock block = operator.next();
+          if (block == null) {
+            continue;
+          }
+          returnedBlocks++;
+          for (int i = 0; i < block.getPositionCount(); i++) {
+            assertEquals(returnedRows++, block.getColumn(0).getLong(i));
+          }
+        }
+      }
+
+      assertEquals(2, processorCount.get());
+      assertEquals(2 * outputRowCount, returnedRows);
+      Assert.assertTrue("Returned block count: " + returnedBlocks, 
returnedBlocks > 2);
+    } finally {
+      
TSFileDescriptor.getInstance().getConfig().setMaxTsBlockSizeInBytes(originalMaxBlockSize);
+    }
+  }
+
+  @Test
+  public void testCloseReleasesPendingResultState() throws Exception {
+    int originalMaxBlockSize =
+        TSFileDescriptor.getInstance().getConfig().getMaxTsBlockSizeInBytes();
+    int maxBlockSize = 128;
+    int outputRowCount = maxBlockSize * 2;
+    try {
+      OperatorContext operatorContext =
+          createOperatorContext("close_pending_table_function_result");
+      AtomicBoolean processorDestroyed = new AtomicBoolean();
+      TableFunctionProcessorProvider provider =
+          new TableFunctionProcessorProvider() {
+            @Override
+            public TableFunctionDataProcessor getDataProcessor() {
+              return new TableFunctionDataProcessor() {
+                @Override
+                public void process(
+                    Record input,
+                    List<ColumnBuilder> properColumnBuilders,
+                    ColumnBuilder passThroughIndexBuilder) {
+                  for (int i = 0; i < outputRowCount; i++) {
+                    properColumnBuilders.get(0).writeLong(i);
+                  }
+                }
+
+                @Override
+                public void beforeDestroy() {
+                  processorDestroyed.set(true);
+                }
+              };
+            }
+          };
+      AtomicBoolean childClosed = new AtomicBoolean();
+      Operator child = constructLongChildOperator(operatorContext, new long[] 
{1}, childClosed);
+      IoTDBLocal ioTDBLocal = mock(IoTDBLocal.class);
+
+      
TSFileDescriptor.getInstance().getConfig().setMaxTsBlockSizeInBytes(maxBlockSize);
+      TableFunctionOperator operator =
+          new TableFunctionOperator(
+              operatorContext,
+              provider,
+              child,
+              Collections.singletonList(TSDataType.INT64),
+              Collections.singletonList(TSDataType.INT64),
+              1,
+              Collections.singletonList(0),
+              Collections.emptyList(),
+              false,
+              Collections.emptyList(),
+              false,
+              ioTDBLocal);
+      operator.isBlocked();
+      Assert.assertNotNull(operator.next());
+      long retainedSizeBeforeClose = operator.ramBytesUsed();
+
+      operator.close();
+
+      Assert.assertTrue(childClosed.get());
+      Assert.assertTrue(processorDestroyed.get());
+      verify(ioTDBLocal).close();
+      Assert.assertTrue(operator.ramBytesUsed() < retainedSizeBeforeClose);
+    } finally {
+      
TSFileDescriptor.getInstance().getConfig().setMaxTsBlockSizeInBytes(originalMaxBlockSize);
+    }
+  }
+
+  private Operator constructLongChildOperator(
+      OperatorContext operatorContext, long[] values, AtomicBoolean closed) {
+    return new Operator() {
+      private boolean consumed;
+
+      @Override
+      public OperatorContext getOperatorContext() {
+        return operatorContext;
+      }
+
+      @Override
+      public TsBlock next() {
+        TsBlockBuilder builder =
+            new TsBlockBuilder(values.length, 
Collections.singletonList(TSDataType.INT64));
+        for (long value : values) {
+          builder.getColumnBuilder(0).writeLong(value);
+          builder.declarePosition();
+        }
+        consumed = true;
+        return builder.build(
+            new RunLengthEncodedColumn(TIME_COLUMN_TEMPLATE, 
builder.getPositionCount()));
+      }
+
+      @Override
+      public boolean hasNext() {
+        return !consumed;
+      }
+
+      @Override
+      public void close() {
+        closed.set(true);
+      }
+
+      @Override
+      public boolean isFinished() {
+        return consumed;
+      }
+
+      @Override
+      public long calculateMaxPeekMemory() {
+        return 0;
+      }
+
+      @Override
+      public long calculateMaxReturnSize() {
+        return 0;
+      }
+
+      @Override
+      public long calculateRetainedSizeAfterCallingNext() {
+        return 0;
+      }
+
+      @Override
+      public long ramBytesUsed() {
+        return 0;
+      }
+    };
+  }
+
+  private Operator constructLongChildOperator(OperatorContext operatorContext, 
long[] values) {
+    return constructLongChildOperator(operatorContext, values, new 
AtomicBoolean());
+  }
+
+  private OperatorContext createOperatorContext(String queryName) {
+    QueryId queryId = new QueryId(queryName);
+    FragmentInstanceId instanceId =
+        new FragmentInstanceId(new PlanFragmentId(queryId, 0), 
"stub-instance");
+    FragmentInstanceStateMachine stateMachine =
+        new FragmentInstanceStateMachine(instanceId, 
instanceNotificationExecutor);
+    FragmentInstanceContext fragmentInstanceContext =
+        createFragmentInstanceContext(instanceId, stateMachine);
+    DriverContext driverContext = new DriverContext(fragmentInstanceContext, 
0);
+    return driverContext.addOperatorContext(
+        0, new PlanNodeId("tvf"), TableFunctionOperator.class.getSimpleName());
+  }
+
   private void checkIteratorSimply(Slice slice, List<List<Object>> expected) {
     Iterator<Record> recordIterable = slice.getRequiredRecordIterator(false);
     int i = 0;

Reply via email to