From c491f04d9cf28856aa3aaff9c67c42f56c362aa0 Mon Sep 17 00:00:00 2001 From: Sai Dixith Date: Wed, 22 Jul 2026 15:35:27 +0530 Subject: [PATCH 1/2] Synchronizer: Add consolidated sync report and --fail-on-error flag Prints a consolidated per-entity-type synchronization report at the end of every run, and adds --fail-on-error to exit non-zero when the report contains failures. Entities that were already in sync (skipped) are now tallied separately in the report so idempotent re-runs don't render an empty report. --- polaris-synchronizer/README.md | 22 +++ .../sync/polaris/PolarisSynchronizer.java | 173 ++++++++++++++---- .../polaris/planning/plan/EntityType.java | 32 ++++ .../polaris/planning/plan/SyncOutcome.java | 28 +++ .../planning/plan/SynchronizationReport.java | 140 ++++++++++++++ .../plan/SynchronizationReportTest.java | 112 ++++++++++++ .../sync/polaris/SyncPolarisCommand.java | 17 +- 7 files changed, 486 insertions(+), 38 deletions(-) create mode 100644 polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/EntityType.java create mode 100644 polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SyncOutcome.java create mode 100644 polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReport.java create mode 100644 polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReportTest.java diff --git a/polaris-synchronizer/README.md b/polaris-synchronizer/README.md index c7e754a3..99b1da06 100644 --- a/polaris-synchronizer/README.md +++ b/polaris-synchronizer/README.md @@ -184,3 +184,25 @@ java -jar cli/build/libs/polaris-synchronizer-cli.jar sync-polaris \ > nor remove or modify them or their assignments to principals/principal-roles on the target. This is to accommodate that > the tool itself will be running with the permission levels for these principals and roles, and we do not want to modify > the tool's permissions at runtime. + +At the end of every run, `sync-polaris` prints a consolidated synchronization report summarizing how +many entities of each type were created, overwritten, removed, skipped (already in sync), or failed, +along with a list of any failures encountered: + +``` +=== Synchronization Report === +Principal: 3 created, 0 overwritten, 0 removed, 0 skipped, 1 failed +Catalog: 2 created, 1 overwritten, 0 removed, 5 skipped, 0 failed +Table: 20 created, 0 overwritten, 0 removed, 40 skipped, 2 failed + +Failures: + - Principal 'alice': + - Table 'db.orders': +=============================== +``` + +By default, `sync-polaris` exits with status `0` regardless of whether individual entities failed to +synchronize (matching the pre-existing behavior, where failures are logged and the run continues). +Pass `--fail-on-error` to make the command exit with a non-zero status if the report contains any +failures. This is distinct from `--halt-on-failure`, which aborts the run as soon as the first failure +occurs; `--fail-on-error` lets the run finish synchronizing everything it can, then fails afterward. 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 dad781f8..0e33e474 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 @@ -36,7 +36,10 @@ import org.apache.polaris.tools.sync.polaris.catalog.ETagManager; import org.apache.polaris.tools.sync.polaris.catalog.MetadataNotModifiedException; import org.apache.polaris.tools.sync.polaris.planning.SynchronizationPlanner; +import org.apache.polaris.tools.sync.polaris.planning.plan.EntityType; +import org.apache.polaris.tools.sync.polaris.planning.plan.SyncOutcome; import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationPlan; +import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationReport; import org.apache.polaris.tools.sync.polaris.service.IcebergCatalogService; import org.apache.polaris.tools.sync.polaris.service.PolarisService; import org.apache.polaris.tools.sync.polaris.service.impl.PolarisIcebergCatalogService; @@ -63,6 +66,8 @@ public class PolarisSynchronizer { private final boolean diffOnly; + private final SynchronizationReport report; + public PolarisSynchronizer( Logger clientLogger, boolean haltOnFailure, @@ -70,7 +75,8 @@ public PolarisSynchronizer( PolarisService source, PolarisService target, ETagManager etagManager, - boolean diffOnly) { + boolean diffOnly, + SynchronizationReport report) { this.clientLogger = clientLogger == null ? LoggerFactory.getLogger(PolarisSynchronizer.class) : clientLogger; this.haltOnFailure = haltOnFailure; @@ -79,6 +85,7 @@ public PolarisSynchronizer( this.target = target; this.etagManager = etagManager; this.diffOnly = diffOnly; + this.report = report; } /** @@ -123,16 +130,20 @@ public void syncPrincipals() { principalSyncPlan .entitiesToSkipAndSkipChildren() .forEach( - principal -> - clientLogger.info("Skipping principal {}.", principal.getName())); + principal -> { + clientLogger.info("Skipping principal {}.", principal.getName()); + report.recordSuccess(EntityType.PRINCIPAL, SyncOutcome.SKIPPED); + }); principalSyncPlan .entitiesNotModified() .forEach( - principal -> + principal -> { clientLogger.info( "No change detected for principal {}, skipping.", - principal.getName())); + principal.getName()); + report.recordSuccess(EntityType.PRINCIPAL, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; final int totalSyncsToComplete = totalSyncsToComplete(principalSyncPlan); @@ -147,10 +158,12 @@ public void syncPrincipals() { ++syncsCompleted, totalSyncsToComplete ); + report.recordSuccess(EntityType.PRINCIPAL, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error("Failed to create principal {} on target. - {}/{}", principal.getName(), ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.PRINCIPAL, principal.getName(), e); } } @@ -165,10 +178,12 @@ public void syncPrincipals() { ++syncsCompleted, totalSyncsToComplete ); + report.recordSuccess(EntityType.PRINCIPAL, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error("Failed to overwrite principal {} on target. - {}/{}", principal.getName(), ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.PRINCIPAL, principal.getName(), e); } } @@ -177,10 +192,12 @@ public void syncPrincipals() { target.dropPrincipal(principal.getName()); clientLogger.info("Removed principal {} on target. - {}/{}", principal.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error("Failed to remove principal {} ont target. - {}/{}", principal.getName(), ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.PRINCIPAL, principal.getName(), e); } } @@ -225,17 +242,21 @@ public void syncAssignedPrincipalRolesForPrincipal(String principalName) { assignedPrincipalRoleSyncPlan .entitiesToSkip() .forEach( - principalRole -> + principalRole -> { clientLogger.info("Skipping assignment of principal-role {} to principal {}.", - principalName, principalRole.getName())); + principalName, principalRole.getName()); + report.recordSuccess(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, SyncOutcome.SKIPPED); + }); assignedPrincipalRoleSyncPlan .entitiesNotModified() .forEach( - principalRole -> + principalRole -> { clientLogger.info( "Principal {} is already assigned to principal-role {}, skipping.", - principalName, principalRole.getName())); + principalName, principalRole.getName()); + report.recordSuccess(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; final int totalSyncsToComplete = totalSyncsToComplete(assignedPrincipalRoleSyncPlan); @@ -245,10 +266,12 @@ public void syncAssignedPrincipalRolesForPrincipal(String principalName) { target.assignPrincipalRole(principalName, principalRole.getName()); clientLogger.info("Assigned principal-role {} to principal {}. - {}/{}", principalRole.getName(), principalName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error("Failed to assign principal-role {} to principal {}. - {}/{}", principalRole.getName(), principalName, ++syncsCompleted, totalSyncsToComplete); + report.recordFailure(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, principalRole.getName(), e); } } @@ -257,10 +280,12 @@ public void syncAssignedPrincipalRolesForPrincipal(String principalName) { target.assignPrincipalRole(principalName, principalRole.getName()); clientLogger.info("Assigned principal-role {} to principal {}. - {}/{}", principalRole.getName(), principalName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error("Failed to assign principal-role {} to principal {}. - {}/{}", principalRole.getName(), principalName, ++syncsCompleted, totalSyncsToComplete); + report.recordFailure(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, principalRole.getName(), e); } } @@ -269,10 +294,12 @@ public void syncAssignedPrincipalRolesForPrincipal(String principalName) { target.revokePrincipalRole(principalName, principalRole.getName()); clientLogger.info("Revoked principal-role {} from principal {}. - {}/{}", principalRole.getName(), principalName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error("Failed to revoke principal-role {} to principal {}. - {}/{}", principalRole.getName(), principalName, ++syncsCompleted, totalSyncsToComplete); + report.recordFailure(EntityType.PRINCIPAL_ROLE_ASSIGNMENT, principalRole.getName(), e); } } } @@ -307,16 +334,20 @@ public void syncPrincipalRoles() { principalRoleSyncPlan .entitiesToSkip() .forEach( - principalRole -> - clientLogger.info("Skipping principal-role {}.", principalRole.getName())); + principalRole -> { + clientLogger.info("Skipping principal-role {}.", principalRole.getName()); + report.recordSuccess(EntityType.PRINCIPAL_ROLE, SyncOutcome.SKIPPED); + }); principalRoleSyncPlan .entitiesNotModified() .forEach( - principalRole -> + principalRole -> { clientLogger.info( "No change detected for principal-role {}, skipping.", - principalRole.getName())); + principalRole.getName()); + report.recordSuccess(EntityType.PRINCIPAL_ROLE, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; final int totalSyncsToComplete = totalSyncsToComplete(principalRoleSyncPlan); @@ -329,6 +360,7 @@ public void syncPrincipalRoles() { principalRole.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL_ROLE, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -337,6 +369,7 @@ public void syncPrincipalRoles() { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.PRINCIPAL_ROLE, principalRole.getName(), e); } } @@ -349,6 +382,7 @@ public void syncPrincipalRoles() { principalRole.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL_ROLE, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -357,6 +391,7 @@ public void syncPrincipalRoles() { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.PRINCIPAL_ROLE, principalRole.getName(), e); } } @@ -368,6 +403,7 @@ public void syncPrincipalRoles() { principalRole.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.PRINCIPAL_ROLE, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -376,6 +412,7 @@ public void syncPrincipalRoles() { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.PRINCIPAL_ROLE, principalRole.getName(), e); } } } @@ -434,22 +471,26 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String assignedPrincipalRoleSyncPlan .entitiesToSkip() .forEach( - principalRole -> + principalRole -> { clientLogger.info( "Skipping assignment of principal-role {} to catalog-role {} in catalog {}.", principalRole.getName(), catalogRoleName, - catalogName)); + catalogName); + report.recordSuccess(EntityType.CATALOG_ROLE_ASSIGNMENT, SyncOutcome.SKIPPED); + }); assignedPrincipalRoleSyncPlan .entitiesNotModified() .forEach( - principalRole -> + principalRole -> { clientLogger.info( "Principal-role {} is already assigned to catalog-role {} in catalog {}. Skipping.", principalRole.getName(), catalogRoleName, - catalogName)); + catalogName); + report.recordSuccess(EntityType.CATALOG_ROLE_ASSIGNMENT, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; int totalSyncsToComplete = totalSyncsToComplete(assignedPrincipalRoleSyncPlan); @@ -465,6 +506,7 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG_ROLE_ASSIGNMENT, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -475,6 +517,7 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG_ROLE_ASSIGNMENT, principalRole.getName(), e); } } @@ -489,6 +532,7 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG_ROLE_ASSIGNMENT, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -499,6 +543,7 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG_ROLE_ASSIGNMENT, principalRole.getName(), e); } } @@ -513,6 +558,7 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG_ROLE_ASSIGNMENT, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -523,6 +569,7 @@ public void syncAssigneePrincipalRolesForCatalogRole(String catalogName, String ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG_ROLE_ASSIGNMENT, principalRole.getName(), e); } } } @@ -556,21 +603,28 @@ public void syncCatalogs() { catalogSyncPlan .entitiesToSkip() - .forEach(catalog -> clientLogger.info("Skipping catalog {}.", catalog.getName())); + .forEach(catalog -> { + clientLogger.info("Skipping catalog {}.", catalog.getName()); + report.recordSuccess(EntityType.CATALOG, SyncOutcome.SKIPPED); + }); catalogSyncPlan .entitiesToSkipAndSkipChildren() .forEach( - catalog -> + catalog -> { clientLogger.info( - "Skipping catalog {} and all child entities.", catalog.getName())); + "Skipping catalog {} and all child entities.", catalog.getName()); + report.recordSuccess(EntityType.CATALOG, SyncOutcome.SKIPPED); + }); catalogSyncPlan .entitiesNotModified() .forEach( - catalog -> + catalog -> { clientLogger.info( - "No change detected in catalog {}. Skipping.", catalog.getName())); + "No change detected in catalog {}. Skipping.", catalog.getName()); + report.recordSuccess(EntityType.CATALOG, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; int totalSyncsToComplete = totalSyncsToComplete(catalogSyncPlan); @@ -583,6 +637,7 @@ public void syncCatalogs() { catalog.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -591,6 +646,7 @@ public void syncCatalogs() { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG, catalog.getName(), e); } } @@ -603,6 +659,7 @@ public void syncCatalogs() { catalog.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -611,6 +668,7 @@ public void syncCatalogs() { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG, catalog.getName(), e); } } @@ -622,6 +680,7 @@ public void syncCatalogs() { catalog.getName(), ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -630,6 +689,7 @@ public void syncCatalogs() { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG, catalog.getName(), e); } } @@ -654,6 +714,7 @@ public void syncCatalogs() { "Failed to synchronize Iceberg REST catalog for Polaris catalog {}.", catalog.getName(), e); + report.recordFailure(EntityType.CATALOG, catalog.getName(), e); if (haltOnFailure) throw new RuntimeException(e); continue; } @@ -707,27 +768,33 @@ public void syncCatalogRoles(String catalogName) { catalogRoleSyncPlan .entitiesToSkip() .forEach( - catalogRole -> + catalogRole -> { clientLogger.info( - "Skipping catalog-role {} in catalog {}.", catalogRole.getName(), catalogName)); + "Skipping catalog-role {} in catalog {}.", catalogRole.getName(), catalogName); + report.recordSuccess(EntityType.CATALOG_ROLE, SyncOutcome.SKIPPED); + }); catalogRoleSyncPlan .entitiesToSkipAndSkipChildren() .forEach( - catalogRole -> + catalogRole -> { clientLogger.info( "Skipping catalog-role {} in catalog {} and all child entities.", catalogRole.getName(), - catalogName)); + catalogName); + report.recordSuccess(EntityType.CATALOG_ROLE, SyncOutcome.SKIPPED); + }); catalogRoleSyncPlan .entitiesNotModified() .forEach( - catalogRole -> + catalogRole -> { clientLogger.info( "No change detected in catalog-role {} in catalog {}. Skipping.", catalogRole.getName(), - catalogName)); + catalogName); + report.recordSuccess(EntityType.CATALOG_ROLE, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; int totalSyncsToComplete = totalSyncsToComplete(catalogRoleSyncPlan); @@ -741,6 +808,7 @@ public void syncCatalogRoles(String catalogName) { catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG_ROLE, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -750,6 +818,7 @@ public void syncCatalogRoles(String catalogName) { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG_ROLE, catalogRole.getName(), e); } } @@ -763,6 +832,7 @@ public void syncCatalogRoles(String catalogName) { catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG_ROLE, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -772,6 +842,7 @@ public void syncCatalogRoles(String catalogName) { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG_ROLE, catalogRole.getName(), e); } } @@ -784,6 +855,7 @@ public void syncCatalogRoles(String catalogName) { catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.CATALOG_ROLE, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -793,6 +865,7 @@ public void syncCatalogRoles(String catalogName) { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.CATALOG_ROLE, catalogRole.getName(), e); } } @@ -853,22 +926,26 @@ private void syncGrants(String catalogName, String catalogRoleName) { grantSyncPlan .entitiesToSkip() .forEach( - grant -> + grant -> { clientLogger.info( "Skipping addition of grant {} to catalog-role {} in catalog {}.", grant.getType(), catalogRoleName, - catalogName)); + catalogName); + report.recordSuccess(EntityType.GRANT, SyncOutcome.SKIPPED); + }); grantSyncPlan .entitiesNotModified() .forEach( - grant -> + grant -> { clientLogger.info( "Grant {} was already added to catalog-role {} in catalog {}. Skipping.", grant.getType(), catalogRoleName, - catalogName)); + catalogName); + report.recordSuccess(EntityType.GRANT, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; int totalSyncsToComplete = totalSyncsToComplete(grantSyncPlan); @@ -883,6 +960,7 @@ private void syncGrants(String catalogName, String catalogRoleName) { catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.GRANT, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -893,6 +971,7 @@ private void syncGrants(String catalogName, String catalogRoleName) { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.GRANT, grant.getType().toString(), e); } } @@ -906,6 +985,7 @@ private void syncGrants(String catalogName, String catalogRoleName) { catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.GRANT, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -916,6 +996,7 @@ private void syncGrants(String catalogName, String catalogRoleName) { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.GRANT, grant.getType().toString(), e); } } @@ -929,6 +1010,7 @@ private void syncGrants(String catalogName, String catalogRoleName) { catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.GRANT, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -939,6 +1021,7 @@ private void syncGrants(String catalogName, String catalogRoleName) { ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.GRANT, grant.getType().toString(), e); } } } @@ -1004,12 +1087,14 @@ public void syncNamespaces( namespaceSynchronizationPlan .entitiesNotModified() .forEach( - namespace -> + namespace -> { clientLogger.info( "No change detected for namespace {} in namespace {} for catalog {}, skipping.", namespace, parentNamespace, - catalogName)); + catalogName); + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.SKIPPED); + }); for (Namespace namespace : namespaceSynchronizationPlan.entitiesToCreate()) { try { @@ -1022,6 +1107,7 @@ public void syncNamespaces( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -1032,6 +1118,7 @@ public void syncNamespaces( ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.NAMESPACE, namespace.toString(), e); } } @@ -1056,6 +1143,7 @@ public void syncNamespaces( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.SKIPPED); continue; } } @@ -1069,6 +1157,7 @@ public void syncNamespaces( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.OVERWRITTEN); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -1079,6 +1168,7 @@ public void syncNamespaces( ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.NAMESPACE, namespace.toString(), e); } } @@ -1092,6 +1182,7 @@ public void syncNamespaces( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -1102,6 +1193,7 @@ public void syncNamespaces( ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.NAMESPACE, namespace.toString(), e); } } @@ -1168,12 +1260,14 @@ public void syncTables( tableSyncPlan .entitiesToSkip() .forEach( - tableId -> + tableId -> { clientLogger.info( "Skipping table {} in namespace {} in catalog {}.", tableId, namespace, - catalogName)); + catalogName); + report.recordSuccess(EntityType.TABLE, SyncOutcome.SKIPPED); + }); int syncsCompleted = 0; int totalSyncsToComplete = totalSyncsToComplete(tableSyncPlan); @@ -1200,6 +1294,7 @@ public void syncTables( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.TABLE, SyncOutcome.CREATED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -1210,6 +1305,7 @@ public void syncTables( ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.TABLE, tableId.toString(), e); } } @@ -1243,6 +1339,7 @@ public void syncTables( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.TABLE, SyncOutcome.OVERWRITTEN); } catch (MetadataNotModifiedException e) { clientLogger.info( "Table {} in namespace {} in catalog {} with was not modified, not overwriting in target catalog. - {}/{}", @@ -1251,6 +1348,7 @@ public void syncTables( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.TABLE, SyncOutcome.SKIPPED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.error( @@ -1261,6 +1359,7 @@ public void syncTables( ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.TABLE, tableId.toString(), e); } } @@ -1274,6 +1373,7 @@ public void syncTables( catalogName, ++syncsCompleted, totalSyncsToComplete); + report.recordSuccess(EntityType.TABLE, SyncOutcome.REMOVED); } catch (Exception e) { if (haltOnFailure) throw e; clientLogger.info( @@ -1284,6 +1384,7 @@ public void syncTables( ++syncsCompleted, totalSyncsToComplete, e); + report.recordFailure(EntityType.TABLE, table.toString(), e); } } } diff --git a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/EntityType.java b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/EntityType.java new file mode 100644 index 00000000..e040d6ff --- /dev/null +++ b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/EntityType.java @@ -0,0 +1,32 @@ +/* + * 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.planning.plan; + +/** The category of entity a {@link SynchronizationReport} outcome is recorded against. */ +public enum EntityType { + PRINCIPAL, + PRINCIPAL_ROLE, + PRINCIPAL_ROLE_ASSIGNMENT, + CATALOG, + CATALOG_ROLE, + CATALOG_ROLE_ASSIGNMENT, + GRANT, + NAMESPACE, + TABLE +} diff --git a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SyncOutcome.java b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SyncOutcome.java new file mode 100644 index 00000000..8437c944 --- /dev/null +++ b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SyncOutcome.java @@ -0,0 +1,28 @@ +/* + * 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.planning.plan; + +/** The outcome of an individual entity sync attempt, recorded in a {@link SynchronizationReport}. */ +public enum SyncOutcome { + CREATED, + OVERWRITTEN, + REMOVED, + SKIPPED, + FAILED +} diff --git a/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReport.java b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReport.java new file mode 100644 index 00000000..1bbf0b30 --- /dev/null +++ b/polaris-synchronizer/api/src/main/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReport.java @@ -0,0 +1,140 @@ +/* + * 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.planning.plan; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +/** + * Accumulates the outcome of every individual entity sync attempt performed during a run, so that + * a single consolidated summary can be printed at the end instead of relying on interleaved logs. + */ +public class SynchronizationReport { + + private record Failure(EntityType type, String identifier, String message) {} + + private final Map> counts; + + private final List failures; + + public SynchronizationReport() { + this.counts = new EnumMap<>(EntityType.class); + for (EntityType type : EntityType.values()) { + Map outcomeCounts = new EnumMap<>(SyncOutcome.class); + for (SyncOutcome outcome : SyncOutcome.values()) { + outcomeCounts.put(outcome, 0); + } + this.counts.put(type, outcomeCounts); + } + this.failures = new ArrayList<>(); + } + + /** + * Records that an entity was successfully synced. + * + * @param type the category of entity that was synced + * @param outcome the outcome of the sync; must not be {@link SyncOutcome#FAILED} - use {@link + * #recordFailure(EntityType, String, Exception)} for failures + */ + public void recordSuccess(EntityType type, SyncOutcome outcome) { + if (outcome == SyncOutcome.FAILED) { + throw new IllegalArgumentException("Use recordFailure() to record a failed sync."); + } + Map outcomeCounts = counts.get(type); + outcomeCounts.put(outcome, outcomeCounts.get(outcome) + 1); + } + + /** + * Records that an entity failed to sync. + * + * @param type the category of entity that failed to sync + * @param identifier a human-readable identifier for the entity, e.g. its name + * @param cause the exception that caused the failure + */ + public void recordFailure(EntityType type, String identifier, Exception cause) { + Map outcomeCounts = counts.get(type); + outcomeCounts.put(SyncOutcome.FAILED, outcomeCounts.get(SyncOutcome.FAILED) + 1); + failures.add(new Failure(type, identifier, cause.getMessage())); + } + + /** Returns true if any entity failed to sync. */ + public boolean hasFailures() { + return !failures.isEmpty(); + } + + private static String displayName(EntityType type) { + return switch (type) { + case PRINCIPAL -> "Principal"; + case PRINCIPAL_ROLE -> "PrincipalRole"; + case PRINCIPAL_ROLE_ASSIGNMENT -> "PrincipalRoleAssignment"; + case CATALOG -> "Catalog"; + case CATALOG_ROLE -> "CatalogRole"; + case CATALOG_ROLE_ASSIGNMENT -> "CatalogRoleAssignment"; + case GRANT -> "Grant"; + case NAMESPACE -> "Namespace"; + case TABLE -> "Table"; + }; + } + + /** Renders the accumulated counts and failures as a human-readable text block. */ + public String render() { + StringBuilder sb = new StringBuilder(); + sb.append("=== Synchronization Report ===\n"); + + for (EntityType type : EntityType.values()) { + Map outcomeCounts = counts.get(type); + int total = + outcomeCounts.get(SyncOutcome.CREATED) + + outcomeCounts.get(SyncOutcome.OVERWRITTEN) + + outcomeCounts.get(SyncOutcome.REMOVED) + + outcomeCounts.get(SyncOutcome.SKIPPED) + + outcomeCounts.get(SyncOutcome.FAILED); + + if (total == 0) { + continue; + } + + sb.append( + String.format( + "%-24s%d created, %d overwritten, %d removed, %d skipped, %d failed%n", + displayName(type) + ":", + outcomeCounts.get(SyncOutcome.CREATED), + outcomeCounts.get(SyncOutcome.OVERWRITTEN), + outcomeCounts.get(SyncOutcome.REMOVED), + outcomeCounts.get(SyncOutcome.SKIPPED), + outcomeCounts.get(SyncOutcome.FAILED))); + } + + if (hasFailures()) { + sb.append("\nFailures:\n"); + for (Failure failure : failures) { + sb.append( + String.format( + " - %s '%s': %s%n", + displayName(failure.type()), failure.identifier(), failure.message())); + } + } + + sb.append("==============================="); + + return sb.toString(); + } +} diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReportTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReportTest.java new file mode 100644 index 00000000..20533212 --- /dev/null +++ b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/planning/plan/SynchronizationReportTest.java @@ -0,0 +1,112 @@ +/* + * 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.planning.plan; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class SynchronizationReportTest { + + @Test + public void recordSuccessIncrementsOnlyTargetedCell() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordSuccess(EntityType.PRINCIPAL, SyncOutcome.CREATED); + + String rendered = report.render(); + + Assertions.assertTrue(rendered.contains("Principal:")); + Assertions.assertTrue(rendered.contains("1 created, 0 overwritten, 0 removed, 0 skipped, 0 failed")); + Assertions.assertFalse(rendered.contains("Catalog:")); + } + + @Test + public void recordFailureIncrementsFailedCountAndCapturesIdentifierAndMessage() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordFailure(EntityType.TABLE, "db.orders", new RuntimeException("connection refused")); + + String rendered = report.render(); + + Assertions.assertTrue(rendered.contains("Table:")); + Assertions.assertTrue(rendered.contains("0 created, 0 overwritten, 0 removed, 0 skipped, 1 failed")); + Assertions.assertTrue(rendered.contains("Table 'db.orders': connection refused")); + } + + @Test + public void hasFailuresIsFalseForAnAllSuccessReport() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordSuccess(EntityType.CATALOG, SyncOutcome.CREATED); + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.REMOVED); + + Assertions.assertFalse(report.hasFailures()); + } + + @Test + public void hasFailuresIsTrueOnceAnyFailureIsRecorded() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordSuccess(EntityType.CATALOG, SyncOutcome.CREATED); + report.recordFailure(EntityType.GRANT, "TABLE", new RuntimeException("boom")); + + Assertions.assertTrue(report.hasFailures()); + } + + @Test + public void renderOmitsAllZeroEntityTypesAndFailuresSectionWhenEmpty() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordSuccess(EntityType.PRINCIPAL_ROLE, SyncOutcome.CREATED); + + String rendered = report.render(); + + Assertions.assertTrue(rendered.contains("PrincipalRole:")); + Assertions.assertFalse(rendered.contains("Grant:")); + Assertions.assertFalse(rendered.contains("Namespace:")); + Assertions.assertFalse(rendered.contains("Failures:")); + } + + @Test + public void renderIncludesEachFailureWhenMultipleFailuresExist() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordFailure(EntityType.PRINCIPAL, "alice", new RuntimeException("timeout")); + report.recordFailure(EntityType.TABLE, "db.returns", new RuntimeException("not found")); + + String rendered = report.render(); + + Assertions.assertTrue(rendered.contains("Failures:")); + Assertions.assertTrue(rendered.contains("Principal 'alice': timeout")); + Assertions.assertTrue(rendered.contains("Table 'db.returns': not found")); + } + + @Test + public void recordSuccessWithSkippedIncrementsSkippedCountAndDoesNotCountAsFailure() { + SynchronizationReport report = new SynchronizationReport(); + + report.recordSuccess(EntityType.NAMESPACE, SyncOutcome.SKIPPED); + + String rendered = report.render(); + + Assertions.assertTrue(rendered.contains("Namespace:")); + Assertions.assertTrue(rendered.contains("0 created, 0 overwritten, 0 removed, 1 skipped, 0 failed")); + Assertions.assertFalse(report.hasFailures()); + } +} 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 851d66f4..dd3cf04a 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 @@ -26,6 +26,7 @@ import org.apache.polaris.tools.sync.polaris.planning.ModificationAwarePlanner; 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.SynchronizationReport; import org.apache.polaris.tools.sync.polaris.service.PolarisService; import org.apache.polaris.tools.sync.polaris.service.impl.PolarisApiService; import org.slf4j.Logger; @@ -118,6 +119,13 @@ public class SyncPolarisCommand implements Callable { ) private BaseStrategyPlanner.Strategy strategy; + @CommandLine.Option( + names = {"--fail-on-error"}, + description = "Exit with a non-zero status if any entity failed to synchronize, checked after the " + + "synchronization run completes. Unlike --halt-on-failure, this does not stop the run early." + ) + private boolean failOnError; + @Override public Integer call() throws Exception { SynchronizationPlanner planner = SynchronizationPlanner.builder(new BaseStrategyPlanner(strategy)) @@ -126,6 +134,8 @@ public Integer call() throws Exception { .wrapBy(AccessControlAwarePlanner::new) .build(); + SynchronizationReport report = new SynchronizationReport(); + // auto generate omnipotent principals with write access on the target, read only access on source sourceProperties.put(PolarisApiService.ICEBERG_WRITE_ACCESS_PROPERTY, Boolean.toString(false)); targetProperties.put(PolarisApiService.ICEBERG_WRITE_ACCESS_PROPERTY, Boolean.toString(true)); @@ -145,7 +155,8 @@ public Integer call() throws Exception { source, target, etagManager, - diffOnly); + diffOnly, + report); synchronizer.syncPrincipalRoles(); if (shouldSyncPrincipals) { consoleLog.warn("Principal migration will reset credentials on the target Polaris instance. " + @@ -155,6 +166,8 @@ public Integer call() throws Exception { synchronizer.syncCatalogs(); } - return 0; + consoleLog.info(report.render()); + + return (failOnError && report.hasFailures()) ? 1 : 0; } } From bdc7670af8cd6c466bd6a75267fd45f2253283b7 Mon Sep 17 00:00:00 2001 From: Sai Dixith Date: Mon, 27 Jul 2026 11:17:38 +0530 Subject: [PATCH 2/2] fix: resolve merge conflict in PolarisSynchronizer constructor The merge of main into this branch incorrectly combined the SynchronizationReport and skipIcebergContent constructor parameter additions from two separately-developed features, leaving a malformed duplicated parameter list that failed to compile. Merge the two parameters into a single correct signature and update all call sites. --- .../polaris/tools/sync/polaris/PolarisSynchronizer.java | 2 +- .../polaris/PolarisSynchronizerSkipIcebergContentTest.java | 4 ++++ .../apache/polaris/tools/sync/polaris/SyncPolarisCommand.java | 2 +- 3 files changed, 6 insertions(+), 2 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 f72948d1..f7f96f80 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 @@ -77,7 +77,7 @@ public PolarisSynchronizer( PolarisService target, ETagManager etagManager, boolean diffOnly, - SynchronizationReport report) { + SynchronizationReport report, boolean skipIcebergContent) { this.clientLogger = clientLogger == null ? LoggerFactory.getLogger(PolarisSynchronizer.class) : clientLogger; diff --git a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizerSkipIcebergContentTest.java b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizerSkipIcebergContentTest.java index e9e0a2c3..34cc3546 100644 --- a/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizerSkipIcebergContentTest.java +++ b/polaris-synchronizer/api/src/test/java/org/apache/polaris/tools/sync/polaris/PolarisSynchronizerSkipIcebergContentTest.java @@ -37,6 +37,7 @@ import org.apache.polaris.tools.sync.polaris.catalog.NoOpETagManager; import org.apache.polaris.tools.sync.polaris.planning.NoOpSyncPlanner; import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationPlan; +import org.apache.polaris.tools.sync.polaris.planning.plan.SynchronizationReport; import org.apache.polaris.tools.sync.polaris.service.IcebergCatalogService; import org.apache.polaris.tools.sync.polaris.service.PolarisService; import org.junit.jupiter.api.Assertions; @@ -299,6 +300,7 @@ public void testSkipIcebergContentSkipsIcebergSyncButStillSyncsCatalogRoles() { target, new NoOpETagManager(), false, + new SynchronizationReport(), true); synchronizer.syncCatalogs(); @@ -323,6 +325,7 @@ public void testIcebergContentSyncedWhenNotSkipped() { target, new NoOpETagManager(), false, + new SynchronizationReport(), false); synchronizer.syncCatalogs(); @@ -354,6 +357,7 @@ public void testTableScopedGrantsStillAttemptedWhenIcebergContentSkipped() { target, new NoOpETagManager(), false, + new SynchronizationReport(), true); synchronizer.syncCatalogs(); 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 b5c26569..e761a86c 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 @@ -163,7 +163,7 @@ public Integer call() throws Exception { target, etagManager, diffOnly, - report); + report, skipIcebergContent); synchronizer.syncPrincipalRoles(); if (shouldSyncPrincipals) {