@@ -2672,4 +2672,201 @@ protected FileStoreTable createFileStoreTable(Consumer<Options> configure, RowTy
26722672 "" ));
26732673 return new PrimaryKeyFileStoreTable (FileIOFinder .find (tablePath ), tablePath , tableSchema );
26742674 }
2675+
2676+ @ Test
2677+ public void testSnapshotSequenceOrdering () throws Exception {
2678+ FileStoreTable table =
2679+ createFileStoreTable (
2680+ conf -> conf .set (CoreOptions .SEQUENCE_SNAPSHOT_ORDERING , true ));
2681+ StreamTableWrite write = table .newWrite (commitUser );
2682+ StreamTableCommit commit = table .newCommit (commitUser );
2683+
2684+ // Snapshot 1: write pk=(1,10) many times so that the per-record sequence number is high.
2685+ for (int i = 0 ; i < 100 ; i ++) {
2686+ write .write (rowData (1 , 10 , 999L ));
2687+ }
2688+ commit .commit (0 , write .prepareCommit (false , 0 ));
2689+
2690+ // Snapshot 2: write pk=(1,10) once with a lower value. Because the snapshot id (2)
2691+ // is larger than snapshot 1, this record should win even though its per-record sequence
2692+ // number is much lower.
2693+ write .write (rowData (1 , 10 , 1L ));
2694+ commit .commit (1 , write .prepareCommit (false , 1 ));
2695+
2696+ List <Split > splits = toSplits (table .newSnapshotReader ().read ().dataSplits ());
2697+ TableRead read = table .newReadBuilder ().newRead ();
2698+ Function <InternalRow , String > toString =
2699+ r -> r .getInt (0 ) + "|" + r .getInt (1 ) + "|" + r .getLong (2 );
2700+ List <String > result = getResult (read , splits , toString );
2701+ assertThat (result ).containsExactly ("1|10|1" );
2702+
2703+ write .close ();
2704+ commit .close ();
2705+ }
2706+
2707+ @ Test
2708+ public void testSnapshotSequenceOrderingFallsBackToSequenceWithinSnapshot () throws Exception {
2709+ FileStoreTable table =
2710+ createFileStoreTable (
2711+ conf -> conf .set (CoreOptions .SEQUENCE_SNAPSHOT_ORDERING , true ));
2712+ StreamTableWrite write = table .newWrite (commitUser );
2713+ StreamTableCommit commit = table .newCommit (commitUser );
2714+
2715+ // Within a single snapshot, sequence number is the tiebreaker. The later write (999)
2716+ // gets a higher sequence number and should win.
2717+ write .write (rowData (1 , 10 , 1L ));
2718+ write .write (rowData (1 , 10 , 999L ));
2719+ commit .commit (0 , write .prepareCommit (false , 0 ));
2720+
2721+ List <Split > splits = toSplits (table .newSnapshotReader ().read ().dataSplits ());
2722+ TableRead read = table .newReadBuilder ().newRead ();
2723+ Function <InternalRow , String > toString =
2724+ r -> r .getInt (0 ) + "|" + r .getInt (1 ) + "|" + r .getLong (2 );
2725+ List <String > result = getResult (read , splits , toString );
2726+ assertThat (result ).containsExactly ("1|10|999" );
2727+
2728+ write .close ();
2729+ commit .close ();
2730+ }
2731+
2732+ @ Test
2733+ public void testSnapshotSequenceOrderingCompactionPreservesInputSnapshotId () throws Exception {
2734+ FileStoreTable table =
2735+ createFileStoreTable (
2736+ conf -> conf .set (CoreOptions .SEQUENCE_SNAPSHOT_ORDERING , true ));
2737+ StreamTableWrite write = table .newWrite (commitUser );
2738+ StreamTableCommit commit = table .newCommit (commitUser );
2739+
2740+ // Snapshot 1: write pk=(1,10) with val=100
2741+ write .write (rowData (1 , 10 , 100L ));
2742+ commit .commit (0 , write .prepareCommit (false , 0 ));
2743+
2744+ // Snapshot 2: write pk=(1,10) with val=200 (this should win after compaction)
2745+ write .write (rowData (1 , 10 , 200L ));
2746+ commit .commit (1 , write .prepareCommit (false , 1 ));
2747+
2748+ // Snapshot 3: write a DIFFERENT key pk=(1,20) — this creates a concurrent write
2749+ // that arrives between the compaction's base snapshot and its commit.
2750+ write .write (rowData (1 , 20 , 300L ));
2751+ commit .commit (2 , write .prepareCommit (false , 2 ));
2752+
2753+ // Snapshot 4: compact (processes files from snapshots 1+2+3)
2754+ write .compact (binaryRow (1 ), 0 , true );
2755+ commit .commit (3 , write .prepareCommit (false , 3 ));
2756+
2757+ // Snapshot 5: write pk=(1,10) with val=999 — this is newer than ALL previous writes
2758+ // and MUST win over the compacted result. If compaction incorrectly stamped with
2759+ // snapshot 4, and this write gets snapshot 5, it still wins (5 > 4). But if
2760+ // compaction stamped with max(inputs) = 3, this also wins (5 > 3). So we need a
2761+ // scenario where compaction shadow newer writes.
2762+ //
2763+ // The real test: verify the compacted file's minSequenceNumber is max(inputs), not
2764+ // the compaction commit's snapshotId. We check this via file metadata.
2765+ write .close ();
2766+ commit .close ();
2767+
2768+ List <DataSplit > splits = table .newSnapshotReader ().read ().dataSplits ();
2769+ for (DataSplit split : splits ) {
2770+ for (DataFileMeta file : split .dataFiles ()) {
2771+ // After compaction, the compacted file should have minSequenceNumber equal
2772+ // to max(input snapshot ids), NOT the compaction commit's snapshot id (4).
2773+ // Input snapshots are 1, 2, 3 → max = 3. Compaction commit is snapshot 4.
2774+ assertThat (file .minSequenceNumber ())
2775+ .as (
2776+ "Compacted file %s should have minSequenceNumber <= max(input snapshots), "
2777+ + "not the compaction commit's snapshot id" ,
2778+ file .fileName ())
2779+ .isLessThanOrEqualTo (3 );
2780+ }
2781+ }
2782+
2783+ // Also verify the read result is correct
2784+ TableRead read = table .newReadBuilder ().newRead ();
2785+ Function <InternalRow , String > toString =
2786+ r -> r .getInt (0 ) + "|" + r .getInt (1 ) + "|" + r .getLong (2 );
2787+ List <String > result = getResult (read , toSplits (splits ), toString );
2788+ assertThat (result ).containsExactlyInAnyOrder ("1|10|200" , "1|20|300" );
2789+ }
2790+
2791+ @ Test
2792+ public void testSnapshotSequenceOrderingWithChangelogInput () throws Exception {
2793+ FileStoreTable table =
2794+ createFileStoreTable (
2795+ conf -> {
2796+ conf .set (CoreOptions .SEQUENCE_SNAPSHOT_ORDERING , true );
2797+ conf .set (CHANGELOG_PRODUCER , ChangelogProducer .INPUT );
2798+ });
2799+ StreamTableWrite write = table .newWrite (commitUser );
2800+ StreamTableCommit commit = table .newCommit (commitUser );
2801+
2802+ write .write (rowData (1 , 10 , 100L ));
2803+ commit .commit (0 , write .prepareCommit (false , 0 ));
2804+
2805+ write .write (rowData (1 , 10 , 1L ));
2806+ commit .commit (1 , write .prepareCommit (false , 1 ));
2807+
2808+ List <Split > splits = toSplits (table .newSnapshotReader ().read ().dataSplits ());
2809+ TableRead read = table .newReadBuilder ().newRead ();
2810+ Function <InternalRow , String > toString =
2811+ r -> r .getInt (0 ) + "|" + r .getInt (1 ) + "|" + r .getLong (2 );
2812+ List <String > result = getResult (read , splits , toString );
2813+ assertThat (result ).containsExactly ("1|10|1" );
2814+
2815+ write .close ();
2816+ commit .close ();
2817+ }
2818+
2819+ @ Test
2820+ public void testSnapshotSequenceOrderingWithChangelogLookup () throws Exception {
2821+ FileStoreTable table =
2822+ createFileStoreTable (
2823+ conf -> {
2824+ conf .set (CoreOptions .SEQUENCE_SNAPSHOT_ORDERING , true );
2825+ conf .set (CHANGELOG_PRODUCER , LOOKUP );
2826+ });
2827+ StreamTableWrite write =
2828+ table .newWrite (commitUser ).withIOManager (new IOManagerImpl (tempDir .toString ()));
2829+ StreamTableCommit commit = table .newCommit (commitUser );
2830+
2831+ write .write (rowData (1 , 10 , 100L ));
2832+ commit .commit (0 , write .prepareCommit (false , 0 ));
2833+
2834+ write .write (rowData (1 , 10 , 1L ));
2835+ commit .commit (1 , write .prepareCommit (false , 1 ));
2836+
2837+ List <Split > splits = toSplits (table .newSnapshotReader ().read ().dataSplits ());
2838+ TableRead read = table .newReadBuilder ().newRead ();
2839+ Function <InternalRow , String > toString =
2840+ r -> r .getInt (0 ) + "|" + r .getInt (1 ) + "|" + r .getLong (2 );
2841+ List <String > result = getResult (read , splits , toString );
2842+ assertThat (result ).containsExactly ("1|10|1" );
2843+
2844+ write .close ();
2845+ commit .close ();
2846+ }
2847+
2848+ @ Test
2849+ public void testSnapshotSequenceOrderingDeleteFromLaterSnapshot () throws Exception {
2850+ FileStoreTable table =
2851+ createFileStoreTable (
2852+ conf -> conf .set (CoreOptions .SEQUENCE_SNAPSHOT_ORDERING , true ));
2853+ StreamTableWrite write = table .newWrite (commitUser );
2854+ StreamTableCommit commit = table .newCommit (commitUser );
2855+
2856+ write .write (rowData (1 , 10 , 100L ));
2857+ commit .commit (0 , write .prepareCommit (false , 0 ));
2858+
2859+ write .write (rowDataWithKind (RowKind .DELETE , 1 , 10 , 100L ));
2860+ commit .commit (1 , write .prepareCommit (false , 1 ));
2861+
2862+ List <Split > splits = toSplits (table .newSnapshotReader ().read ().dataSplits ());
2863+ TableRead read = table .newReadBuilder ().newRead ();
2864+ Function <InternalRow , String > toString =
2865+ r -> r .getInt (0 ) + "|" + r .getInt (1 ) + "|" + r .getLong (2 );
2866+ List <String > result = getResult (read , splits , toString );
2867+ assertThat (result ).isEmpty ();
2868+
2869+ write .close ();
2870+ commit .close ();
2871+ }
26752872}
0 commit comments