-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add row lineage metadata columns to Iceberg reader #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "velox/connectors/hive/iceberg/IcebergMetadataColumns.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "velox/connectors/hive/iceberg/IcebergSplit.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "velox/dwio/common/BufferUtil.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "velox/vector/DecodedVector.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| using namespace facebook::velox::dwio::common; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -60,12 +61,76 @@ void IcebergSplitReader::prepareSplit( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| std::shared_ptr<common::MetadataFilter> metadataFilter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dwio::common::RuntimeStatistics& runtimeStats, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const folly::F14FastMap<std::string, std::string>& fileReadOps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Expand the file schema to include row lineage metadata columns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (_row_id and _last_updated_sequence_number) if they're requested | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // in the output. These are hidden metadata columns physically stored | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // in Iceberg V3 data files but not listed in the table's logical | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // schema (dataColumns). The Parquet reader needs them in the file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // schema to read them from the file. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto fileSchema = baseReaderOpts_.fileSchema(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (fileSchema) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto names = fileSchema->names(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto types = fileSchema->children(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bool modified = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const auto* colName : | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {IcebergMetadataColumn::kRowIdColumnName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IcebergMetadataColumn::kLastUpdatedSequenceNumberColumnName}) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (readerOutputType_->containsChild(colName) && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !fileSchema->containsChild(colName)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| names.push_back(std::string(colName)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| types.push_back(BIGINT()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modified = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (modified) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| baseReaderOpts_.setFileSchema(ROW(std::move(names), std::move(types))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createReader(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (emptySplit_) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+85
to
93
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Initialize row lineage fields BEFORE getAdaptedRowType() because | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // adaptColumns() needs them to decide how to handle missing columns. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstRowId_ = std::nullopt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (auto it = hiveSplit_->infoColumns.find("$first_row_id"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it != hiveSplit_->infoColumns.end()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto value = folly::to<int64_t>(it->second); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value >= 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstRowId_ = value; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dataSequenceNumber_ = std::nullopt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (auto it = hiveSplit_->infoColumns.find("$data_sequence_number"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it != hiveSplit_->infoColumns.end()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dataSequenceNumber_ = folly::to<int64_t>(it->second); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto rowType = getAdaptedRowType(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After adaptColumns(), check if row lineage columns need null replacement | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // in next(). If adaptColumns() set them as constants (column missing from | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // file), no replacement is needed. If the column is read from the file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (not constant), null values must be replaced per spec. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastUpdatedSeqNumOutputIndex_ = std::nullopt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (dataSequenceNumber_.has_value()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto* seqNumSpec = scanSpec_->childByName( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IcebergMetadataColumn::kLastUpdatedSequenceNumberColumnName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (seqNumSpec && !seqNumSpec->isConstant()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastUpdatedSeqNumOutputIndex_ = readerOutputType_->getChildIdxIfExists( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IcebergMetadataColumn::kLastUpdatedSequenceNumberColumnName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rowIdOutputIndex_ = std::nullopt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (firstRowId_.has_value()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rowIdOutputIndex_ = readerOutputType_->getChildIdxIfExists( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IcebergMetadataColumn::kRowIdColumnName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (checkIfSplitIsEmpty(runtimeStats)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VELOX_CHECK(emptySplit_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -160,6 +225,131 @@ uint64_t IcebergSplitReader::next(uint64_t size, VectorPtr& output) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto rowsScanned = baseRowReader_->next(actualSize, output, &mutation); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // For Iceberg V3 row lineage: replace null values in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // _last_updated_sequence_number with the data sequence number from the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // file's manifest entry. Per the spec, null means the value should be | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // inherited from the manifest entry's sequence number. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lastUpdatedSeqNumOutputIndex_.has_value() && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dataSequenceNumber_.has_value() && rowsScanned > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto* rowOutput = output->as<RowVector>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rowOutput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto& seqNumChild = rowOutput->childAt(*lastUpdatedSeqNumOutputIndex_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Load lazy vector - the Parquet reader wraps columns in LazyVector. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seqNumChild = BaseVector::loadedVectorShared(seqNumChild); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto vectorSize = seqNumChild->size(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (seqNumChild->isConstantEncoding()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (seqNumChild->isNullAt(0)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seqNumChild = std::make_shared<ConstantVector<int64_t>>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| connectorQueryCtx_->memoryPool(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vectorSize, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BIGINT(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| static_cast<int64_t>(*dataSequenceNumber_)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (seqNumChild->mayHaveNulls()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use DecodedVector to handle any encoding (flat, dictionary, etc.). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DecodedVector decoded(*seqNumChild); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (decoded.mayHaveNulls()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto pool = connectorQueryCtx_->memoryPool(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auto newFlat = BaseVector::create<FlatVector<int64_t>>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BIGINT(), vectorSize, pool); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (vector_size_t i = 0; i < vectorSize; ++i) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (decoded.isNullAt(i)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| newFlat->set(i, *dataSequenceNumber_); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| newFlat->set(i, decoded.valueAt<int64_t>(i)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seqNumChild = newFlat; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
226
to
+266
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (seqNumChild->mayHaveNulls()) { | |
| // Handle any vector encoding (flat, dictionary, etc.) by creating | |
| // a new flat vector with null values replaced. | |
| auto pool = connectorQueryCtx_->memoryPool(); | |
| auto* simpleVec = seqNumChild->as<SimpleVector<int64_t>>(); | |
| auto newFlat = | |
| BaseVector::create<FlatVector<int64_t>>(BIGINT(), vectorSize, pool); | |
| for (vector_size_t i = 0; i < vectorSize; ++i) { | |
| if (simpleVec->isNullAt(i)) { | |
| newFlat->set(i, *dataSequenceNumber_); | |
| } else { | |
| newFlat->set(i, simpleVec->valueAt(i)); | |
| } | |
| } | |
| seqNumChild = newFlat; | |
| } | |
| } else if (seqNumChild->mayHaveNulls()) { | |
| // Fast-path: 'mayHaveNulls' is conservative; only rewrite when we | |
| // actually have nulls to replace. | |
| if (seqNumChild->hasNulls()) { | |
| // Handle any vector encoding (flat, dictionary, etc.) by creating | |
| // a new flat vector with null values replaced. | |
| auto pool = connectorQueryCtx_->memoryPool(); | |
| auto* simpleVec = seqNumChild->as<SimpleVector<int64_t>>(); | |
| auto newFlat = | |
| BaseVector::create<FlatVector<int64_t>>(BIGINT(), vectorSize, pool); | |
| for (vector_size_t i = 0; i < vectorSize; ++i) { | |
| if (simpleVec->isNullAt(i)) { | |
| newFlat->set(i, *dataSequenceNumber_); | |
| } else { | |
| newFlat->set(i, simpleVec->valueAt(i)); | |
| } | |
| } | |
| seqNumChild = newFlat; | |
| } | |
| } |
Copilot
AI
Mar 11, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_row_id is computed using sequential output indices (e.g., batchStartPos + i / batchStartPos + j), but RowReader::next() can return a compacted output (e.g., dictionary-wrapped) after applying scan filters and deletions. In those cases, output row 0 may correspond to an arbitrary original row position within the scanned batch, so this computation can produce incorrect _row_id values. Consider deriving the per-output-row file position from the vector's dictionary indices (e.g., via DecodedVector) and computing first_row_id + (batchStartPos + originalRowOffset) for each output row, instead of assuming output index == file position.
Copilot
AI
Mar 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _row_id computation currently assumes output row i corresponds to file position batchStartPos + i (or reconstructs positions using only the delete bitmap). This breaks as soon as any filter (or randomSkip) is applied because the output is dictionary-wrapped with indices into the scanned batch; positions are no longer contiguous and aren’t derivable from i. Consider using the dictionary indices (or a decoded vector) to map each output row back to its scanned-row offset j, then compute first_row_id + batchStartPos + j (this also removes the need for the separate delete-bitmap walk). Also, avoid using rowsScanned for vector sizing/iteration here for the same reason as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Guard against null colType when constructing a null constant for missing columns
In this fallback branch, colType may be null if the field name is not found in either readerOutputType_ or tableSchema. Passing a null type into createNullConstant(colType, ...) would likely crash. Consider adding a VELOX_CHECK to enforce non-null here or handling the null case explicitly with a clear error path instead of failing implicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new metadata column IDs for
_row_idand_last_updated_sequence_numberare hard-coded magic numbers. For consistency with the existing delete-file metadata IDs above, consider introducing namedstatic constexpr int32_tconstants (and ideally a short comment pointing to the Iceberg spec section/source for these IDs). This makes it easier to validate and update these values later.