|
22 | 22 | import org.apache.paimon.data.BinaryRow; |
23 | 23 | import org.apache.paimon.data.BinaryRowWriter; |
24 | 24 | import org.apache.paimon.data.GenericRow; |
| 25 | +import org.apache.paimon.deletionvectors.BucketedDvMaintainer; |
25 | 26 | import org.apache.paimon.fs.Path; |
26 | 27 | import org.apache.paimon.fs.local.LocalFileIO; |
| 28 | +import org.apache.paimon.index.IndexFileHandler; |
| 29 | +import org.apache.paimon.index.IndexFileMeta; |
27 | 30 | import org.apache.paimon.io.CompactIncrement; |
28 | 31 | import org.apache.paimon.io.DataFileMeta; |
29 | 32 | import org.apache.paimon.io.DataFilePathFactory; |
@@ -538,6 +541,188 @@ public void testStrictModeForOverwrite() throws Exception { |
538 | 541 | commit2.close(); |
539 | 542 | } |
540 | 543 |
|
| 544 | + @Test |
| 545 | + public void testStrictModeForDvOnlyOverwrite() throws Exception { |
| 546 | + // Regression test for the partition-overlap check on DV-only OVERWRITE |
| 547 | + // snapshots: such snapshots have no data-file delta, so the check must |
| 548 | + // also look at index-manifest delta. |
| 549 | + String path = tempDir.toString(); |
| 550 | + RowType rowType = |
| 551 | + RowType.of( |
| 552 | + new DataType[] {DataTypes.INT(), DataTypes.INT(), DataTypes.BIGINT()}, |
| 553 | + new String[] {"pt", "k", "v"}); |
| 554 | + |
| 555 | + Options options = new Options(); |
| 556 | + options.set(CoreOptions.PATH, path); |
| 557 | + options.set(CoreOptions.BUCKET, 1); |
| 558 | + options.set(CoreOptions.BUCKET_KEY, "k"); |
| 559 | + options.set(CoreOptions.NUM_SORTED_RUNS_COMPACTION_TRIGGER, 10); |
| 560 | + options.set(CoreOptions.DELETION_VECTORS_ENABLED, true); |
| 561 | + TableSchema tableSchema = |
| 562 | + SchemaUtils.forceCommit( |
| 563 | + new SchemaManager(LocalFileIO.create(), new Path(path)), |
| 564 | + new Schema( |
| 565 | + rowType.getFields(), |
| 566 | + Collections.singletonList("pt"), |
| 567 | + Collections.emptyList(), |
| 568 | + options.toMap(), |
| 569 | + "")); |
| 570 | + FileStoreTable table = |
| 571 | + FileStoreTableFactory.create( |
| 572 | + LocalFileIO.create(), |
| 573 | + new Path(path), |
| 574 | + tableSchema, |
| 575 | + CatalogEnvironment.empty()); |
| 576 | + BinaryRow pt1 = partitionRow(1); |
| 577 | + BinaryRow pt2 = partitionRow(2); |
| 578 | + |
| 579 | + // user1 writes pt=1 and pt=2 -> snapshot 1 (APPEND) |
| 580 | + String user1 = UUID.randomUUID().toString(); |
| 581 | + TableWriteImpl<?> write1 = table.newWrite(user1); |
| 582 | + TableCommitImpl commit1 = table.newCommit(user1); |
| 583 | + write1.write(GenericRow.of(1, 0, 0L)); |
| 584 | + write1.write(GenericRow.of(2, 0, 0L)); |
| 585 | + commit1.commit(1, write1.prepareCommit(false, 1)); |
| 586 | + |
| 587 | + // user2 with strict mode, last-safe-snapshot=2 (skip its own snapshot 2) |
| 588 | + String user2 = UUID.randomUUID().toString(); |
| 589 | + FileStoreTable tableWithStrict = |
| 590 | + table.copy(singletonMap(COMMIT_STRICT_MODE_LAST_SAFE_SNAPSHOT.key(), "2")); |
| 591 | + TableWriteImpl<?> write2 = tableWithStrict.newWrite(user2); |
| 592 | + TableCommitImpl commit2 = tableWithStrict.newCommit(user2); |
| 593 | + write2.write(GenericRow.of(1, 1, 1L)); |
| 594 | + commit2.commit(1, write2.prepareCommit(false, 1)); |
| 595 | + |
| 596 | + // user1 DV-only OVERWRITE on pt=2 -> snapshot 3 (OVERWRITE, no data-file delta) |
| 597 | + commitDvOnly(table, user1, pt2, 2); |
| 598 | + |
| 599 | + // user2 COMPACT on pt=1: DV-only on pt=2 does not overlap -> no error |
| 600 | + write2.write(GenericRow.of(1, 5, 5L)); |
| 601 | + write2.compact(pt1, 0, true); |
| 602 | + assertThatCode(() -> commit2.commit(2, write2.prepareCommit(true, 2))) |
| 603 | + .doesNotThrowAnyException(); |
| 604 | + |
| 605 | + // user1 DV-only OVERWRITE on pt=1 -> snapshot 5 (OVERWRITE, no data-file delta) |
| 606 | + commitDvOnly(table, user1, pt1, 3); |
| 607 | + |
| 608 | + // user2 COMPACT on pt=1: DV-only on pt=1 overlaps -> must throw |
| 609 | + write2.write(GenericRow.of(1, 7, 7L)); |
| 610 | + write2.compact(pt1, 0, true); |
| 611 | + assertThatThrownBy(() -> commit2.commit(3, write2.prepareCommit(true, 3))) |
| 612 | + .isInstanceOf(RuntimeException.class) |
| 613 | + .hasMessageContaining( |
| 614 | + "Giving up committing as commit.strict-mode.last-safe-snapshot is set."); |
| 615 | + |
| 616 | + write1.close(); |
| 617 | + commit1.close(); |
| 618 | + write2.close(); |
| 619 | + commit2.close(); |
| 620 | + } |
| 621 | + |
| 622 | + @Test |
| 623 | + public void testStrictModeShortCircuitsWhenIndexManifestUnchanged() throws Exception { |
| 624 | + String path = tempDir.toString(); |
| 625 | + RowType rowType = |
| 626 | + RowType.of( |
| 627 | + new DataType[] {DataTypes.INT(), DataTypes.INT(), DataTypes.BIGINT()}, |
| 628 | + new String[] {"pt", "k", "v"}); |
| 629 | + |
| 630 | + Options options = new Options(); |
| 631 | + options.set(CoreOptions.PATH, path); |
| 632 | + options.set(CoreOptions.BUCKET, 1); |
| 633 | + options.set(CoreOptions.BUCKET_KEY, "k"); |
| 634 | + options.set(CoreOptions.NUM_SORTED_RUNS_COMPACTION_TRIGGER, 10); |
| 635 | + options.set(CoreOptions.DELETION_VECTORS_ENABLED, true); |
| 636 | + TableSchema tableSchema = |
| 637 | + SchemaUtils.forceCommit( |
| 638 | + new SchemaManager(LocalFileIO.create(), new Path(path)), |
| 639 | + new Schema( |
| 640 | + rowType.getFields(), |
| 641 | + Collections.singletonList("pt"), |
| 642 | + Collections.emptyList(), |
| 643 | + options.toMap(), |
| 644 | + "")); |
| 645 | + FileStoreTable table = |
| 646 | + FileStoreTableFactory.create( |
| 647 | + LocalFileIO.create(), |
| 648 | + new Path(path), |
| 649 | + tableSchema, |
| 650 | + CatalogEnvironment.empty()); |
| 651 | + BinaryRow pt3 = partitionRow(3); |
| 652 | + |
| 653 | + // user1 writes pt=1 and pt=3 -> snapshot 1 (APPEND) |
| 654 | + String user1 = UUID.randomUUID().toString(); |
| 655 | + TableWriteImpl<?> write1 = table.newWrite(user1); |
| 656 | + TableCommitImpl commit1 = table.newCommit(user1); |
| 657 | + write1.write(GenericRow.of(1, 0, 0L)); |
| 658 | + write1.write(GenericRow.of(3, 0, 0L)); |
| 659 | + commit1.commit(1, write1.prepareCommit(false, 1)); |
| 660 | + |
| 661 | + // user1 commits a DV on pt=3 -> snapshot 2 (OVERWRITE, dv-only). |
| 662 | + // indexManifest now records pt=3. |
| 663 | + commitDvOnly(table, user1, pt3, 2); |
| 664 | + |
| 665 | + // user1 OVERWRITE on pt=1 -> snapshot 3 (OVERWRITE). |
| 666 | + // pt=1 has no DV, so writeIndexFiles produces no new index file and |
| 667 | + // snapshot 3 reuses snapshot 2's indexManifest file name. |
| 668 | + TableCommitImpl commit1Ow = table.newCommit(user1).withOverwrite(singletonMap("pt", "1")); |
| 669 | + write1.write(GenericRow.of(1, 9, 9L)); |
| 670 | + commit1Ow.commit(3, write1.prepareCommit(false, 3)); |
| 671 | + |
| 672 | + // user2 with last-safe=2 writes pt=3 -> APPEND. |
| 673 | + // Snapshot 3 is OVERWRITE on pt=1, its data delta does not touch pt=3, |
| 674 | + // and although its indexManifest still contains pt=3 (inherited from |
| 675 | + // snapshot 2), the short-circuit must skip the full-set intersection |
| 676 | + // and avoid throwing. |
| 677 | + String user2 = UUID.randomUUID().toString(); |
| 678 | + FileStoreTable tableWithStrict = |
| 679 | + table.copy(singletonMap(COMMIT_STRICT_MODE_LAST_SAFE_SNAPSHOT.key(), "2")); |
| 680 | + TableWriteImpl<?> write2 = tableWithStrict.newWrite(user2); |
| 681 | + TableCommitImpl commit2 = tableWithStrict.newCommit(user2); |
| 682 | + write2.write(GenericRow.of(3, 1, 1L)); |
| 683 | + assertThatCode(() -> commit2.commit(1, write2.prepareCommit(false, 1))) |
| 684 | + .doesNotThrowAnyException(); |
| 685 | + |
| 686 | + write1.close(); |
| 687 | + commit1.close(); |
| 688 | + commit1Ow.close(); |
| 689 | + write2.close(); |
| 690 | + commit2.close(); |
| 691 | + } |
| 692 | + |
| 693 | + private void commitDvOnly(FileStoreTable table, String user, BinaryRow partition, long commitId) |
| 694 | + throws Exception { |
| 695 | + // Pick a real data file in the partition; ConflictDetection requires the |
| 696 | + // DV to reference an existing data file name. |
| 697 | + String dataFileName = |
| 698 | + table.store().newScan().withPartitionFilter(Collections.singletonList(partition)) |
| 699 | + .plan().files().stream() |
| 700 | + .findFirst() |
| 701 | + .map(e -> e.file().fileName()) |
| 702 | + .orElseThrow( |
| 703 | + () -> |
| 704 | + new IllegalStateException( |
| 705 | + "No data file in partition for DV commit")); |
| 706 | + IndexFileHandler indexFileHandler = table.store().newIndexFileHandler(); |
| 707 | + BucketedDvMaintainer dvMaintainer = |
| 708 | + BucketedDvMaintainer.factory(indexFileHandler) |
| 709 | + .create(partition, 0, Collections.emptyList()); |
| 710 | + dvMaintainer.notifyNewDeletion(dataFileName, 0); |
| 711 | + IndexFileMeta dvIndex = dvMaintainer.writeDeletionVectorsIndex().get(); |
| 712 | + try (TableCommitImpl commit = table.newCommit(user)) { |
| 713 | + commit.commit( |
| 714 | + commitId, |
| 715 | + Collections.singletonList( |
| 716 | + new CommitMessageImpl( |
| 717 | + partition, |
| 718 | + 0, |
| 719 | + 1, |
| 720 | + DataIncrement.indexIncrement( |
| 721 | + Collections.singletonList(dvIndex)), |
| 722 | + CompactIncrement.emptyIncrement()))); |
| 723 | + } |
| 724 | + } |
| 725 | + |
541 | 726 | @Test |
542 | 727 | public void testStrictModeForOverwriteCheckAppend() throws Exception { |
543 | 728 | String path = tempDir.toString(); |
|
0 commit comments