Skip to content

Commit fb7cc83

Browse files
authored
feat: read v3 row lineage metadata columns (#822)
Synthesize _row_id and _last_updated_sequence_number when reading data files that omit row lineage columns, and preserve physical non-null lineage values when present. Carry inherited data sequence numbers on DataFile metadata so scan planning and readers follow the same model as Java.
1 parent f9193ad commit fb7cc83

22 files changed

Lines changed: 630 additions & 28 deletions

src/iceberg/arrow/metadata_column_util.cc

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,38 @@
2424

2525
#include "iceberg/arrow/arrow_status_internal.h"
2626
#include "iceberg/arrow/metadata_column_util_internal.h"
27+
#include "iceberg/metadata_columns.h"
28+
#include "iceberg/util/checked_cast.h"
2729

2830
namespace iceberg::arrow {
2931

32+
bool CanInheritRowLineageValue(int32_t field_id,
33+
const MetadataColumnContext& metadata_context) {
34+
if (field_id == MetadataColumns::kRowIdColumnId) {
35+
return metadata_context.first_row_id.has_value();
36+
}
37+
if (field_id == MetadataColumns::kLastUpdatedSequenceNumberColumnId) {
38+
return metadata_context.data_sequence_number.has_value();
39+
}
40+
return false;
41+
}
42+
43+
Status AppendInheritedRowLineageValue(int32_t field_id,
44+
const MetadataColumnContext& metadata_context,
45+
::arrow::ArrayBuilder* array_builder) {
46+
auto* int_builder = internal::checked_cast<::arrow::Int64Builder*>(array_builder);
47+
if (!CanInheritRowLineageValue(field_id, metadata_context)) {
48+
ICEBERG_ARROW_RETURN_NOT_OK(int_builder->AppendNull());
49+
} else if (field_id == MetadataColumns::kRowIdColumnId) {
50+
ICEBERG_ARROW_RETURN_NOT_OK(int_builder->Append(
51+
metadata_context.first_row_id.value() + metadata_context.next_file_pos));
52+
} else {
53+
ICEBERG_ARROW_RETURN_NOT_OK(
54+
int_builder->Append(metadata_context.data_sequence_number.value()));
55+
}
56+
return {};
57+
}
58+
3059
Result<std::shared_ptr<::arrow::Array>> MakeFilePathArray(const std::string& file_path,
3160
int64_t num_rows,
3261
::arrow::MemoryPool* pool) {
@@ -53,4 +82,41 @@ Result<std::shared_ptr<::arrow::Array>> MakeRowPositionArray(int64_t start_posit
5382
return array;
5483
}
5584

85+
Result<std::shared_ptr<::arrow::Array>> MakeRowIdArray(
86+
std::optional<int64_t> first_row_id, int64_t start_position, int64_t num_rows,
87+
::arrow::MemoryPool* pool) {
88+
::arrow::Int64Builder builder(pool);
89+
ICEBERG_ARROW_RETURN_NOT_OK(builder.Reserve(num_rows));
90+
if (!first_row_id.has_value()) {
91+
ICEBERG_ARROW_RETURN_NOT_OK(builder.AppendNulls(num_rows));
92+
} else {
93+
for (int64_t row_index = 0; row_index < num_rows; ++row_index) {
94+
ICEBERG_ARROW_RETURN_NOT_OK(
95+
builder.Append(first_row_id.value() + start_position + row_index));
96+
}
97+
}
98+
99+
std::shared_ptr<::arrow::Array> array;
100+
ICEBERG_ARROW_RETURN_NOT_OK(builder.Finish(&array));
101+
return array;
102+
}
103+
104+
Result<std::shared_ptr<::arrow::Array>> MakeLastUpdatedSequenceNumberArray(
105+
std::optional<int64_t> data_sequence_number, int64_t num_rows,
106+
::arrow::MemoryPool* pool) {
107+
::arrow::Int64Builder builder(pool);
108+
ICEBERG_ARROW_RETURN_NOT_OK(builder.Reserve(num_rows));
109+
if (!data_sequence_number.has_value()) {
110+
ICEBERG_ARROW_RETURN_NOT_OK(builder.AppendNulls(num_rows));
111+
} else {
112+
for (int64_t row_index = 0; row_index < num_rows; ++row_index) {
113+
ICEBERG_ARROW_RETURN_NOT_OK(builder.Append(data_sequence_number.value()));
114+
}
115+
}
116+
117+
std::shared_ptr<::arrow::Array> array;
118+
ICEBERG_ARROW_RETURN_NOT_OK(builder.Finish(&array));
119+
return array;
120+
}
121+
56122
} // namespace iceberg::arrow

src/iceberg/arrow/metadata_column_util_internal.h

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,36 @@
2424

2525
#include <cstdint>
2626
#include <memory>
27+
#include <optional>
2728
#include <string>
2829

2930
#include <arrow/type_fwd.h>
3031

3132
#include "iceberg/result.h"
3233

34+
namespace arrow {
35+
class ArrayBuilder;
36+
} // namespace arrow
37+
3338
namespace iceberg::arrow {
3439

3540
/// \brief Context for populating metadata columns during reading.
3641
struct MetadataColumnContext {
3742
std::string file_path; // The file path to populate _file column.
3843
int64_t next_file_pos = 0; // The next row position for populating _pos column.
44+
std::optional<int64_t> first_row_id; // First row ID used to populate _row_id.
45+
std::optional<int64_t> data_sequence_number; // Data sequence number for lineage.
3946
};
4047

48+
/// \brief Check if row lineage metadata can be inherited for the column.
49+
bool CanInheritRowLineageValue(int32_t field_id,
50+
const MetadataColumnContext& metadata_context);
51+
52+
/// \brief Append one inherited row lineage value, or null if unavailable.
53+
Status AppendInheritedRowLineageValue(int32_t field_id,
54+
const MetadataColumnContext& metadata_context,
55+
::arrow::ArrayBuilder* array_builder);
56+
4157
/// \brief Create a constant string array for _file column.
4258
///
4359
/// Creates an Arrow StringArray where all values are the same file path string.
@@ -62,4 +78,25 @@ Result<std::shared_ptr<::arrow::Array>> MakeRowPositionArray(int64_t start_posit
6278
int64_t num_rows,
6379
::arrow::MemoryPool* pool);
6480

81+
/// \brief Create a row ID array for _row_id column.
82+
///
83+
/// \param first_row_id First row ID of the data file.
84+
/// \param start_position Starting row position (inclusive).
85+
/// \param num_rows Number of rows in the batch.
86+
/// \param pool Arrow memory pool.
87+
/// \return Arrow Int64Array for _row_id.
88+
Result<std::shared_ptr<::arrow::Array>> MakeRowIdArray(
89+
std::optional<int64_t> first_row_id, int64_t start_position, int64_t num_rows,
90+
::arrow::MemoryPool* pool);
91+
92+
/// \brief Create a sequence number array for _last_updated_sequence_number column.
93+
///
94+
/// \param data_sequence_number Data sequence number of the data file.
95+
/// \param num_rows Number of rows in the batch.
96+
/// \param pool Arrow memory pool.
97+
/// \return Arrow Int64Array for _last_updated_sequence_number.
98+
Result<std::shared_ptr<::arrow::Array>> MakeLastUpdatedSequenceNumberArray(
99+
std::optional<int64_t> data_sequence_number, int64_t num_rows,
100+
::arrow::MemoryPool* pool);
101+
65102
} // namespace iceberg::arrow

src/iceberg/avro/avro_data_util.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ Status AppendStructToBuilder(const ::avro::NodePtr& avro_node,
9797
} else if (field_id == MetadataColumns::kFilePositionColumnId) {
9898
auto int_builder = internal::checked_cast<::arrow::Int64Builder*>(field_builder);
9999
ICEBERG_ARROW_RETURN_NOT_OK(int_builder->Append(metadata_context.next_file_pos));
100+
} else if (MetadataColumns::IsRowLineageColumn(field_id)) {
101+
ICEBERG_RETURN_UNEXPECTED(arrow::AppendInheritedRowLineageValue(
102+
field_id, metadata_context, field_builder));
100103
} else {
101104
return NotSupported("Unsupported metadata column field id: {}", field_id);
102105
}
@@ -463,9 +466,16 @@ Status AppendFieldToBuilder(const ::avro::NodePtr& avro_node,
463466
return {};
464467
}
465468

469+
const bool is_row_lineage =
470+
MetadataColumns::IsRowLineageColumn(projected_field.field_id());
471+
466472
if (avro_node->type() == ::avro::AVRO_UNION) {
467473
size_t branch = avro_datum.unionBranch();
468474
if (avro_node->leafAt(branch)->type() == ::avro::AVRO_NULL) {
475+
if (is_row_lineage) {
476+
return arrow::AppendInheritedRowLineageValue(projected_field.field_id(),
477+
metadata_context, array_builder);
478+
}
469479
ICEBERG_ARROW_RETURN_NOT_OK(array_builder->AppendNull());
470480
return {};
471481
} else {

src/iceberg/avro/avro_direct_decoder.cc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ Status DecodeStructToBuilder(const ::avro::NodePtr& avro_node, ::avro::Decoder&
218218
} else if (field_id == MetadataColumns::kFilePositionColumnId) {
219219
auto int_builder = internal::checked_cast<::arrow::Int64Builder*>(field_builder);
220220
ICEBERG_ARROW_RETURN_NOT_OK(int_builder->Append(metadata_context.next_file_pos));
221+
} else if (MetadataColumns::IsRowLineageColumn(field_id)) {
222+
ICEBERG_RETURN_UNEXPECTED(arrow::AppendInheritedRowLineageValue(
223+
field_id, metadata_context, field_builder));
221224
} else {
222225
return NotSupported("Unsupported metadata column field id: {}", field_id);
223226
}
@@ -594,6 +597,9 @@ Status DecodeFieldToBuilder(const ::avro::NodePtr& avro_node, ::avro::Decoder& d
594597
return {};
595598
}
596599

600+
const bool is_row_lineage =
601+
MetadataColumns::IsRowLineageColumn(projected_field.field_id());
602+
597603
if (avro_node->type() == ::avro::AVRO_UNION) {
598604
const size_t branch_index = decoder.decodeUnionIndex();
599605

@@ -606,6 +612,10 @@ Status DecodeFieldToBuilder(const ::avro::NodePtr& avro_node, ::avro::Decoder& d
606612

607613
const auto& branch_node = avro_node->leafAt(branch_index);
608614
if (branch_node->type() == ::avro::AVRO_NULL) {
615+
if (is_row_lineage) {
616+
return arrow::AppendInheritedRowLineageValue(projected_field.field_id(),
617+
metadata_context, array_builder);
618+
}
609619
ICEBERG_ARROW_RETURN_NOT_OK(array_builder->AppendNull());
610620
return {};
611621
} else {

src/iceberg/avro/avro_reader.cc

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ bool HasRowPositionColumn(const Schema& schema) {
6868
return false;
6969
}
7070

71+
bool HasRowIdColumn(const Schema& schema) {
72+
for (const auto& field : schema.fields()) {
73+
if (field.field_id() == MetadataColumns::kRowIdColumnId) {
74+
return true;
75+
}
76+
}
77+
return false;
78+
}
79+
7180
// Abstract base class for Avro read backends.
7281
class AvroReadBackend {
7382
public:
@@ -332,9 +341,17 @@ class AvroReader::Impl {
332341
return NotSupported(
333342
"Reading '_pos' metadata column with split is not supported for Avro files.");
334343
}
344+
if (options.split->offset != 0 && HasRowIdColumn(*read_schema_)) {
345+
return NotSupported(
346+
"Reading '_row_id' metadata column with split is not supported for Avro "
347+
"files.");
348+
}
335349
}
336350

337-
metadata_context_ = {.file_path = options.path, .next_file_pos = 0};
351+
metadata_context_ = {.file_path = options.path,
352+
.next_file_pos = 0,
353+
.first_row_id = options.first_row_id,
354+
.data_sequence_number = options.data_sequence_number};
338355

339356
return {};
340357
}

src/iceberg/catalog/rest/json_serde.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ Result<std::vector<std::shared_ptr<FileScanTask>>> FileScanTasksFromJson(
323323
GetJsonValue<nlohmann::json>(task_json, kDataFile));
324324
ICEBERG_ASSIGN_OR_RAISE(
325325
auto data_file, DataFileFromJson(data_file_json, partition_spec_by_id, schema));
326+
// FIXME: REST scan-task DataFile JSON currently carries first-row-id,
327+
// but not the manifest-entry data sequence number. Until the REST API exposes
328+
// it, REST-planned tasks cannot inherit _last_updated_sequence_number.
329+
// See https://github.com/apache/iceberg-cpp/issues/834.
326330

327331
std::vector<std::shared_ptr<DataFile>> task_delete_files;
328332
if (task_json.contains(kDeleteFileReferences) &&

src/iceberg/data/file_scan_task_reader.cc

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ ReaderOptions MakeReaderOptions(const DataFile& data_file, std::shared_ptr<FileI
4343
std::shared_ptr<Schema> projection,
4444
std::shared_ptr<Expression> filter,
4545
std::shared_ptr<NameMapping> name_mapping,
46-
ReaderProperties properties) {
46+
ReaderProperties properties,
47+
std::optional<int64_t> first_row_id,
48+
std::optional<int64_t> data_sequence_number) {
4749
return ReaderOptions{
4850
.path = data_file.file_path,
4951
.length = static_cast<size_t>(data_file.file_size_in_bytes),
5052
.io = std::move(io),
5153
.projection = std::move(projection),
5254
.filter = std::move(filter),
5355
.name_mapping = std::move(name_mapping),
56+
.first_row_id = first_row_id,
57+
.data_sequence_number = data_sequence_number,
5458
.properties = std::move(properties),
5559
};
5660
}
@@ -161,9 +165,9 @@ class FileScanTaskReader::Impl {
161165
data_file->file_size_in_bytes);
162166

163167
if (task.delete_files().empty()) {
164-
auto options =
165-
MakeReaderOptions(*data_file, io_, projected_schema_, task.residual_filter(),
166-
name_mapping_, properties_);
168+
auto options = MakeReaderOptions(
169+
*data_file, io_, projected_schema_, task.residual_filter(), name_mapping_,
170+
properties_, data_file->first_row_id, data_file->data_sequence_number);
167171
ICEBERG_ASSIGN_OR_RAISE(
168172
auto reader, ReaderFactoryRegistry::Open(data_file->file_format, options));
169173
return MakeArrowArrayStream(std::move(reader));
@@ -187,8 +191,9 @@ class FileScanTaskReader::Impl {
187191
ProjectionContext::Make(*required_schema, *projected_schema_,
188192
project_batch_function));
189193

190-
auto options = MakeReaderOptions(*data_file, io_, required_schema,
191-
task.residual_filter(), name_mapping_, properties_);
194+
auto options = MakeReaderOptions(
195+
*data_file, io_, required_schema, task.residual_filter(), name_mapping_,
196+
properties_, data_file->first_row_id, data_file->data_sequence_number);
192197
ICEBERG_ASSIGN_OR_RAISE(auto reader,
193198
ReaderFactoryRegistry::Open(data_file->file_format, options));
194199

src/iceberg/file_reader.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
/// \file iceberg/file_reader.h
2323
/// Reader interface for file formats like Parquet, Avro and ORC.
2424

25+
#include <cstdint>
2526
#include <functional>
2627
#include <memory>
2728
#include <optional>
@@ -105,6 +106,10 @@ struct ICEBERG_EXPORT ReaderOptions {
105106
/// \brief Name mapping for schema evolution compatibility. Used when reading files
106107
/// that may have different field names than the current schema.
107108
std::shared_ptr<class NameMapping> name_mapping;
109+
/// \brief First row ID inherited by the data file, if assigned.
110+
std::optional<int64_t> first_row_id;
111+
/// \brief Data sequence number inherited by the manifest entry, if assigned.
112+
std::optional<int64_t> data_sequence_number;
108113
/// \brief Format-specific or implementation-specific properties.
109114
ReaderProperties properties;
110115
};

src/iceberg/inheritable_metadata.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ Status BaseInheritableMetadata::Apply(ManifestEntry& entry) {
6464

6565
if (entry.data_file) {
6666
entry.data_file->partition_spec_id = spec_id_;
67+
entry.data_file->data_sequence_number = entry.sequence_number;
68+
// TODO(gangwu): Also propagate file_sequence_number and manifest_location
69+
// when C++ adds consumers that need Java-style inherited file metadata.
6770
} else {
6871
return InvalidManifest("Manifest entry has no data file");
6972
}

src/iceberg/manifest/manifest_entry.h

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,18 @@ struct ICEBERG_EXPORT DataFile {
174174
/// present
175175
std::optional<int64_t> content_size_in_bytes;
176176

177+
/// \note Fields defined below are inherited and not persisted to the DataFile struct.
178+
177179
/// \brief Partition spec id for this data file.
178-
/// \note This field is for internal use only and will not be persisted to manifest
179-
/// entry.
180+
/// \note This inherited field is for internal use only and will not be persisted to
181+
/// the DataFile struct.
180182
std::optional<int32_t> partition_spec_id;
181183

184+
/// \brief Data sequence number for this data file.
185+
/// \note This inherited field is for internal use only and will not be persisted to
186+
/// the DataFile struct.
187+
std::optional<int64_t> data_sequence_number;
188+
182189
static constexpr int32_t kContentFieldId = 134;
183190
inline static const SchemaField kContent = SchemaField::MakeOptional(
184191
kContentFieldId, "content", int32(),

0 commit comments

Comments
 (0)