|
20 | 20 |
|
21 | 21 | import static java.nio.charset.StandardCharsets.UTF_8;
|
22 | 22 | import static org.apache.iceberg.types.Types.NestedField.required;
|
| 23 | +import static org.assertj.core.api.Assertions.assertThat; |
| 24 | +import static org.assertj.core.api.Fail.fail; |
23 | 25 | import static org.mockito.ArgumentMatchers.any;
|
24 | 26 | import static org.mockito.ArgumentMatchers.isA;
|
25 | 27 | import static org.mockito.Mockito.doReturn;
|
|
30 | 32 | import com.azure.core.exception.HttpResponseException;
|
31 | 33 | import com.google.cloud.storage.StorageException;
|
32 | 34 | import com.google.common.collect.ImmutableMap;
|
| 35 | +import com.google.common.collect.Iterables; |
| 36 | +import com.google.common.collect.Streams; |
33 | 37 | import io.quarkus.test.junit.QuarkusMock;
|
34 | 38 | import io.quarkus.test.junit.QuarkusTestProfile;
|
35 | 39 | import io.quarkus.test.junit.TestProfile;
|
|
38 | 42 | import jakarta.inject.Inject;
|
39 | 43 | import jakarta.ws.rs.core.SecurityContext;
|
40 | 44 | import java.io.IOException;
|
| 45 | +import java.io.UncheckedIOException; |
41 | 46 | import java.lang.reflect.Method;
|
42 | 47 | import java.time.Clock;
|
43 | 48 | import java.util.Arrays;
|
|
49 | 54 | import java.util.UUID;
|
50 | 55 | import java.util.function.Function;
|
51 | 56 | import java.util.function.Supplier;
|
| 57 | +import java.util.stream.Collectors; |
52 | 58 | import java.util.stream.Stream;
|
53 | 59 | import org.apache.commons.lang3.NotImplementedException;
|
54 | 60 | import org.apache.iceberg.BaseTable;
|
55 | 61 | import org.apache.iceberg.CatalogProperties;
|
| 62 | +import org.apache.iceberg.ContentFile; |
| 63 | +import org.apache.iceberg.ContentScanTask; |
| 64 | +import org.apache.iceberg.DataOperations; |
56 | 65 | import org.apache.iceberg.DeleteFile;
|
57 | 66 | import org.apache.iceberg.FileMetadata;
|
| 67 | +import org.apache.iceberg.FileScanTask; |
| 68 | +import org.apache.iceberg.MetadataUpdate; |
58 | 69 | import org.apache.iceberg.PartitionSpec;
|
59 | 70 | import org.apache.iceberg.RowDelta;
|
60 | 71 | import org.apache.iceberg.Schema;
|
|
63 | 74 | import org.apache.iceberg.Table;
|
64 | 75 | import org.apache.iceberg.TableMetadata;
|
65 | 76 | import org.apache.iceberg.TableMetadataParser;
|
| 77 | +import org.apache.iceberg.TableOperations; |
| 78 | +import org.apache.iceberg.UpdateRequirement; |
| 79 | +import org.apache.iceberg.UpdateRequirements; |
66 | 80 | import org.apache.iceberg.UpdateSchema;
|
67 | 81 | import org.apache.iceberg.catalog.CatalogTests;
|
68 | 82 | import org.apache.iceberg.catalog.Namespace;
|
|
73 | 87 | import org.apache.iceberg.exceptions.ForbiddenException;
|
74 | 88 | import org.apache.iceberg.exceptions.NoSuchNamespaceException;
|
75 | 89 | import org.apache.iceberg.inmemory.InMemoryFileIO;
|
| 90 | +import org.apache.iceberg.io.CloseableIterable; |
76 | 91 | import org.apache.iceberg.io.FileIO;
|
| 92 | +import org.apache.iceberg.rest.requests.UpdateTableRequest; |
77 | 93 | import org.apache.iceberg.types.Types;
|
| 94 | +import org.apache.iceberg.util.CharSequenceSet; |
78 | 95 | import org.apache.polaris.core.PolarisCallContext;
|
79 | 96 | import org.apache.polaris.core.PolarisDiagnostics;
|
80 | 97 | import org.apache.polaris.core.admin.model.AwsStorageConfigInfo;
|
|
115 | 132 | import org.apache.polaris.service.admin.PolarisAdminService;
|
116 | 133 | import org.apache.polaris.service.catalog.PolarisPassthroughResolutionView;
|
117 | 134 | import org.apache.polaris.service.catalog.iceberg.IcebergCatalog;
|
| 135 | +import org.apache.polaris.service.catalog.iceberg.IcebergCatalogHandler; |
118 | 136 | import org.apache.polaris.service.catalog.io.DefaultFileIOFactory;
|
119 | 137 | import org.apache.polaris.service.catalog.io.ExceptionMappingFileIO;
|
120 | 138 | import org.apache.polaris.service.catalog.io.FileIOFactory;
|
|
129 | 147 | import org.apache.polaris.service.types.NotificationRequest;
|
130 | 148 | import org.apache.polaris.service.types.NotificationType;
|
131 | 149 | import org.apache.polaris.service.types.TableUpdateNotification;
|
| 150 | +import org.assertj.core.api.AbstractCollectionAssert; |
132 | 151 | import org.assertj.core.api.Assertions;
|
| 152 | +import org.assertj.core.api.ListAssert; |
133 | 153 | import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
134 | 154 | import org.junit.jupiter.api.AfterEach;
|
135 | 155 | import org.junit.jupiter.api.Assumptions;
|
@@ -520,36 +540,100 @@ public void testConcurrentWritesWithRollbackNonEmptyTable() {
|
520 | 540 | catalog.createNamespace(NS);
|
521 | 541 | }
|
522 | 542 |
|
523 |
| - Table table = catalog.buildTable(TABLE, SCHEMA).withPartitionSpec(SPEC).create(); |
| 543 | + Table table = |
| 544 | + catalog |
| 545 | + .buildTable(TABLE, SCHEMA) |
| 546 | + .withPartitionSpec(SPEC) |
| 547 | + .withProperties(Map.of("rollback.compaction.on-conflicts.enabled", "true")) |
| 548 | + .create(); |
524 | 549 | this.assertNoFiles(table);
|
| 550 | + |
525 | 551 | // commit FILE_A
|
526 | 552 | catalog.loadTable(TABLE).newFastAppend().appendFile(FILE_A).commit();
|
527 | 553 | this.assertFiles(catalog.loadTable(TABLE), FILE_A);
|
528 | 554 | table.refresh();
|
| 555 | + |
529 | 556 | long lastSnapshotId = table.currentSnapshot().snapshotId();
|
530 |
| - // Apply the deletes based on FILE_A |
531 |
| - // this should conflict when we try to commit |
532 |
| - // without the change |
533 |
| - RowDelta rowDelta = |
| 557 | + |
| 558 | + // Apply the deletes based on FILE_A |
| 559 | + // this should conflict when we try to commit without the change. |
| 560 | + RowDelta originalRowDelta = |
534 | 561 | table
|
535 | 562 | .newRowDelta()
|
536 | 563 | .addDeletes(FILE_A_DELETES)
|
537 | 564 | .validateFromSnapshot(lastSnapshotId)
|
538 | 565 | .validateDataFilesExist(List.of(FILE_A.location()));
|
539 |
| - Snapshot uncomittedSnapshot = rowDelta.apply(); |
| 566 | + // Make client ready with updates, don't reach out to IRC server yet |
| 567 | + Snapshot s = originalRowDelta.apply(); |
| 568 | + TableOperations ops = ((BaseTable) catalog.loadTable(TABLE)).operations(); |
| 569 | + TableMetadata base = ops.current(); |
| 570 | + TableMetadata.Builder update = TableMetadata.buildFrom(base); |
| 571 | + update.setBranchSnapshot(s, "main"); |
| 572 | + TableMetadata updatedMetadata = update.build(); |
| 573 | + List<MetadataUpdate> updates = updatedMetadata.changes(); |
| 574 | + List<UpdateRequirement> requirements = UpdateRequirements.forUpdateTable(base, updates); |
| 575 | + UpdateTableRequest request = UpdateTableRequest.create(TABLE, requirements, updates); |
540 | 576 |
|
541 | 577 | // replace FILE_A with FILE_B
|
| 578 | + // commit the transaction. |
542 | 579 | catalog.loadTable(TABLE).newRewrite().addFile(FILE_B).deleteFile(FILE_A).commit();
|
543 | 580 |
|
544 |
| - // no try commit FILE_A delete, It's impossible to mimic the contention |
545 |
| - // situation presently as commit triggers client side validation again |
546 |
| - // even when the apply() method has been called before |
547 |
| - // TODO: to better add such test cases, may be mocking the taskOps to not |
548 |
| - // do the refresh, hence make on base snapshot. |
549 |
| - // also there is an issue on overriding this as all things are package scope |
550 |
| - // For ex MergingSnapshotPro |
551 |
| - // rowDelta.commit(); |
552 |
| - this.assertFiles(catalog.loadTable(TABLE), FILE_B); |
| 581 | + try { |
| 582 | + // Now call IRC server to commit delete operation. |
| 583 | + IcebergCatalogHandler.commit(((BaseTable) catalog.loadTable(TABLE)).operations(), request); |
| 584 | + } catch (Exception e) { |
| 585 | + fail("Rollback Compaction on conflict feature failed : " + e.getMessage()); |
| 586 | + } |
| 587 | + |
| 588 | + table.refresh(); |
| 589 | + |
| 590 | + // Assert only 2 snapshots and no snapshot of REPLACE left. |
| 591 | + Snapshot currentSnapshot = table.snapshot(table.refs().get("main").snapshotId()); |
| 592 | + int totalSnapshots = 1; |
| 593 | + while (currentSnapshot.parentId() != null) { |
| 594 | + // no snapshot in the hierarchy for REPLACE operations |
| 595 | + assertThat(currentSnapshot.operation()).isNotEqualTo(DataOperations.REPLACE); |
| 596 | + currentSnapshot = table.snapshot(currentSnapshot.parentId()); |
| 597 | + totalSnapshots += 1; |
| 598 | + } |
| 599 | + assertThat(totalSnapshots).isEqualTo(2); |
| 600 | + |
| 601 | + // Inspect the files 1 DELETE file i.e. FILE_A_DELETES and 1 DATA FILE FILE_A |
| 602 | + try { |
| 603 | + try (CloseableIterable<FileScanTask> tasks = table.newScan().planFiles()) { |
| 604 | + List<CharSequence> dataFilePaths = |
| 605 | + Streams.stream(tasks) |
| 606 | + .map(ContentScanTask::file) |
| 607 | + .map(ContentFile::location) |
| 608 | + .collect(Collectors.toList()); |
| 609 | + List<CharSequence> deleteFilePaths = |
| 610 | + Streams.stream(tasks) |
| 611 | + .flatMap(t -> t.deletes().stream().map(ContentFile::location)) |
| 612 | + .collect(Collectors.toList()); |
| 613 | + ((ListAssert) |
| 614 | + Assertions.assertThat(dataFilePaths) |
| 615 | + .as("Should contain expected number of data files", new Object[0])) |
| 616 | + .hasSize(1); |
| 617 | + ((ListAssert) |
| 618 | + Assertions.assertThat(deleteFilePaths) |
| 619 | + .as("Should contain expected number of delete files", new Object[0])) |
| 620 | + .hasSize(1); |
| 621 | + ((AbstractCollectionAssert) |
| 622 | + Assertions.assertThat(CharSequenceSet.of(dataFilePaths)) |
| 623 | + .as("Should contain correct file paths", new Object[0])) |
| 624 | + .isEqualTo( |
| 625 | + CharSequenceSet.of( |
| 626 | + Iterables.transform(Arrays.asList(FILE_A), ContentFile::location))); |
| 627 | + ((AbstractCollectionAssert) |
| 628 | + Assertions.assertThat(CharSequenceSet.of(deleteFilePaths)) |
| 629 | + .as("Should contain correct file paths", new Object[0])) |
| 630 | + .isEqualTo( |
| 631 | + CharSequenceSet.of( |
| 632 | + Iterables.transform(Arrays.asList(FILE_A_DELETES), ContentFile::location))); |
| 633 | + } |
| 634 | + } catch (IOException e) { |
| 635 | + throw new UncheckedIOException(e); |
| 636 | + } |
553 | 637 | }
|
554 | 638 |
|
555 | 639 | @Test
|
@@ -584,7 +668,7 @@ public void testValidateNotificationWhenTableAndNamespacesDontExist() {
|
584 | 668 | // We should be able to send the notification without creating the metadata file since it's
|
585 | 669 | // only validating the ability to send the CREATE/UPDATE notification possibly before actually
|
586 | 670 | // creating the table at all on the remote catalog.
|
587 |
| - Assertions.assertThat(catalog.sendNotification(table, request)) |
| 671 | + assertThat(catalog.sendNotification(table, request)) |
588 | 672 | .as("Notification should be sent successfully")
|
589 | 673 | .isTrue();
|
590 | 674 | Assertions.assertThat(catalog.namespaceExists(namespace))
|
|
0 commit comments