This is an automated email from the ASF dual-hosted git repository. emaynard pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/polaris-tools.git
The following commit(s) were added to refs/heads/main by this push: new 46fe5e0 Added append only, upsert only modes for polaris-synchronizer, and put modification aware behind opt-in flag. (#11) 46fe5e0 is described below commit 46fe5e057e617eecee4da41bc36791736e3c55de Author: Mansehaj Singh <mseh...@gmail.com> AuthorDate: Fri Apr 25 15:06:58 2025 -0700 Added append only, upsert only modes for polaris-synchronizer, and put modification aware behind opt-in flag. (#11) * Added append only and remove strategies and put modification aware behind configurable flag * Rename to diffOnly * Changed test --- .../tools/sync/polaris/PolarisSynchronizer.java | 37 ++- ...zationPlanner.java => BaseStrategyPlanner.java} | 96 +++++-- .../polaris/planning/SynchronizationPlanner.java | 4 +- .../SourceParitySynchronizationPlannerTest.java | 303 --------------------- .../strategy/AbstractBaseStrategyPlannerTest.java | 238 ++++++++++++++++ .../CreateAndOverwriteBaseStrategyPlannerTest.java | 30 ++ .../CreateOnlyBaseStrategyPlannerTest.java | 30 ++ .../strategy/ReplicateBaseStrategyPlannerTest.java | 30 ++ .../tools/sync/polaris/SyncPolarisCommand.java | 29 +- 9 files changed, 454 insertions(+), 343 deletions(-) diff --git a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizer.java b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizer.java index a521ecf..a724437 100644 --- a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizer.java +++ b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizer.java @@ -61,13 +61,16 @@ public class PolarisSynchronizer { private final boolean haltOnFailure; + private final boolean diffOnly; + public PolarisSynchronizer( Logger clientLogger, boolean haltOnFailure, SynchronizationPlanner synchronizationPlanner, PolarisService source, PolarisService target, - ETagManager etagManager) { + ETagManager etagManager, + boolean diffOnly) { this.clientLogger = clientLogger == null ? LoggerFactory.getLogger(PolarisSynchronizer.class) : clientLogger; this.haltOnFailure = haltOnFailure; @@ -75,6 +78,7 @@ public class PolarisSynchronizer { this.source = source; this.target = target; this.etagManager = etagManager; + this.diffOnly = diffOnly; } /** @@ -1035,18 +1039,25 @@ public class PolarisSynchronizer { try { Map<String, String> sourceNamespaceMetadata = sourceIcebergCatalogService.loadNamespaceMetadata(namespace); - Map<String, String> targetNamespaceMetadata = - targetIcebergCatalogService.loadNamespaceMetadata(namespace); - if (sourceNamespaceMetadata.equals(targetNamespaceMetadata)) { - clientLogger.info( - "Namespace metadata for namespace {} in namespace {} for catalog {} was not modified, skipping. - {}/{}", - namespace, - parentNamespace, - catalogName, - ++syncsCompleted, - totalSyncsToComplete); - continue; + if (this.diffOnly) { + // if only configured to migrate the diff between the source and the target Polaris, + // then we can load the target namespace metadata and perform a comparison to discontinue early + // if we notice the metadata did not change + + Map<String, String> targetNamespaceMetadata = + targetIcebergCatalogService.loadNamespaceMetadata(namespace); + + if (sourceNamespaceMetadata.equals(targetNamespaceMetadata)) { + clientLogger.info( + "Namespace metadata for namespace {} in namespace {} for catalog {} was not modified, skipping. - {}/{}", + namespace, + parentNamespace, + catalogName, + ++syncsCompleted, + totalSyncsToComplete); + continue; + } } targetIcebergCatalogService.setNamespaceProperties(namespace, sourceNamespaceMetadata); @@ -1206,7 +1217,7 @@ public class PolarisSynchronizer { try { Table table; - if (sourceIcebergCatalogService instanceof PolarisIcebergCatalogService polarisCatalogService) { + if (this.diffOnly && sourceIcebergCatalogService instanceof PolarisIcebergCatalogService polarisCatalogService) { String etag = etagManager.getETag(catalogName, tableId); table = polarisCatalogService.loadTable(tableId, etag); } else { diff --git a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SourceParitySynchronizationPlanner.java b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/BaseStrategyPlanner.java similarity index 67% rename from polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SourceParitySynchronizationPlanner.java rename to polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/BaseStrategyPlanner.java index a60fea7..39a2bac 100644 --- a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SourceParitySynchronizationPlanner.java +++ b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/BaseStrategyPlanner.java @@ -33,21 +33,50 @@ import org.apache.polaris.core.admin.model.PrincipalRole; import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationPlan; /** - * Sync planner that attempts to create total parity between the source and target Polaris - * instances. This involves creating new entities, overwriting entities that exist on both source - * and target, and removing entities that exist only on the target. + * Planner that implements the base level strategy that can be applied to synchronize the source and target. + * Can be configured at different levels of modification. */ -public class SourceParitySynchronizationPlanner implements SynchronizationPlanner { +public class BaseStrategyPlanner implements SynchronizationPlanner { /** - * Sort entities from the source into create, overwrite, and remove categories + * The strategy to employ when using {@link BaseStrategyPlanner}. + */ + public enum Strategy { + + /** + * Only create entities that exist on source but don't already exist on the target + */ + CREATE_ONLY, + + /** + * Create entities that do not exist on the target, and overwrite existing ones with same name/identifier + */ + CREATE_AND_OVERWRITE, + + /** + * Create entities that exist on the source and not target, update entities that exist on both, remove entities + * from the target that do not exist on the source. + */ + REPLICATE + + } + + private final Strategy strategy; + + public BaseStrategyPlanner(Strategy strategy) { + this.strategy = strategy; + } + + /** + * Sort entities from the source into create, overwrite, remove, and skip categories * on the basis of which identifiers exist on the source and target Polaris. * Identifiers that are both on the source and target instance will be marked - * for overwrite. Entities that are only on the source instance will be marked for - * creation. Entities that are only on the target instance will be marked for deletion. + * for overwrite if overwriting is enabled. Entities that are only on the source instance + * will be marked for creation. Entities that are only on the target instance will be marked for deletion + * only if the {@link Strategy#REPLICATE} strategy is used. * @param entitiesOnSource the entities from the source * @param entitiesOnTarget the entities from the target - * @param supportOverwrites true if "overwriting" the entity is necessary. Most grant record entities do not need overwriting. + * @param requiresOverwrites true if "overwriting" the entity is necessary. Most grant record entities do not need overwriting. * @param entityIdentifierSupplier consumes an entity and returns an identifying representation of that entity * @return a {@link SynchronizationPlan} with the entities sorted based on the souce parity strategy * @param <T> the type of the entity @@ -55,7 +84,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne private <T> SynchronizationPlan<T> sortOnIdentifier( Collection<T> entitiesOnSource, Collection<T> entitiesOnTarget, - boolean supportOverwrites, + boolean requiresOverwrites, Function<T, Object> entityIdentifierSupplier ) { Set<Object> sourceEntityIdentifiers = entitiesOnSource.stream().map(entityIdentifierSupplier).collect(Collectors.toSet()); @@ -65,11 +94,28 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne for (T entityOnSource : entitiesOnSource) { Object sourceEntityId = entityIdentifierSupplier.apply(entityOnSource); + if (targetEntityIdentifiers.contains(sourceEntityId)) { - if (supportOverwrites) { + // If an entity with this identifier exists on both the source and the target + + if (strategy == Strategy.CREATE_ONLY) { + // if the same entity identifier is on the source and target, + // but we only permit creates, skip it + plan.skipEntity(entityOnSource); + } else { // if the same entity identifier is on the source and the target, // overwrite the one on the target with the one on the source - plan.overwriteEntity(entityOnSource); + + if (requiresOverwrites) { + // If the entity requires a drop-and-recreate to perform an overwrite. + // some grant records can be "created" indefinitely even if they already exists, for example, + // catalog roles can be assigned the same principal role many times + plan.overwriteEntity(entityOnSource); + } else { + // if the entity is not a type that requires "overwriting" in the sense of + // dropping and recreating, then just create it again + plan.createEntity(entityOnSource); + } } } else { // if the entity identifier only exists on the source, that means @@ -89,7 +135,15 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne // or a catalog role was revoked from a principal role, in which case the target // should reflect this change when the tool is run multiple times, because we don't // want to take chances with over-extending privileges - plan.removeEntity(entityOnTarget); + + if (strategy == Strategy.REPLICATE) { + plan.removeEntity(entityOnTarget); + } else { + // skip children here because if we want to remove the entity + // and then that means it does not exist on the source, so there are no child + // entities to sync + plan.skipEntityAndSkipChildren(entityOnTarget); + } } } @@ -99,7 +153,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne @Override public SynchronizationPlan<Principal> planPrincipalSync( List<Principal> principalsOnSource, List<Principal> principalsOnTarget) { - return sortOnIdentifier(principalsOnSource, principalsOnTarget, /* supportsOverwrites */ true, Principal::getName); + return sortOnIdentifier(principalsOnSource, principalsOnTarget, /* requiresOverwrites */ true, Principal::getName); } @Override @@ -111,7 +165,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne return sortOnIdentifier( assignedPrincipalRolesOnSource, assignedPrincipalRolesOnTarget, - /* supportsOverwrites */ false, // do not need to overwrite an assignment of a principal role to a principal + /* requiresOverwrites */ false, // do not need to overwrite an assignment of a principal role to a principal PrincipalRole::getName ); } @@ -123,7 +177,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne return sortOnIdentifier( principalRolesOnSource, principalRolesOnTarget, - /* supportsOverwrites */ true, + /* requiresOverwrites */ true, PrincipalRole::getName ); } @@ -131,7 +185,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne @Override public SynchronizationPlan<Catalog> planCatalogSync( List<Catalog> catalogsOnSource, List<Catalog> catalogsOnTarget) { - return sortOnIdentifier(catalogsOnSource, catalogsOnTarget, /* supportsOverwrites */ true, Catalog::getName); + return sortOnIdentifier(catalogsOnSource, catalogsOnTarget, /* requiresOverwrites */ true, Catalog::getName); } @Override @@ -140,7 +194,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne List<CatalogRole> catalogRolesOnSource, List<CatalogRole> catalogRolesOnTarget) { return sortOnIdentifier( - catalogRolesOnSource, catalogRolesOnTarget, /* supportsOverwrites */ true, CatalogRole::getName); + catalogRolesOnSource, catalogRolesOnTarget, /* requiresOverwrites */ true, CatalogRole::getName); } @Override @@ -152,7 +206,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne return sortOnIdentifier( grantsOnSource, grantsOnTarget, - /* supportsOverwrites */ false, + /* requiresOverwrites */ false, grant -> grant // grants can just be compared by the entire generated object ); } @@ -166,7 +220,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne return sortOnIdentifier( assignedPrincipalRolesOnSource, assignedPrincipalRolesOnTarget, - /* supportsOverwrites */ false, + /* requiresOverwrites */ false, PrincipalRole::getName ); } @@ -177,7 +231,7 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne Namespace namespace, List<Namespace> namespacesOnSource, List<Namespace> namespacesOnTarget) { - return sortOnIdentifier(namespacesOnSource, namespacesOnTarget, /* supportsOverwrites */ true, ns -> ns); + return sortOnIdentifier(namespacesOnSource, namespacesOnTarget, /* requiresOverwrites */ true, ns -> ns); } @Override @@ -187,6 +241,6 @@ public class SourceParitySynchronizationPlanner implements SynchronizationPlanne Set<TableIdentifier> tablesOnSource, Set<TableIdentifier> tablesOnTarget) { return sortOnIdentifier( - tablesOnSource, tablesOnTarget, /* supportsOverwrites */ true, tableId -> tableId); + tablesOnSource, tablesOnTarget, /* requiresOverwrites */ true, tableId -> tableId); } } diff --git a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SynchronizationPlanner.java b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SynchronizationPlanner.java index 842cdcf..8f76945 100644 --- a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SynchronizationPlanner.java +++ b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/SynchronizationPlanner.java @@ -53,7 +53,7 @@ public interface SynchronizationPlanner { private final List<PlannerWrapper> plannerWrappers = new ArrayList<>(); - private SynchronizationPlannerBuilder(SourceParitySynchronizationPlanner innermost) { + private SynchronizationPlannerBuilder(BaseStrategyPlanner innermost) { this.innermost = innermost; } @@ -90,7 +90,7 @@ public interface SynchronizationPlanner { } } - static SynchronizationPlannerBuilder builder(SourceParitySynchronizationPlanner innermost) { + static SynchronizationPlannerBuilder builder(BaseStrategyPlanner innermost) { return new SynchronizationPlannerBuilder(innermost); } diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/SourceParitySynchronizationPlannerTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/SourceParitySynchronizationPlannerTest.java deleted file mode 100644 index 18a961e..0000000 --- a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/SourceParitySynchronizationPlannerTest.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.tools.sync.polaris; - -import java.util.List; -import java.util.Set; -import org.apache.iceberg.catalog.Namespace; -import org.apache.iceberg.catalog.TableIdentifier; -import org.apache.polaris.core.admin.model.Catalog; -import org.apache.polaris.core.admin.model.CatalogRole; -import org.apache.polaris.core.admin.model.GrantResource; -import org.apache.polaris.core.admin.model.Principal; -import org.apache.polaris.core.admin.model.PrincipalRole; -import org.apache.polaris.tools.sync.polaris.planning.SourceParitySynchronizationPlanner; -import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationPlan; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class SourceParitySynchronizationPlannerTest { - - private static final Catalog CATALOG_1 = new Catalog().name("catalog-1"); - - private static final Catalog CATALOG_2 = new Catalog().name("catalog-2"); - - private static final Catalog CATALOG_3 = new Catalog().name("catalog-3"); - - @Test - public void testCreatesNewCatalogOverwritesOldCatalogRemovesDroppedCatalog() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<Catalog> plan = - planner.planCatalogSync(List.of(CATALOG_1, CATALOG_2), List.of(CATALOG_2, CATALOG_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(CATALOG_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(CATALOG_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(CATALOG_1)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(CATALOG_2)); - Assertions.assertTrue(plan.entitiesToOverwrite().contains(CATALOG_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(CATALOG_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(CATALOG_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(CATALOG_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(CATALOG_3)); - } - - private static final Principal PRINCIPAL_1 = - new Principal().name("principal-1"); - - private static final Principal PRINCIPAL_2 = - new Principal().name("principal-2"); - - private static final Principal PRINCIPAL_3 = - new Principal().name("principal-3"); - - @Test - public void testCreatesNewPrincipalOverwritesOldPrincipalRemovesDroppedPrincipal() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<Principal> plan = - planner.planPrincipalSync(List.of(PRINCIPAL_1, PRINCIPAL_2), List.of(PRINCIPAL_2, PRINCIPAL_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(PRINCIPAL_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(PRINCIPAL_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(PRINCIPAL_1)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(PRINCIPAL_2)); - Assertions.assertTrue(plan.entitiesToOverwrite().contains(PRINCIPAL_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(PRINCIPAL_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(PRINCIPAL_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(PRINCIPAL_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(PRINCIPAL_3)); - } - - private static final PrincipalRole ASSIGNED_TO_PRINCIPAL_1 = - new PrincipalRole().name("principal-role-1"); - - private static final PrincipalRole ASSIGNED_TO_PRINCIPAL_2 = - new PrincipalRole().name("principal-role-2"); - - private static final PrincipalRole ASSIGNED_TO_PRINCIPAL_3 = - new PrincipalRole().name("principal-role-3"); - - @Test - public void testAssignsNewPrincipalRoleRevokesDroppedPrincipalRoleForPrincipal() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<PrincipalRole> plan = - planner.planAssignPrincipalsToPrincipalRolesSync( - "principal", - List.of(ASSIGNED_TO_PRINCIPAL_1, ASSIGNED_TO_PRINCIPAL_2), - List.of(ASSIGNED_TO_PRINCIPAL_2, ASSIGNED_TO_PRINCIPAL_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(ASSIGNED_TO_PRINCIPAL_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(ASSIGNED_TO_PRINCIPAL_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(ASSIGNED_TO_PRINCIPAL_1)); - - // special case: no concept of overwriting the assignment of a principal role - Assertions.assertFalse(plan.entitiesToCreate().contains(ASSIGNED_TO_PRINCIPAL_2)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(ASSIGNED_TO_PRINCIPAL_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(ASSIGNED_TO_PRINCIPAL_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(ASSIGNED_TO_PRINCIPAL_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(ASSIGNED_TO_PRINCIPAL_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(ASSIGNED_TO_PRINCIPAL_3)); - } - - private static final PrincipalRole PRINCIPAL_ROLE_1 = - new PrincipalRole().name("principal-role-1"); - - private static final PrincipalRole PRINCIPAL_ROLE_2 = - new PrincipalRole().name("principal-role-2"); - - private static final PrincipalRole PRINCIPAL_ROLE_3 = - new PrincipalRole().name("principal-role-3"); - - @Test - public void testCreatesNewPrincipalRoleOverwritesOldPrincipalRoleRemovesDroppedPrincipalRole() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<PrincipalRole> plan = - planner.planPrincipalRoleSync( - List.of(PRINCIPAL_ROLE_1, PRINCIPAL_ROLE_2), - List.of(PRINCIPAL_ROLE_2, PRINCIPAL_ROLE_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(PRINCIPAL_ROLE_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(PRINCIPAL_ROLE_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(PRINCIPAL_ROLE_1)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(PRINCIPAL_ROLE_2)); - Assertions.assertTrue(plan.entitiesToOverwrite().contains(PRINCIPAL_ROLE_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(PRINCIPAL_ROLE_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(PRINCIPAL_ROLE_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(PRINCIPAL_ROLE_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(PRINCIPAL_ROLE_3)); - } - - private static final CatalogRole CATALOG_ROLE_1 = new CatalogRole().name("catalog-role-1"); - - private static final CatalogRole CATALOG_ROLE_2 = new CatalogRole().name("catalog-role-2"); - - private static final CatalogRole CATALOG_ROLE_3 = new CatalogRole().name("catalog-role-3"); - - @Test - public void testCreatesNewCatalogRoleOverwritesOldCatalogRoleRemovesDroppedCatalogRole() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<CatalogRole> plan = - planner.planCatalogRoleSync( - "catalog", - List.of(CATALOG_ROLE_1, CATALOG_ROLE_2), - List.of(CATALOG_ROLE_2, CATALOG_ROLE_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(CATALOG_ROLE_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(CATALOG_ROLE_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(CATALOG_ROLE_1)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(CATALOG_ROLE_2)); - Assertions.assertTrue(plan.entitiesToOverwrite().contains(CATALOG_ROLE_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(CATALOG_ROLE_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(CATALOG_ROLE_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(CATALOG_ROLE_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(CATALOG_ROLE_3)); - } - - private static final GrantResource GRANT_1 = - new GrantResource().type(GrantResource.TypeEnum.CATALOG); - - private static final GrantResource GRANT_2 = - new GrantResource().type(GrantResource.TypeEnum.NAMESPACE); - - private static final GrantResource GRANT_3 = - new GrantResource().type(GrantResource.TypeEnum.TABLE); - - @Test - public void testCreatesNewGrantResourceRemovesDroppedGrantResource() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<GrantResource> plan = - planner.planGrantSync( - "catalog", "catalogRole", List.of(GRANT_1, GRANT_2), List.of(GRANT_2, GRANT_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(GRANT_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(GRANT_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(GRANT_1)); - - // special case: no concept of overwriting a grant - Assertions.assertFalse(plan.entitiesToCreate().contains(GRANT_2)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(GRANT_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(GRANT_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(GRANT_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(GRANT_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(GRANT_3)); - } - - private static final PrincipalRole ASSIGNED_TO_CATALOG_ROLE_1 = - new PrincipalRole().name("principal-role-1"); - - private static final PrincipalRole ASSIGNED_TO_CATALOG_ROLE_2 = - new PrincipalRole().name("principal-role-2"); - - private static final PrincipalRole ASSIGNED_TO_CATALOG_ROLE_3 = - new PrincipalRole().name("principal-role-3"); - - @Test - public void testAssignsNewPrincipalRoleRevokesDroppedPrincipalRole() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<PrincipalRole> plan = - planner.planAssignPrincipalRolesToCatalogRolesSync( - "catalog", - "catalogRole", - List.of(ASSIGNED_TO_PRINCIPAL_1, ASSIGNED_TO_PRINCIPAL_2), - List.of(ASSIGNED_TO_PRINCIPAL_2, ASSIGNED_TO_PRINCIPAL_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(ASSIGNED_TO_PRINCIPAL_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(ASSIGNED_TO_PRINCIPAL_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(ASSIGNED_TO_PRINCIPAL_1)); - - // special case: no concept of overwriting the assignment of a principal role - Assertions.assertFalse(plan.entitiesToCreate().contains(ASSIGNED_TO_PRINCIPAL_2)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(ASSIGNED_TO_PRINCIPAL_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(ASSIGNED_TO_PRINCIPAL_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(ASSIGNED_TO_PRINCIPAL_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(ASSIGNED_TO_PRINCIPAL_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(ASSIGNED_TO_PRINCIPAL_3)); - } - - private static final Namespace NS_1 = Namespace.of("ns1"); - - private static final Namespace NS_2 = Namespace.of("ns2"); - - private static final Namespace NS_3 = Namespace.of("ns3"); - - @Test - public void testCreatesNewNamespaceOverwritesOldNamespaceDropsDroppedNamespace() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - SynchronizationPlan<Namespace> plan = - planner.planNamespaceSync( - "catalog", Namespace.empty(), List.of(NS_1, NS_2), List.of(NS_2, NS_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(NS_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(NS_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(NS_1)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(NS_2)); - Assertions.assertTrue(plan.entitiesToOverwrite().contains(NS_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(NS_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(NS_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(NS_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(NS_3)); - } - - private static final TableIdentifier TABLE_1 = TableIdentifier.of("ns", "table1"); - - private static final TableIdentifier TABLE_2 = TableIdentifier.of("ns", "table2"); - - private static final TableIdentifier TABLE_3 = TableIdentifier.of("ns", "table3"); - - @Test - public void - testCreatesNewTableIdentifierOverwritesOldTableIdentifierRevokesDroppedTableIdentifier() { - SourceParitySynchronizationPlanner planner = new SourceParitySynchronizationPlanner(); - - SynchronizationPlan<TableIdentifier> plan = - planner.planTableSync( - "catalog", Namespace.empty(), Set.of(TABLE_1, TABLE_2), Set.of(TABLE_2, TABLE_3)); - - Assertions.assertTrue(plan.entitiesToCreate().contains(TABLE_1)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(TABLE_1)); - Assertions.assertFalse(plan.entitiesToRemove().contains(TABLE_1)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(TABLE_2)); - Assertions.assertTrue(plan.entitiesToOverwrite().contains(TABLE_2)); - Assertions.assertFalse(plan.entitiesToRemove().contains(TABLE_2)); - - Assertions.assertFalse(plan.entitiesToCreate().contains(TABLE_3)); - Assertions.assertFalse(plan.entitiesToOverwrite().contains(TABLE_3)); - Assertions.assertTrue(plan.entitiesToRemove().contains(TABLE_3)); - } -} diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/AbstractBaseStrategyPlannerTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/AbstractBaseStrategyPlannerTest.java new file mode 100644 index 0000000..e5b9679 --- /dev/null +++ b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/AbstractBaseStrategyPlannerTest.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.tools.sync.polaris.strategy; + +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.polaris.core.admin.model.Catalog; +import org.apache.polaris.core.admin.model.CatalogRole; +import org.apache.polaris.core.admin.model.GrantResource; +import org.apache.polaris.core.admin.model.Principal; +import org.apache.polaris.core.admin.model.PrincipalRole; +import org.apache.polaris.tools.sync.polaris.planning.BaseStrategyPlanner; +import org.apache.polaris.tools.sync.polaris.planning.SynchronizationPlanner; +import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationPlan; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public abstract class AbstractBaseStrategyPlannerTest { + + private final BaseStrategyPlanner.Strategy strategy; + + protected AbstractBaseStrategyPlannerTest(BaseStrategyPlanner.Strategy strategy) { + this.strategy = strategy; + } + + protected final Catalog CATALOG_1 = new Catalog().name("catalog-1"); + + protected final Catalog CATALOG_2 = new Catalog().name("catalog-2"); + + protected final Catalog CATALOG_3 = new Catalog().name("catalog-3"); + + protected void testStrategy(Function<SynchronizationPlanner, SynchronizationPlan<?>> planSupplier, + Object entityToCreate, + Object entityToOverwrite, + Object entityToRemove) { + testStrategy(planSupplier, true, entityToCreate, entityToOverwrite, entityToRemove); + } + + /** + * Test a generated plan for correctness in the case that there is 1 entity only on the source, + * 1 entity on both source and target, and 1 entity only on target. + * @param planSupplier generates the plan + * @param requiresOverwrite if the entity requires a drop and recreate (most grant records do not) + * @param entityOnSource the entity that is only on the source instance + * @param entityOnBoth the entity that is on both instances + * @param entityOnTarget the entity that is only on the target instance + */ + protected void testStrategy( + Function<SynchronizationPlanner, SynchronizationPlan<?>> planSupplier, + boolean requiresOverwrite, + Object entityOnSource, + Object entityOnBoth, + Object entityOnTarget + ) { + BaseStrategyPlanner planner = new BaseStrategyPlanner(strategy); + + SynchronizationPlan<?> plan = planSupplier.apply(planner); + + Assertions.assertTrue(plan.entitiesToCreate().contains(entityOnSource)); + Assertions.assertFalse(plan.entitiesToOverwrite().contains(entityOnSource)); + Assertions.assertFalse(plan.entitiesToRemove().contains(entityOnSource)); + + if (!requiresOverwrite && strategy != BaseStrategyPlanner.Strategy.CREATE_ONLY) { + // if the entity is not one that needs to be overwritten, then any strategy + // besides CREATE_ONLY should instead schedule a "create" operation + Assertions.assertTrue(plan.entitiesToCreate().contains(entityOnBoth)); + } else { + Assertions.assertFalse(plan.entitiesToCreate().contains(entityOnBoth)); + } + + if (strategy == BaseStrategyPlanner.Strategy.CREATE_ONLY) { + // make sure entities to overwrite are skipped in CREATE_ONLY mode + Assertions.assertTrue(plan.entitiesToSkip().contains(entityOnBoth)); + } else if (requiresOverwrite) { + Assertions.assertTrue(plan.entitiesToOverwrite().contains(entityOnBoth)); + } + Assertions.assertFalse(plan.entitiesToRemove().contains(entityOnBoth)); + + Assertions.assertFalse(plan.entitiesToCreate().contains(entityOnTarget)); + Assertions.assertFalse(plan.entitiesToOverwrite().contains(entityOnTarget)); + if (strategy == BaseStrategyPlanner.Strategy.REPLICATE) { + Assertions.assertTrue(plan.entitiesToRemove().contains(entityOnTarget)); + } else { + // only REPLICATE should remove entities from the target + Assertions.assertTrue(plan.entitiesToSkipAndSkipChildren().contains(entityOnTarget)); + } + } + + @Test + public void testCatalogStrategy() { + testStrategy(planner -> planner.planCatalogSync( + List.of(CATALOG_1, CATALOG_2), List.of(CATALOG_2, CATALOG_3)), + CATALOG_1, CATALOG_2, CATALOG_3); + } + + protected final Principal PRINCIPAL_1 = new Principal().name("principal-1"); + + protected final Principal PRINCIPAL_2 = new Principal().name("principal-2"); + + protected final Principal PRINCIPAL_3 = new Principal().name("principal-3"); + + @Test + public void testPrincipalStrategy() { + testStrategy(planner -> planner.planPrincipalSync( + List.of(PRINCIPAL_1, PRINCIPAL_2), List.of(PRINCIPAL_2, PRINCIPAL_3)), + PRINCIPAL_1, PRINCIPAL_2, PRINCIPAL_3); + } + + protected final PrincipalRole ASSIGNED_TO_PRINCIPAL_1 = new PrincipalRole().name("principal-role-1"); + + protected final PrincipalRole ASSIGNED_TO_PRINCIPAL_2 = new PrincipalRole().name("principal-role-2"); + + protected final PrincipalRole ASSIGNED_TO_PRINCIPAL_3 = new PrincipalRole().name("principal-role-3"); + + @Test + public void testAssignmentOfPrincipalRoleToPrincipalStrategy() { + testStrategy(planner -> + planner.planAssignPrincipalsToPrincipalRolesSync( + "principal", + List.of(ASSIGNED_TO_PRINCIPAL_1, ASSIGNED_TO_PRINCIPAL_2), + List.of(ASSIGNED_TO_PRINCIPAL_2, ASSIGNED_TO_PRINCIPAL_3)), + false, /* requiresOverwrite */ + ASSIGNED_TO_PRINCIPAL_1, ASSIGNED_TO_PRINCIPAL_2, ASSIGNED_TO_PRINCIPAL_3); + } + + protected final PrincipalRole PRINCIPAL_ROLE_1 = new PrincipalRole().name("principal-role-1"); + + protected final PrincipalRole PRINCIPAL_ROLE_2 = new PrincipalRole().name("principal-role-2"); + + protected final PrincipalRole PRINCIPAL_ROLE_3 = new PrincipalRole().name("principal-role-3"); + + @Test + public void testPrincipalRoleStrategy() { + testStrategy(planner -> planner.planPrincipalRoleSync( + List.of(PRINCIPAL_ROLE_1, PRINCIPAL_ROLE_2), + List.of(PRINCIPAL_ROLE_2, PRINCIPAL_ROLE_3)), + PRINCIPAL_ROLE_1, PRINCIPAL_ROLE_2, PRINCIPAL_ROLE_3); + } + + protected final CatalogRole CATALOG_ROLE_1 = new CatalogRole().name("catalog-role-1"); + + protected final CatalogRole CATALOG_ROLE_2 = new CatalogRole().name("catalog-role-2"); + + protected final CatalogRole CATALOG_ROLE_3 = new CatalogRole().name("catalog-role-3"); + + @Test + public void testCatalogRoleStrategy() { + testStrategy(planner -> + planner.planCatalogRoleSync( + "catalog", + List.of(CATALOG_ROLE_1, CATALOG_ROLE_2), + List.of(CATALOG_ROLE_2, CATALOG_ROLE_3)), + CATALOG_ROLE_1, CATALOG_ROLE_2, CATALOG_ROLE_3); + + } + + protected final GrantResource GRANT_1 = new GrantResource().type(GrantResource.TypeEnum.CATALOG); + + protected final GrantResource GRANT_2 = new GrantResource().type(GrantResource.TypeEnum.NAMESPACE); + + protected final GrantResource GRANT_3 = new GrantResource().type(GrantResource.TypeEnum.TABLE); + + @Test + public void testCreatesNewGrantResourceRemovesDroppedGrantResource() { + testStrategy(planner -> planner.planGrantSync( + "catalog", "catalogRole", + List.of(GRANT_1, GRANT_2), List.of(GRANT_2, GRANT_3)), + false, /* requiresOverwrite */ + GRANT_1, GRANT_2, GRANT_3); + } + + protected final PrincipalRole ASSIGNED_TO_CATALOG_ROLE_1 = new PrincipalRole().name("principal-role-1"); + + protected final PrincipalRole ASSIGNED_TO_CATALOG_ROLE_2 = new PrincipalRole().name("principal-role-2"); + + protected final PrincipalRole ASSIGNED_TO_CATALOG_ROLE_3 = new PrincipalRole().name("principal-role-3"); + + @Test + public void testAssignPrincipalRoleToCatalogRoleStrategy() { + testStrategy(planner -> + planner.planAssignPrincipalRolesToCatalogRolesSync( + "catalog", + "catalogRole", + List.of(ASSIGNED_TO_CATALOG_ROLE_1, ASSIGNED_TO_CATALOG_ROLE_2), + List.of(ASSIGNED_TO_CATALOG_ROLE_2, ASSIGNED_TO_CATALOG_ROLE_3)), + false, /* requiresOverwrite */ + ASSIGNED_TO_CATALOG_ROLE_1, ASSIGNED_TO_CATALOG_ROLE_2, ASSIGNED_TO_CATALOG_ROLE_3); + } + + protected final Namespace NS_1 = Namespace.of("ns1"); + + protected final Namespace NS_2 = Namespace.of("ns2"); + + protected final Namespace NS_3 = Namespace.of("ns3"); + + @Test + public void testNamespaceStrategy() { + testStrategy(planner -> planner.planNamespaceSync( + "catalog", Namespace.empty(), List.of(NS_1, NS_2), List.of(NS_2, NS_3)), + NS_1, NS_2, NS_3); + } + + protected final TableIdentifier TABLE_1 = TableIdentifier.of("ns", "table1"); + + protected final TableIdentifier TABLE_2 = TableIdentifier.of("ns", "table2"); + + protected final TableIdentifier TABLE_3 = TableIdentifier.of("ns", "table3"); + + @Test + public void testTableStrategy() { + testStrategy(planner -> + planner.planTableSync( + "catalog", Namespace.empty(), Set.of(TABLE_1, TABLE_2), Set.of(TABLE_2, TABLE_3)), + TABLE_1, TABLE_2, TABLE_3); + } + +} diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/CreateAndOverwriteBaseStrategyPlannerTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/CreateAndOverwriteBaseStrategyPlannerTest.java new file mode 100644 index 0000000..a2d41d5 --- /dev/null +++ b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/CreateAndOverwriteBaseStrategyPlannerTest.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.tools.sync.polaris.strategy; + +import org.apache.polaris.tools.sync.polaris.planning.BaseStrategyPlanner; + +public class CreateAndOverwriteBaseStrategyPlannerTest extends AbstractBaseStrategyPlannerTest { + + protected CreateAndOverwriteBaseStrategyPlannerTest() { + super(BaseStrategyPlanner.Strategy.CREATE_AND_OVERWRITE); + } + +} diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/CreateOnlyBaseStrategyPlannerTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/CreateOnlyBaseStrategyPlannerTest.java new file mode 100644 index 0000000..ebf1678 --- /dev/null +++ b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/CreateOnlyBaseStrategyPlannerTest.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.tools.sync.polaris.strategy; + +import org.apache.polaris.tools.sync.polaris.planning.BaseStrategyPlanner; + +public class CreateOnlyBaseStrategyPlannerTest extends AbstractBaseStrategyPlannerTest { + + protected CreateOnlyBaseStrategyPlannerTest() { + super(BaseStrategyPlanner.Strategy.CREATE_ONLY); + } + +} diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/ReplicateBaseStrategyPlannerTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/ReplicateBaseStrategyPlannerTest.java new file mode 100644 index 0000000..11e8322 --- /dev/null +++ b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/strategy/ReplicateBaseStrategyPlannerTest.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.tools.sync.polaris.strategy; + + +import org.apache.polaris.tools.sync.polaris.planning.BaseStrategyPlanner; + +public class ReplicateBaseStrategyPlannerTest extends AbstractBaseStrategyPlannerTest { + + protected ReplicateBaseStrategyPlannerTest() { + super(BaseStrategyPlanner.Strategy.REPLICATE); + } + +} diff --git a/polaris-synchronizer/cli/src/main/java/org/apache/polaris/tools/sync/polaris/SyncPolarisCommand.java b/polaris-synchronizer/cli/src/main/java/org/apache/polaris/tools/sync/polaris/SyncPolarisCommand.java index 31388eb..851d66f 100644 --- a/polaris-synchronizer/cli/src/main/java/org/apache/polaris/tools/sync/polaris/SyncPolarisCommand.java +++ b/polaris-synchronizer/cli/src/main/java/org/apache/polaris/tools/sync/polaris/SyncPolarisCommand.java @@ -24,7 +24,7 @@ import org.apache.polaris.tools.sync.polaris.catalog.ETagManager; import org.apache.polaris.tools.sync.polaris.planning.AccessControlAwarePlanner; import org.apache.polaris.tools.sync.polaris.planning.CatalogNameFilterPlanner; import org.apache.polaris.tools.sync.polaris.planning.ModificationAwarePlanner; -import org.apache.polaris.tools.sync.polaris.planning.SourceParitySynchronizationPlanner; +import org.apache.polaris.tools.sync.polaris.planning.BaseStrategyPlanner; import org.apache.polaris.tools.sync.polaris.planning.SynchronizationPlanner; import org.apache.polaris.tools.sync.polaris.service.PolarisService; import org.apache.polaris.tools.sync.polaris.service.impl.PolarisApiService; @@ -98,10 +98,30 @@ public class SyncPolarisCommand implements Callable<Integer> { ) private String catalogNameRegex; + @CommandLine.Option( + names = {"--diff-only"}, + description = "Only synchronize the diff between the source and target Polaris." + ) + private boolean diffOnly; + + @CommandLine.Option( + names = {"--strategy"}, + defaultValue = "CREATE_ONLY", + description = "The synchronization strategy to use. Options: " + + "\n\t- CREATE_ONLY: (default) Only create entities that exist on the source and do not exist on the " + + "target." + + "\n\t- CREATE_AND_OVERWRITE: Create entities that exist on the source and not on the target and " + + "overwrite entities that exist on both the source and the target." + + "\n\t- REPLICATE: Create entities that exist on the source and not on the target, " + + "overwrite entities that exist on both the source and the target, " + + "and remove entities from the target that do not exist on the source." + ) + private BaseStrategyPlanner.Strategy strategy; + @Override public Integer call() throws Exception { - SynchronizationPlanner planner = SynchronizationPlanner.builder(new SourceParitySynchronizationPlanner()) - .wrapBy(ModificationAwarePlanner::new) + SynchronizationPlanner planner = SynchronizationPlanner.builder(new BaseStrategyPlanner(strategy)) + .conditionallyWrapBy(diffOnly, ModificationAwarePlanner::new) .conditionallyWrapBy(catalogNameRegex != null, p -> new CatalogNameFilterPlanner(catalogNameRegex, p)) .wrapBy(AccessControlAwarePlanner::new) .build(); @@ -124,7 +144,8 @@ public class SyncPolarisCommand implements Callable<Integer> { planner, source, target, - etagManager); + etagManager, + diffOnly); synchronizer.syncPrincipalRoles(); if (shouldSyncPrincipals) { consoleLog.warn("Principal migration will reset credentials on the target Polaris instance. " +