Repository navigation
Conversation
FrankChen021
left a comment
There was a problem hiding this comment.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 1 |
| P2 | 1 |
| P3 | 0 |
| Total | 2 |
Reviewed 8 of 8 changed files.
This is an automated review by Codex GPT-5.5
| { | ||
| private static final Logger log = new Logger(IcebergNativeRecordReader.class); | ||
|
|
||
| private static final WarehouseFileIO WAREHOUSE_FILE_IO = new WarehouseFileIO(); |
There was a problem hiding this comment.
[P1] Undefined FileIO class breaks compilation
The new reader instantiates WarehouseFileIO, but no class with that name is defined in this package or elsewhere in the repo, and there is no import that could resolve it. Any build that compiles this extension will fail before the V2 ingestion path can run. Use an existing Iceberg FileIO implementation or add the missing implementation.
|
|
||
| // Handle residual filter based on mode | ||
| if (detectedResidual != null) { | ||
| if (detectedResidual == null) { |
There was a problem hiding this comment.
[P2] Residual-filter check is inverted
This condition now enters the residual-handling branch when detectedResidual is null, even though null means no residual was found. With ResidualFilterMode.FAIL, valid scans that have no residual, such as partition-pruned scans, will throw a residual-filter error, while scans that do have a residual skip the warning/failure path. Restore the check to run only when detectedResidual != null.
FrankChen021
left a comment
There was a problem hiding this comment.
🟡 Changes recommended
The current V2 reader has one blocking correctness issue and two additional behavior regressions: nested equality deletes are not resolved through Iceberg's nested field names, fixed-width binary equality keys cannot match because byte arrays use identity equality, and the native V2 path bypasses the configured Parquet input format. The two earlier MergeLens findings on the missing FileIO class and inverted residual-filter check are resolved in the current head.
Reviewed 10 of 10 changed files.
Validation: static review of the incremental diff, full merge-base diff, current surrounding code, Druid input-source/task wiring, and the Iceberg 1.11.0 local sources; git diff --check passed. No builds, tests, dependency installs, or formatters were run.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 1 |
| P2 | 2 |
| P3 | 0 |
| Total | 3 |
This is an automated review by Codex GPT-5.6-Luna(max)
After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.
| for (String fieldName : fieldNames) { | ||
| key.put(fieldName, dataRecord.getField(fieldName)); | ||
| } | ||
| return keys.contains(key); |
There was a problem hiding this comment.
[P2] Fixed binary equality keys use identity comparison
Finding: GenericParquetReaders.FixedReader returns a new byte[] for each fixed-width value. The delete key map and the data-row key map therefore contain distinct arrays, and HashMap.equals compares those arrays by identity rather than contents; equality deletes on Iceberg fixed fields never match and the deleted rows remain visible.
Suggestion: Canonicalize binary and fixed values to a content-based representation, or use deep/content equality when constructing and checking equality-delete keys.
FrankChen021
left a comment
There was a problem hiding this comment.
🟡 Changes recommended
The current V2 delete-file path still has two concrete compatibility regressions: it bypasses the configured Parquet InputFormat, and its native row conversion does not apply Druid dimension discovery/include-all/exclusion semantics. Both can change ingestion results for otherwise valid configurations.
The earlier findings for nested equality deletes and fixed-width binary equality keys appear resolved in the current head. The prior FileIO and residual-filter findings also remain resolved.
Reviewed 10 of 10 changed files.
Scope: updated-since-review incremental diff from 5fc1408, the full merge-base diff, and relevant Druid/Iceberg surrounding code.
Validation: static review only; git diff --check passed. No builds, tests, dependency installs, or formatters were run.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 0 |
| P2 | 2 |
| P3 | 0 |
| Total | 2 |
This is an automated review by Codex GPT-5.6-Luna(max)
After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.
# Conflicts: # docs/development/extensions-contrib/iceberg.md # extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergCatalog.java # extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergInputSource.java
| List<Integer> fieldIds = fieldIdsStr.isEmpty() | ||
| ? Collections.emptyList() | ||
| : Arrays.stream(fieldIdsStr.split(",")) | ||
| .map(Integer::parseInt) |
FrankChen021
left a comment
There was a problem hiding this comment.
🟡 Changes recommended
The current head retains two correctness regressions in the Iceberg V2 delete-aware path: it bypasses configured Parquet InputFormat behavior, and its native row conversion does not honor Druid dimension discovery/include-all/exclusion semantics. I also found that the new scan-planning path leaves Iceberg's CloseableIterable unclosed, which skips scan completion and can leak planning resources on failure.
The earlier findings for nested equality deletes and fixed-width binary equality keys appear resolved in the current head. The prior FileIO and residual-filter findings also remain resolved.
Reviewed 10 of 10 changed files:
- docs/development/extensions-contrib/iceberg.md
- extensions-contrib/druid-iceberg-extensions/pom.xml
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/common/IcebergDruidModule.java
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergCatalog.java
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergFileTaskInputSource.java
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergInputSource.java
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergNativeRecordReader.java
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/IcebergRecordConverter.java
- extensions-contrib/druid-iceberg-extensions/src/main/java/org/apache/druid/iceberg/input/WarehouseFileIO.java
- extensions-contrib/druid-iceberg-extensions/src/test/java/org/apache/druid/iceberg/input/IcebergInputSourceTest.java
Scope: started from the prepared incremental diff, then reviewed the full merge-base diff, prior review findings, current-head code, Druid input-source/task wiring, and relevant Iceberg local sources.
Validation: static review only; git diff --check passed. No builds, tests, dependency installs, or formatters were run.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 0 |
| P2 | 2 |
| P3 | 1 |
| Total | 3 |
This is an automated review by Codex GPT-5.6-Luna(max)
After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.
| } | ||
| if (hasDeleteFiles) { | ||
| // V2 path: create a combined reader across all file-scan tasks. | ||
| final List<IcebergNativeRecordReader> taskReaders = v2Tasks.stream() |
There was a problem hiding this comment.
[P2] V2 reads ignore the configured input format
Finding: When any planned task has delete files, this branch routes every task through IcebergNativeRecordReader, but the reader is constructed only from InputRowSchema and the supplied InputFormat is discarded. ParquetInputFormat options such as flattenSpec and binaryAsString are therefore skipped for V2 tables, including data files without deletes, so a valid ingestion spec can produce different or missing indexed values from the existing warehouse-reader path.
Suggestion: Preserve the configured Parquet input-format semantics on the delete-aware path, either by carrying the relevant options into the native reader or by applying delete filtering around the existing Druid input-format reader.
| { | ||
| DateTime timestamp = inputRowSchema.getTimestampSpec().extractTimestamp(row); | ||
|
|
||
| List<String> dimensions = inputRowSchema.getDimensionsSpec().getDimensionNames(); |
There was a problem hiding this comment.
[P2] Native conversion ignores dimension discovery rules
Finding: The native V2 conversion uses only DimensionsSpec.getDimensionNames(), and when that list is empty it treats every non-timestamp column as a dimension. It never applies DimensionsSpec.useSchemaDiscovery(), includeAllDimensions, or dimensionExclusions, so V2 ingestion can omit dimensions that discovery/include-all should add or index fields that the ingestion spec explicitly excludes.
Suggestion: Resolve dimensions with the same MapInputRowParser.findDimensions semantics as the normal input-format path, including discovery/include-all behavior and exclusions, before constructing MapBasedInputRow.
| for (FileScanTask task : tasks) { | ||
| dataFilePaths.add(task.file().location()); | ||
|
|
||
| CloseableIterable<FileScanTask> taskIterable = tableScan.planFiles(); |
There was a problem hiding this comment.
[P3] Close the planned scan iterable
Finding: extractFileScanTasksWithSchema obtains the CloseableIterable returned by TableScan.planFiles() and iterates it without closing it. Iceberg wraps scan planning in a close-completion callback that finalizes planning metrics and closes nested manifest iterables; the old path used try-with-resources. Normal scans therefore skip completion, and exception or early-failure paths can retain planning resources across repeated ingestion attempts.
Suggestion: Iterate the planned tasks inside try-with-resources and close the CloseableIterable on both normal completion and failure.
|
@FrankChen021 Please review |
FrankChen021
left a comment
There was a problem hiding this comment.
🟡 Changes recommended
The current V2 delete-aware path still has a blocking input-format propagation regression for parallel ingestion, plus a resource-usage issue in the unsplit reader and a failing binaryAsString regression test. Parallel workers can silently diverge from the configured single-task semantics, and large single-task reads can hold too many Parquet file handles open.
Reviewed 10 of 10 changed files.
Scope: updated-since-review incremental diff from 5067eb3, the full merge-base diff, current surrounding Druid input-source/task wiring, and relevant Iceberg/Parquet reader sources.
Validation: static review only; git diff --check passed. No builds, tests, dependency installs, or formatters were run.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 1 |
| P2 | 2 |
| P3 | 0 |
| Total | 3 |
This is an automated review by Codex GPT-6 Luna(Max)
After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.
| public boolean needsFormat() | ||
| { | ||
| // Handles its own reading through Iceberg's native Parquet/ORC reader | ||
| return false; |
There was a problem hiding this comment.
[P1] Preserve the configured input format on V2 split workers
Finding: A parallel-index V2 split is deserialized as this source, and Druid passes an InputFormat to a task only when needsFormat() is true. Returning false therefore makes IcebergNativeRecordReader receive null on every worker, so binaryAsString is silently disabled and flattenSpec fields are silently ignored even though the source-level reader and documentation claim to honor or reject those settings. This makes parallel ingestion produce different dimensions and binary values from single-task ingestion.
Suggestion: Carry the relevant input-format settings in the split source or make the split source participate in Druid's input-format propagation so the native reader receives the configured format on worker tasks.
| public CloseableIterator<InputRow> read(InputStats inputStats) throws IOException | ||
| { | ||
| List<CloseableIterator<InputRow>> iters = new ArrayList<>(); | ||
| for (IcebergNativeRecordReader r : taskReaders) { |
There was a problem hiding this comment.
[P2] Open V2 readers lazily instead of all at once
Finding: The unsplit V2 reader calls read() for every task before returning the concatenated iterator. IcebergNativeRecordReader.read() opens its Parquet iterator during streamRows(), so a table with many data files holds one file handle per data file (and can leak already-open iterators if a later reader fails) until the outer iterator is closed. This can exhaust worker file descriptors for ordinary single-task ingestion.
Suggestion: Build the concatenated iterator lazily and close each task reader before opening the next one, with cleanup for readers opened before an exception.
| * {@code getBinaryAsString()} accessor, mirroring {@code ParquetInputFormat}'s shape without | ||
| * depending on the parquet-extensions module. | ||
| */ | ||
| private static class FakeNestedInputFormat extends NestedInputFormat |
There was a problem hiding this comment.
[P2] Make the reflective test input format accessible
Finding: The binaryAsString regression test passes a private FakeNestedInputFormat class to IcebergNativeRecordReader, which invokes getBinaryAsString reflectively from a different top-level class. Method.invoke performs access checks on the declaring class, so this private test double causes IllegalAccessException (wrapped as RE) instead of returning the configured value and makes testInputSourceV2HonorsBinaryAsString fail.
Suggestion: Use a public test double or avoid reflective access by exposing a shared accessor contract that the reader can call directly.
FrankChen021
left a comment
There was a problem hiding this comment.
🟡 Changes recommended
The current V2 native reader still mishandles Iceberg temporal values and can silently ignore a valid non-discovering flattenSpec. Tables using native timestamp fields can be ingested with incorrect event times, while flattening behavior can diverge from the V1 path; fix these before merging. The three prior findings on input-format propagation, reader resource usage, and binaryAsString reflection are resolved in the current head.
Reviewed 10 of 10 changed files.
Scope: Started from the incremental diff since 2ad1fca, then reviewed the full merge-base diff and relevant current Druid/Iceberg surrounding code.
| Severity | Findings |
|---|---|
| P0 | 0 |
| P1 | 1 |
| P2 | 1 |
| P3 | 0 |
| Total | 2 |
This is an automated review by Codex GPT-5.6-Luna(max)
After addressing the findings or replying to the comments, you can request another review from me to trigger a new automated review.
| if (value == null) { | ||
| return null; | ||
| } | ||
| if (type instanceof Types.TimestampType) { |
There was a problem hiding this comment.
[P1] Iceberg timestamps stay in microseconds
Finding: GenericParquetReaders returns Iceberg DATE/TIME/TIMESTAMP values in numeric day or microsecond units. This branch only converts Java date-time objects and returns numeric values unchanged, so a real TimestampType used by timestampSpec reaches Druid as microseconds while Druid's numeric timestamp parsing treats it as milliseconds. V2 ingestion therefore assigns event times roughly 1,000 times too large, and date fields have the analogous day-unit mismatch.
Suggestion: Convert Iceberg temporal values by type to the millisecond representation used by the existing Parquet and Arrow readers, and add V2 coverage for timestamp and date columns.
| } | ||
| if (inputFormat instanceof NestedInputFormat) { | ||
| JSONPathSpec flattenSpec = ((NestedInputFormat) inputFormat).getFlattenSpec(); | ||
| if (flattenSpec != null && !flattenSpec.getFields().isEmpty()) { |
There was a problem hiding this comment.
[P2] Reject unsupported empty flatten specs
Finding: A valid NestedInputFormat can carry JSONPathSpec with useFieldDiscovery=false and an empty fields list. Druid's ObjectFlatteners treats that as an empty flattened row, but this guard rejects only non-empty fields, so the native reader ignores the flattenSpec and emits all schema fields. V2 ingestion can consequently accept rows and derive dimensions that the V1 path would omit or fail on.
Suggestion: Reject every non-null flattenSpec configuration the native reader cannot implement, including useFieldDiscovery=false with no fields, or apply the same flattening behavior before row conversion.
Fixes #19190
Description
This PR fixes Iceberg delete semantics which was previously broken for v2 tables. No spec changes are required. The same ingestion spec works for V1 and V2 tables. Delete detection is automatic at planning time.
In the V2 path, each
FileScanTaskis encoded as anIcebergFileTaskInputSource, containing the data file path, format, size, delete file metadata (paths, content type, equality field IDs), and the table schema captured as JSON at planning time.IcebergNativeRecordReader processes each split:
- Position delete files: Rows to skip are read and filtered to only positions matching the current data file path.
- Equality delete files : Rows of key tuples are read per equality delete file.
MapBasedInputRowviaIcebergRecordConverterFileIO is resolved per path: local paths use
WarehouseFileIO, non-local paths (HDFS, S3) fall back tocatalog.loadTable().io()using the namespace and table name passed through the split.Release note
The
druid-iceberg-extensionsinput source now correctly handles Iceberg V2 tables that contain row-level delete files. Previously, position deletes and equality deletes were silently ignored, causing deleted rows to appear in ingested segments. Druid now auto-detects delete files at planning time and applies them during ingestion with no spec changes required.Key changed/added classes in this PR
IcebergCatalogIcebergInputSourceThis PR has: