@@ -943,3 +943,145 @@ async fn test_incremental_fixture_complex() {
943943 ] )
944944 . await ;
945945}
946+
947+ #[ tokio:: test]
948+ async fn test_incremental_scan_edge_cases ( ) {
949+ // This test covers several edge cases:
950+ // 1. Multiple data files added in separate snapshots
951+ // 2. Deletes spread across multiple data files
952+ // 3. Partial deletes from multiple files
953+ // 4. Cross-file delete operations in a single snapshot
954+ let fixture = IncrementalTestFixture :: new ( vec ! [
955+ // Snapshot 1: Empty starting point
956+ Operation :: Add ( vec![ ] , "empty.parquet" . to_string( ) ) ,
957+ // Snapshot 2: Add file A with 3 rows
958+ Operation :: Add (
959+ vec![ ( 1 , "a1" . to_string( ) ) , ( 2 , "a2" . to_string( ) ) , ( 3 , "a3" . to_string( ) ) ] ,
960+ "file-a.parquet" . to_string( ) ,
961+ ) ,
962+ // Snapshot 3: Add file B with 4 rows
963+ Operation :: Add (
964+ vec![
965+ ( 10 , "b1" . to_string( ) ) ,
966+ ( 20 , "b2" . to_string( ) ) ,
967+ ( 30 , "b3" . to_string( ) ) ,
968+ ( 40 , "b4" . to_string( ) ) ,
969+ ] ,
970+ "file-b.parquet" . to_string( ) ,
971+ ) ,
972+ // Snapshot 4: Partial delete from file A (delete middle row n=2)
973+ Operation :: Delete ( vec![ ( 1 , "file-a.parquet" . to_string( ) ) ] ) ,
974+ // Snapshot 5: Partial delete from file B (delete first and last rows n=10,40)
975+ Operation :: Delete ( vec![
976+ ( 0 , "file-b.parquet" . to_string( ) ) ,
977+ ( 3 , "file-b.parquet" . to_string( ) ) ,
978+ ] ) ,
979+ // Snapshot 6: Add file C with 2 rows
980+ Operation :: Add (
981+ vec![ ( 100 , "c1" . to_string( ) ) , ( 200 , "c2" . to_string( ) ) ] ,
982+ "file-c.parquet" . to_string( ) ,
983+ ) ,
984+ // Snapshot 7: Delete from multiple files in one snapshot (cross-file deletes)
985+ Operation :: Delete ( vec![
986+ ( 0 , "file-a.parquet" . to_string( ) ) , // n=1
987+ ( 1 , "file-b.parquet" . to_string( ) ) , // n=20
988+ ( 0 , "file-c.parquet" . to_string( ) ) , // n=100
989+ ] ) ,
990+ ] )
991+ . await ;
992+
993+ // Verify we have 7 snapshots
994+ let n_snapshots = fixture. table . metadata ( ) . snapshots ( ) . count ( ) ;
995+ assert_eq ! ( n_snapshots, 7 ) ;
996+
997+ let file_a_path = format ! ( "{}/data/file-a.parquet" , fixture. table_location) ;
998+ let file_b_path = format ! ( "{}/data/file-b.parquet" , fixture. table_location) ;
999+
1000+ // Test 1: Scan from snapshot 1 to 4
1001+ // Should see: file-a (1,2,3), file-b (10,20,30,40) added, then (2) deleted from file-a
1002+ // BUT: The row n=2 was added AFTER snapshot 1, so it won't show as a delete!
1003+ // Net: appends (1,3) from file-a (n=2 added then deleted = net zero), (10,20,30,40) from file-b
1004+ // No deletes (n=2 was added and deleted between snapshots 1 and 4)
1005+ fixture
1006+ . verify_incremental_scan (
1007+ 1 ,
1008+ 4 ,
1009+ vec ! [
1010+ ( 1 , "a1" ) ,
1011+ ( 3 , "a3" ) ,
1012+ ( 10 , "b1" ) ,
1013+ ( 20 , "b2" ) ,
1014+ ( 30 , "b3" ) ,
1015+ ( 40 , "b4" ) ,
1016+ ] ,
1017+ vec ! [ ] , // No deletes - n=2 was added and deleted between snapshots
1018+ )
1019+ . await ;
1020+
1021+ // Test 2: Scan from snapshot 4 to 6
1022+ // Snapshot 4: has file-a (1,3) and file-b (10,20,30,40)
1023+ // Snapshot 5: deletes positions 0,3 from file-b (n=10,40)
1024+ // Snapshot 6: adds file-c (100,200)
1025+ // Net: appends (100,200) from file-c; deletes pos 0,3 from file-b
1026+ fixture
1027+ . verify_incremental_scan (
1028+ 4 ,
1029+ 6 ,
1030+ vec ! [ ( 100 , "c1" ) , ( 200 , "c2" ) ] ,
1031+ vec ! [
1032+ ( 0 , file_b_path. as_str( ) ) , // n=10
1033+ ( 3 , file_b_path. as_str( ) ) , // n=40
1034+ ] ,
1035+ )
1036+ . await ;
1037+
1038+ // Test 3: Scan from snapshot 2 to 7
1039+ // This tests the full lifecycle: multiple adds, partial deletes, more adds, cross-file deletes
1040+ // Starting at snapshot 2: file-a (1,2,3) exists
1041+ // File-b is added in snapshot 3 (after snapshot 2)
1042+ // File-c is added in snapshot 6 (after snapshot 2)
1043+ // By snapshot 7: file-a has (3) at position 2, file-b has (30), file-c has (200)
1044+ //
1045+ // Net appends: file-b (30) and file-c (200) were added after snapshot 2
1046+ // Net deletes: positions 0,1 from file-a (n=1,2) existed at snapshot 2 and were deleted
1047+ // Note: (3) from file-a already existed at snapshot 2, so it's not a net append!
1048+ fixture
1049+ . verify_incremental_scan (
1050+ 2 ,
1051+ 7 ,
1052+ vec ! [ ( 30 , "b3" ) , ( 200 , "c2" ) ] ,
1053+ vec ! [
1054+ ( 0 , file_a_path. as_str( ) ) , // n=1
1055+ ( 1 , file_a_path. as_str( ) ) , // n=2
1056+ ] ,
1057+ )
1058+ . await ;
1059+
1060+ // Test 4: Scan from snapshot 5 to 6
1061+ // Simple test: just adding a new file
1062+ // Snapshot 5 state: file-a (1,3), file-b (20,30)
1063+ // Snapshot 6: adds file-c (100,200)
1064+ // Net: appends (100,200) from file-c, no deletes
1065+ fixture
1066+ . verify_incremental_scan (
1067+ 5 ,
1068+ 6 ,
1069+ vec ! [ ( 100 , "c1" ) , ( 200 , "c2" ) ] ,
1070+ vec ! [ ] ,
1071+ )
1072+ . await ;
1073+
1074+ // Test 5: Scan from snapshot 3 to 4
1075+ // Tests a single delete operation
1076+ // State at 3: file-a (1,2,3), file-b (10,20,30,40)
1077+ // State at 4: file-a (1,3), file-b (10,20,30,40)
1078+ // Net: no appends, 1 delete (position 1, n=2) from file-a
1079+ fixture
1080+ . verify_incremental_scan (
1081+ 3 ,
1082+ 4 ,
1083+ vec ! [ ] ,
1084+ vec ! [ ( 1 , file_a_path. as_str( ) ) ] , // n=2
1085+ )
1086+ . await ;
1087+ }
0 commit comments