2424#include < optional>
2525#include < ranges>
2626#include < string>
27+ #include < unordered_map>
2728#include < unordered_set>
2829#include < vector>
2930
4546#include " iceberg/table_properties.h"
4647#include " iceberg/test/executor.h"
4748#include " iceberg/test/matchers.h"
49+ #include " iceberg/test/retry.h"
4850#include " iceberg/test/update_test_base.h"
4951#include " iceberg/transaction.h"
5052#include " iceberg/update/fast_append.h"
53+ #include " iceberg/update/merge_append.h"
54+ #include " iceberg/update/snapshot_manager.h"
5155#include " iceberg/update/update_properties.h"
5256#include " iceberg/util/macros.h"
5357
@@ -276,6 +280,13 @@ class MergingSnapshotUpdateTest : public MinimalUpdateTestBase {
276280 return TestOverwriteUpdate::Make (TableName (), table_);
277281 }
278282
283+ void UpgradeTableToV3 () {
284+ ICEBERG_UNWRAP_OR_FAIL (auto props, table_->NewUpdateProperties ());
285+ props->Set (TableProperties::kFormatVersion .key (), " 3" );
286+ EXPECT_THAT (props->Commit (), IsOk ());
287+ EXPECT_THAT (table_->Refresh (), IsOk ());
288+ }
289+
279290 void SetTableFormatVersion (int8_t format_version) {
280291 table_->metadata ()->format_version = format_version;
281292 }
@@ -311,6 +322,22 @@ class MergingSnapshotUpdateTest : public MinimalUpdateTestBase {
311322 return result;
312323 }
313324
325+ Result<std::unordered_map<std::string, std::optional<int64_t >>> DataFileFirstRowIds (
326+ const std::shared_ptr<Snapshot>& snapshot, const TableMetadata& metadata) {
327+ SnapshotCache snapshot_cache (snapshot.get ());
328+ ICEBERG_ASSIGN_OR_RAISE (auto manifest_range, snapshot_cache.DataManifests (file_io_));
329+ std::vector<ManifestFile> manifests (manifest_range.begin (), manifest_range.end ());
330+ ICEBERG_ASSIGN_OR_RAISE (auto entries, ReadAllEntries (manifests, metadata));
331+
332+ std::unordered_map<std::string, std::optional<int64_t >> first_row_ids;
333+ for (const auto & entry : entries) {
334+ if (entry.data_file != nullptr ) {
335+ first_row_ids.emplace (entry.data_file ->file_path , entry.data_file ->first_row_id );
336+ }
337+ }
338+ return first_row_ids;
339+ }
340+
314341 // Write a manifest file containing the given data files.
315342 // Returns a ManifestFile with added_snapshot_id = kInvalidSnapshotId so it
316343 // is eligible for snapshot ID inheritance.
@@ -448,10 +475,7 @@ TEST_F(MergingSnapshotUpdateTest, CommitNewDataFile) {
448475}
449476
450477TEST_F (MergingSnapshotUpdateTest, CommitV3NewDataFileAssignsRowLineage) {
451- ICEBERG_UNWRAP_OR_FAIL (auto props, table_->NewUpdateProperties ());
452- props->Set (TableProperties::kFormatVersion .key (), " 3" );
453- EXPECT_THAT (props->Commit (), IsOk ());
454- EXPECT_THAT (table_->Refresh (), IsOk ());
478+ UpgradeTableToV3 ();
455479
456480 ICEBERG_UNWRAP_OR_FAIL (auto op, NewMergeAppend ());
457481 EXPECT_THAT (op->AddFile (file_a_), IsOk ());
@@ -466,6 +490,110 @@ TEST_F(MergingSnapshotUpdateTest, CommitV3NewDataFileAssignsRowLineage) {
466490 EXPECT_EQ (table_->metadata ()->next_row_id , 100 );
467491}
468492
493+ TEST_F (MergingSnapshotUpdateTest, V3MultiFileRowIds) {
494+ UpgradeTableToV3 ();
495+
496+ ICEBERG_UNWRAP_OR_FAIL (auto op, NewMergeAppend ());
497+ EXPECT_THAT (op->AddFile (file_a_), IsOk ());
498+ EXPECT_THAT (op->AddFile (file_b_), IsOk ());
499+ EXPECT_THAT (op->Commit (), IsOk ());
500+
501+ EXPECT_THAT (table_->Refresh (), IsOk ());
502+ ICEBERG_UNWRAP_OR_FAIL (auto snapshot, table_->current_snapshot ());
503+ ICEBERG_UNWRAP_OR_FAIL (auto first_row_id, snapshot->FirstRowId ());
504+ ICEBERG_UNWRAP_OR_FAIL (auto added_rows, snapshot->AddedRows ());
505+ EXPECT_EQ (first_row_id, std::make_optional<int64_t >(0 ));
506+ EXPECT_EQ (added_rows, std::make_optional<int64_t >(200 ));
507+ EXPECT_EQ (table_->metadata ()->next_row_id , 200 );
508+
509+ ICEBERG_UNWRAP_OR_FAIL (auto first_row_ids,
510+ DataFileFirstRowIds (snapshot, *table_->metadata ()));
511+ EXPECT_EQ (first_row_ids.at (file_a_->file_path ), std::make_optional<int64_t >(0 ));
512+ EXPECT_EQ (first_row_ids.at (file_b_->file_path ), std::make_optional<int64_t >(100 ));
513+ }
514+
515+ TEST_F (MergingSnapshotUpdateTest, V3BranchRowIds) {
516+ UpgradeTableToV3 ();
517+ CommitFileA ();
518+
519+ ICEBERG_UNWRAP_OR_FAIL (auto starting_snapshot, table_->current_snapshot ());
520+ const auto starting_snapshot_id = starting_snapshot->snapshot_id ;
521+ const auto starting_next_row_id = table_->metadata ()->next_row_id ;
522+
523+ ICEBERG_UNWRAP_OR_FAIL (auto manager, table_->NewSnapshotManager ());
524+ manager->CreateBranch (" audit" , starting_snapshot_id);
525+ EXPECT_THAT (manager->Commit (), IsOk ());
526+ EXPECT_THAT (table_->Refresh (), IsOk ());
527+
528+ ICEBERG_UNWRAP_OR_FAIL (auto op, NewMergeAppend ());
529+ op->SetTargetBranch (" audit" );
530+ EXPECT_THAT (op->AddFile (file_b_), IsOk ());
531+ EXPECT_THAT (op->Commit (), IsOk ());
532+
533+ EXPECT_THAT (table_->Refresh (), IsOk ());
534+ EXPECT_EQ (table_->metadata ()->next_row_id ,
535+ starting_next_row_id + file_b_->record_count );
536+ ICEBERG_UNWRAP_OR_FAIL (auto current_snapshot, table_->current_snapshot ());
537+ EXPECT_EQ (current_snapshot->snapshot_id , starting_snapshot_id);
538+
539+ auto ref_it = table_->metadata ()->refs .find (" audit" );
540+ ASSERT_NE (ref_it, table_->metadata ()->refs .end ());
541+ ICEBERG_UNWRAP_OR_FAIL (auto branch_snapshot,
542+ table_->metadata ()->SnapshotById (ref_it->second ->snapshot_id ));
543+ ICEBERG_UNWRAP_OR_FAIL (auto first_row_id, branch_snapshot->FirstRowId ());
544+ ICEBERG_UNWRAP_OR_FAIL (auto added_rows, branch_snapshot->AddedRows ());
545+ EXPECT_EQ (first_row_id, std::make_optional (starting_next_row_id));
546+ EXPECT_EQ (added_rows, std::make_optional (file_b_->record_count ));
547+
548+ ICEBERG_UNWRAP_OR_FAIL (auto first_row_ids,
549+ DataFileFirstRowIds (branch_snapshot, *table_->metadata ()));
550+ EXPECT_EQ (first_row_ids.at (file_a_->file_path ), std::make_optional<int64_t >(0 ));
551+ EXPECT_EQ (first_row_ids.at (file_b_->file_path ),
552+ std::make_optional (starting_next_row_id));
553+ }
554+
555+ // A staged v3 append must reassign row IDs when a concurrent commit advances next_row_id.
556+ TEST_F (MergingSnapshotUpdateTest, V3RetryRowIds) {
557+ UpgradeTableToV3 ();
558+ test::FakeRetryEnvironment fake_retry;
559+ ScopedRetryTestHooks retry_hooks (fake_retry.hooks ());
560+
561+ // Stage file_a_ with first_row_id = 0, but do not commit the transaction yet.
562+ ICEBERG_UNWRAP_OR_FAIL (auto txn, table_->NewTransaction ());
563+ ICEBERG_UNWRAP_OR_FAIL (auto append, txn->NewMergeAppend ());
564+ append->AppendFile (file_a_);
565+ EXPECT_THAT (append->Commit (), IsOk ());
566+
567+ ICEBERG_UNWRAP_OR_FAIL (auto staged_snapshot, txn->current ().Snapshot ());
568+ ICEBERG_UNWRAP_OR_FAIL (auto staged_first_row_id, staged_snapshot->FirstRowId ());
569+ EXPECT_EQ (staged_first_row_id, std::make_optional<int64_t >(0 ));
570+
571+ // Commit file_b_ first, advancing table next_row_id and making file_a_'s row IDs stale.
572+ ICEBERG_UNWRAP_OR_FAIL (auto concurrent, table_->NewMergeAppend ());
573+ concurrent->AppendFile (file_b_);
574+ EXPECT_THAT (concurrent->Commit (), IsOk ());
575+ auto after_concurrent = ReloadMetadata ();
576+ EXPECT_EQ (after_concurrent->next_row_id , file_b_->record_count );
577+
578+ // The original transaction must retry and reassign file_a_ after file_b_'s row IDs.
579+ ICEBERG_UNWRAP_OR_FAIL (auto committed_table, txn->Commit ());
580+ EXPECT_FALSE (fake_retry.sleep_durations ().empty ());
581+ EXPECT_EQ (committed_table->metadata ()->next_row_id ,
582+ file_b_->record_count + file_a_->record_count );
583+
584+ ICEBERG_UNWRAP_OR_FAIL (auto snapshot, committed_table->current_snapshot ());
585+ ICEBERG_UNWRAP_OR_FAIL (auto first_row_id, snapshot->FirstRowId ());
586+ ICEBERG_UNWRAP_OR_FAIL (auto added_rows, snapshot->AddedRows ());
587+ EXPECT_EQ (first_row_id, std::make_optional (file_b_->record_count ));
588+ EXPECT_EQ (added_rows, std::make_optional (file_a_->record_count ));
589+
590+ ICEBERG_UNWRAP_OR_FAIL (auto first_row_ids,
591+ DataFileFirstRowIds (snapshot, *committed_table->metadata ()));
592+ EXPECT_EQ (first_row_ids.at (file_b_->file_path ), std::make_optional<int64_t >(0 ));
593+ EXPECT_EQ (first_row_ids.at (file_a_->file_path ),
594+ std::make_optional (file_b_->record_count ));
595+ }
596+
469597TEST_F (MergingSnapshotUpdateTest, CommitMultipleDataFiles) {
470598 ICEBERG_UNWRAP_OR_FAIL (auto op, NewMergeAppend ());
471599 EXPECT_THAT (op->AddFile (file_a_), IsOk ());
0 commit comments