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. " +

Reply via email to