From 54d0b57cac8761396082f3252021c5eeb6e878ef Mon Sep 17 00:00:00 2001 From: Roman Rinchinov Date: Wed, 17 Jun 2026 11:53:48 +0200 Subject: [PATCH 1/5] fix(delta): drain all batches per scan file in DeltaInputSourceIterator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #18606 — only 1024 rows ingested per Parquet file when using the Delta Lake input source. Root cause: filteredBatchIterator was a local variable inside hasNext(). When the method returned true after the first non-empty batch of a file, the iterator went out of scope. The next hasNext() call advanced to the next file, skipping all remaining batches of the current file. With Delta kernel's default batch size of 1024 rows, this caused exactly 1024 rows × N files to be ingested regardless of actual file size. Fix: promote filteredBatchIterator to a field (currentFileIterator) so it survives across hasNext() calls and all batches of a file are drained before advancing to the next file. Also fixed close() to properly close currentFileIterator and drain all remaining file iterators. --- .../delta/input/DeltaInputSourceReader.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java b/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java index 672a126f7c46..d543e0f60fdb 100644 --- a/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java +++ b/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java @@ -95,6 +95,13 @@ private static class DeltaInputSourceIterator implements CloseableIterator> filteredColumnarBatchIterators; + // Keep a reference to the current file's batch iterator so we drain ALL + // its batches before advancing to the next file. + // Bug fix for https://github.com/apache/druid/issues/18606: + // the original code used a local variable for filteredBatchIterator which + // was discarded on return, causing only the first batch (1024 rows) of each + // file to be read. + private io.delta.kernel.utils.CloseableIterator currentFileIterator = null; private io.delta.kernel.utils.CloseableIterator currentBatch = null; private final InputRowSchema inputRowSchema; @@ -111,20 +118,20 @@ public DeltaInputSourceIterator( public boolean hasNext() { while (currentBatch == null || !currentBatch.hasNext()) { - if (!filteredColumnarBatchIterators.hasNext()) { - return false; // No more batches or records to read! - } - - final io.delta.kernel.utils.CloseableIterator filteredBatchIterator = - filteredColumnarBatchIterators.next(); - - while (filteredBatchIterator.hasNext()) { - final FilteredColumnarBatch nextBatch = filteredBatchIterator.next(); + // Drain remaining batches from the current file before moving to the next. + while (currentFileIterator != null && currentFileIterator.hasNext()) { + final FilteredColumnarBatch nextBatch = currentFileIterator.next(); currentBatch = nextBatch.getRows(); if (currentBatch.hasNext()) { return true; } } + + // Advance to the next file. + if (!filteredColumnarBatchIterators.hasNext()) { + return false; + } + currentFileIterator = filteredColumnarBatchIterators.next(); } return true; } @@ -146,8 +153,10 @@ public void close() throws IOException if (currentBatch != null) { currentBatch.close(); } - - if (filteredColumnarBatchIterators.hasNext()) { + if (currentFileIterator != null) { + currentFileIterator.close(); + } + while (filteredColumnarBatchIterators.hasNext()) { filteredColumnarBatchIterators.next().close(); } } From 6b103c661fcafcbd4fd710ce66392ed3641abe5a Mon Sep 17 00:00:00 2001 From: Roman Rinchinov Date: Wed, 17 Jun 2026 12:00:55 +0200 Subject: [PATCH 2/5] test(delta): add regression test for GH-18606 batch drain fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Delta table with 2 Parquet files × 2000 rows (total 4000) where each file exceeds the Delta kernel's default batch size of 1024 rows. Without the fix: DeltaInputSourceIterator returns 1024 × 2 = 2048 rows. With the fix: all 4000 rows are returned correctly. Test: DeltaInputSourceBatchDrainTest.testAllRowsReturnedWhenFilesExceedOneBatch --- .../input/DeltaInputSourceBatchDrainTest.java | 83 ++++++++++++++++++ .../delta/input/LargeRowGroupDeltaTable.java | 56 ++++++++++++ ...-a6fe-5397f37d29d8-c000.snappy.parquet.crc | Bin 0 -> 152 bytes ...-af60-5eaca2f7ba03-c000.snappy.parquet.crc | Bin 0 -> 152 bytes .../_delta_log/.00000000000000000000.json.crc | Bin 0 -> 20 bytes .../_delta_log/00000000000000000000.json | 5 ++ ...42a0-a6fe-5397f37d29d8-c000.snappy.parquet | Bin 0 -> 18078 bytes ...4094-af60-5eaca2f7ba03-c000.snappy.parquet | Bin 0 -> 18185 bytes 8 files changed, 144 insertions(+) create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/.part-00000-42349806-104f-42a0-a6fe-5397f37d29d8-c000.snappy.parquet.crc create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/.part-00001-42b5d278-2c32-4094-af60-5eaca2f7ba03-c000.snappy.parquet.crc create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/_delta_log/.00000000000000000000.json.crc create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/_delta_log/00000000000000000000.json create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/part-00000-42349806-104f-42a0-a6fe-5397f37d29d8-c000.snappy.parquet create mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/part-00001-42b5d278-2c32-4094-af60-5eaca2f7ba03-c000.snappy.parquet diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java new file mode 100644 index 000000000000..02746319eeae --- /dev/null +++ b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java @@ -0,0 +1,83 @@ +/* + * 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.druid.delta.input; + +import org.apache.druid.data.input.InputRow; +import org.apache.druid.data.input.InputSourceReader; +import org.apache.druid.java.util.common.parsers.CloseableIterator; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Regression test for https://github.com/apache/druid/issues/18606. + * + * The bug: {@code DeltaInputSourceIterator.hasNext()} used a local variable for + * the per-file {@code CloseableIterator}. When the method + * returned {@code true} after the first non-empty batch of a file, the iterator + * went out of scope. The next {@code hasNext()} call advanced to the next file, + * skipping all remaining batches. With Delta kernel's default batch size of 1024 + * rows this caused exactly {@code 1024 × numFiles} rows to be returned regardless + * of actual file size. + * + * The fix promotes the per-file iterator to a field ({@code currentFileIterator}) + * so all batches are drained before advancing to the next file. + * + * Test table: 2 Parquet files × 2000 rows = 4000 rows total. + * Without the fix: 1024 × 2 = 2048 rows returned. + * With the fix: 4000 rows returned. + */ +public class DeltaInputSourceBatchDrainTest +{ + @Test + public void testAllRowsReturnedWhenFilesExceedOneBatch() throws IOException + { + final DeltaInputSource inputSource = new DeltaInputSource( + LargeRowGroupDeltaTable.DELTA_TABLE_PATH, + null, + null, + null + ); + + final InputSourceReader reader = inputSource.reader( + LargeRowGroupDeltaTable.SCHEMA, + null, + null + ); + + final List rows = new ArrayList<>(); + try (CloseableIterator iterator = reader.read()) { + while (iterator.hasNext()) { + rows.add(iterator.next()); + } + } + + Assert.assertEquals( + "Expected all rows to be read — regression check for GH-18606 " + + "(DeltaInputSourceIterator only returned first 1024 rows per file). " + + "Got " + rows.size() + " rows, expected " + LargeRowGroupDeltaTable.EXPECTED_ROW_COUNT + ".", + LargeRowGroupDeltaTable.EXPECTED_ROW_COUNT, + rows.size() + ); + } +} \ No newline at end of file diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java new file mode 100644 index 000000000000..d5250bd17435 --- /dev/null +++ b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java @@ -0,0 +1,56 @@ +/* + * 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.druid.delta.input; + +import org.apache.druid.data.input.impl.DimensionsSpec; +import org.apache.druid.data.input.impl.LongDimensionSchema; +import org.apache.druid.data.input.impl.StringDimensionSchema; +import org.apache.druid.data.input.impl.TimestampSpec; +import org.apache.druid.java.util.common.parsers.JSONPathSpec; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Descriptor for a Delta table with 2 Parquet files × 2000 rows = 4000 rows total. + * + * Each file has > 1024 rows, ensuring the Delta kernel reads more than one batch + * per file. Used as a regression test for GH-18606 (DeltaInputSourceIterator only + * returned the first 1024 rows per file). + * + * Generated by src/test/resources/create_delta_table.py (large-row-group-table). + */ +public class LargeRowGroupDeltaTable +{ + public static final String DELTA_TABLE_PATH = + "src/test/resources/large-row-group-table"; + + public static final int EXPECTED_ROW_COUNT = 4000; + + public static final org.apache.druid.data.input.InputRowSchema SCHEMA = + new org.apache.druid.data.input.InputRowSchema( + new TimestampSpec("id", "posix", null), + new DimensionsSpec(Arrays.asList( + new LongDimensionSchema("id"), + new StringDimensionSchema("name") + )), + Collections.emptyList() + ); +} \ No newline at end of file diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/.part-00000-42349806-104f-42a0-a6fe-5397f37d29d8-c000.snappy.parquet.crc b/extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/.part-00000-42349806-104f-42a0-a6fe-5397f37d29d8-c000.snappy.parquet.crc new file mode 100644 index 0000000000000000000000000000000000000000..aa967daecef790eae712f4c108eab2c7dcb931fd GIT binary patch literal 152 zcmV;J0B8SWa$^7h00IE!fF&R8pAaH!0A0Qzs#hSHyF3vbm;&`*-PyZf+w)90e|lng zm~A4PEfoF1#9F_pa>r5ON$0;oiX1|5E{R~FVR95(xG6SpL-`M(N_g${0uZmH*R|O2 zj9T(C1e>~D*knZZiZZiht*NKTJr|xjhf&Q(DVtcx|Gg)IRq8lbqh%cOX@VJY@2HSF GMUNRL<3{WN literal 0 HcmV?d00001 diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/.part-00001-42b5d278-2c32-4094-af60-5eaca2f7ba03-c000.snappy.parquet.crc b/extensions-contrib/druid-deltalake-extensions/src/test/resources/large-row-group-table/.part-00001-42b5d278-2c32-4094-af60-5eaca2f7ba03-c000.snappy.parquet.crc new file mode 100644 index 0000000000000000000000000000000000000000..a46c186eed7b7b8e5d0ba694f6c213f2fb8180bc GIT binary patch literal 152 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 uJ@EYTW3!=`9+wZ&W5 Date: Wed, 17 Jun 2026 12:39:19 +0200 Subject: [PATCH 3/5] test(delta): add BatchDrainRegressionTests to DeltaInputSourceTest for GH-18606 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds LargeRowGroupDeltaTable (2 files × 2000 rows = 4000 total) and a BatchDrainRegressionTests inner class inside DeltaInputSourceTest following the same pattern as existing test classes. The regression test fails with the bug (returns 1024 × 2 = 2048 rows) and passes with the fix (returns all 4000 rows). --- .../input/DeltaInputSourceBatchDrainTest.java | 83 ------------------- .../delta/input/DeltaInputSourceTest.java | 40 +++++++++ .../delta/input/LargeRowGroupDeltaTable.java | 30 ++++--- 3 files changed, 54 insertions(+), 99 deletions(-) delete mode 100644 extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java deleted file mode 100644 index 02746319eeae..000000000000 --- a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceBatchDrainTest.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.druid.delta.input; - -import org.apache.druid.data.input.InputRow; -import org.apache.druid.data.input.InputSourceReader; -import org.apache.druid.java.util.common.parsers.CloseableIterator; -import org.junit.Assert; -import org.junit.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Regression test for https://github.com/apache/druid/issues/18606. - * - * The bug: {@code DeltaInputSourceIterator.hasNext()} used a local variable for - * the per-file {@code CloseableIterator}. When the method - * returned {@code true} after the first non-empty batch of a file, the iterator - * went out of scope. The next {@code hasNext()} call advanced to the next file, - * skipping all remaining batches. With Delta kernel's default batch size of 1024 - * rows this caused exactly {@code 1024 × numFiles} rows to be returned regardless - * of actual file size. - * - * The fix promotes the per-file iterator to a field ({@code currentFileIterator}) - * so all batches are drained before advancing to the next file. - * - * Test table: 2 Parquet files × 2000 rows = 4000 rows total. - * Without the fix: 1024 × 2 = 2048 rows returned. - * With the fix: 4000 rows returned. - */ -public class DeltaInputSourceBatchDrainTest -{ - @Test - public void testAllRowsReturnedWhenFilesExceedOneBatch() throws IOException - { - final DeltaInputSource inputSource = new DeltaInputSource( - LargeRowGroupDeltaTable.DELTA_TABLE_PATH, - null, - null, - null - ); - - final InputSourceReader reader = inputSource.reader( - LargeRowGroupDeltaTable.SCHEMA, - null, - null - ); - - final List rows = new ArrayList<>(); - try (CloseableIterator iterator = reader.read()) { - while (iterator.hasNext()) { - rows.add(iterator.next()); - } - } - - Assert.assertEquals( - "Expected all rows to be read — regression check for GH-18606 " - + "(DeltaInputSourceIterator only returned first 1024 rows per file). " - + "Got " + rows.size() + " rows, expected " + LargeRowGroupDeltaTable.EXPECTED_ROW_COUNT + ".", - LargeRowGroupDeltaTable.EXPECTED_ROW_COUNT, - rows.size() - ); - } -} \ No newline at end of file diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceTest.java b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceTest.java index cbbcaefb3ceb..6a689f31df59 100644 --- a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceTest.java +++ b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/DeltaInputSourceTest.java @@ -439,6 +439,46 @@ private static List readAllRows(InputSourceReader reader) throws IOExc return rows; } + /** + * Regression test for https://github.com/apache/druid/issues/18606. + * + * {@link DeltaInputSourceReader.DeltaInputSourceIterator} used a local variable for the + * per-file {@code CloseableIterator}. When {@code hasNext()} returned + * after the first non-empty batch of a file, that iterator went out of scope. The next + * {@code hasNext()} call advanced to the next file, skipping all remaining batches of the + * current file. With the Delta kernel default batch size of 1024 rows this produced exactly + * {@code 1024 * numFiles} rows regardless of actual file size. + * + * Test table: 2 Parquet files x 2000 rows = 4000 rows total. + * Without the fix: 1024 x 2 = 2048 rows. + * With the fix: 4000 rows. + */ + public static class BatchDrainRegressionTests + { + @Test + public void testAllRowsReturnedWhenFileExceedsOneBatch() throws IOException + { + final DeltaInputSource deltaInputSource = new DeltaInputSource( + LargeRowGroupDeltaTable.DELTA_TABLE_PATH, + null, + null, + null + ); + final InputSourceReader inputSourceReader = deltaInputSource.reader( + LargeRowGroupDeltaTable.SCHEMA, + null, + null + ); + final List rows = readAllRows(inputSourceReader); + Assert.assertEquals( + "Expected all rows to be read. " + + "If this fails with " + (1024 * 2) + " rows, the per-file batch drain bug (GH-18606) has regressed.", + LargeRowGroupDeltaTable.EXPECTED_ROW_COUNT, + rows.size() + ); + } + } + private static void validateRows( final List> expectedRows, final List actualReadRows, diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java index d5250bd17435..e7da71503fe7 100644 --- a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java +++ b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java @@ -19,21 +19,20 @@ package org.apache.druid.delta.input; +import com.google.common.collect.ImmutableList; +import org.apache.druid.data.input.ColumnsFilter; +import org.apache.druid.data.input.InputRowSchema; import org.apache.druid.data.input.impl.DimensionsSpec; import org.apache.druid.data.input.impl.LongDimensionSchema; import org.apache.druid.data.input.impl.StringDimensionSchema; import org.apache.druid.data.input.impl.TimestampSpec; -import org.apache.druid.java.util.common.parsers.JSONPathSpec; - -import java.util.Arrays; -import java.util.Collections; /** * Descriptor for a Delta table with 2 Parquet files × 2000 rows = 4000 rows total. * - * Each file has > 1024 rows, ensuring the Delta kernel reads more than one batch - * per file. Used as a regression test for GH-18606 (DeltaInputSourceIterator only - * returned the first 1024 rows per file). + * Each file has more than 1024 rows, ensuring the Delta kernel reads more than one + * batch per file. Used as a regression test for GH-18606 where + * {@link DeltaInputSourceReader} only returned the first 1024 rows per file. * * Generated by src/test/resources/create_delta_table.py (large-row-group-table). */ @@ -44,13 +43,12 @@ public class LargeRowGroupDeltaTable public static final int EXPECTED_ROW_COUNT = 4000; - public static final org.apache.druid.data.input.InputRowSchema SCHEMA = - new org.apache.druid.data.input.InputRowSchema( - new TimestampSpec("id", "posix", null), - new DimensionsSpec(Arrays.asList( - new LongDimensionSchema("id"), - new StringDimensionSchema("name") - )), - Collections.emptyList() - ); + public static final InputRowSchema SCHEMA = new InputRowSchema( + new TimestampSpec("id", "posix", null), + new DimensionsSpec(ImmutableList.of( + new LongDimensionSchema("id"), + new StringDimensionSchema("name") + )), + ColumnsFilter.all() + ); } \ No newline at end of file From 910bd7518ca253a70cea0b6cfd12f958b0dfca75 Mon Sep 17 00:00:00 2001 From: Roman Rinchinov Date: Wed, 17 Jun 2026 17:13:10 +0200 Subject: [PATCH 4/5] style: add missing newline at end of LargeRowGroupDeltaTable.java --- .../org/apache/druid/delta/input/LargeRowGroupDeltaTable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java index e7da71503fe7..c9a1966af153 100644 --- a/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java +++ b/extensions-contrib/druid-deltalake-extensions/src/test/java/org/apache/druid/delta/input/LargeRowGroupDeltaTable.java @@ -51,4 +51,4 @@ public class LargeRowGroupDeltaTable )), ColumnsFilter.all() ); -} \ No newline at end of file +} From 0c9e615337403e2747e38c52b86d2c644742e216 Mon Sep 17 00:00:00 2001 From: Roman Rinchinov Date: Thu, 18 Jun 2026 15:28:40 +0200 Subject: [PATCH 5/5] fix(delta): close drained file iterator before advancing Each per-file iterator from Scan.transformPhysicalData() owns an underlying Parquet reader/file handle. hasNext() overwrote currentFileIterator with the next file without closing the exhausted one, leaking a handle per completed file on multi-file tables (close() only closed the last and the never-started iterators). Now close the drained iterator before advancing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../druid/delta/input/DeltaInputSourceReader.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java b/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java index d543e0f60fdb..6acbb095f3bb 100644 --- a/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java +++ b/extensions-contrib/druid-deltalake-extensions/src/main/java/org/apache/druid/delta/input/DeltaInputSourceReader.java @@ -131,6 +131,20 @@ public boolean hasNext() if (!filteredColumnarBatchIterators.hasNext()) { return false; } + // Close the drained file iterator before overwriting it. Each iterator from + // Scan.transformPhysicalData() owns an underlying Parquet reader/file handle; + // not closing it here would leak a handle per completed file on multi-file + // tables (only the last and the never-started iterators are closed in close()). + // hasNext() cannot throw checked exceptions, so wrap like the rest of this + // extension (see DeltaInputSource). + if (currentFileIterator != null) { + try { + currentFileIterator.close(); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } currentFileIterator = filteredColumnarBatchIterators.next(); } return true;