You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by di...@apache.org on 2026/06/12 22:10:15 UTC

(polaris) branch main updated: Run JDBC schema validation tests on PostgreSQL (#4602)

This is an automated email from the ASF dual-hosted git repository.

dimas-b pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new b1300e4c73 Run JDBC schema validation tests on PostgreSQL (#4602)
b1300e4c73 is described below

commit b1300e4c73ec37d1ae5105e97dcc83166dc7d3d4
Author: venkateshwaran shanmugham <ve...@gmail.com>
AuthorDate: Sat Jun 13 03:40:10 2026 +0530

    Run JDBC schema validation tests on PostgreSQL (#4602)
    
    * Run JDBC schema tests on PostgreSQL
    
    * JDBC: Remove targeted H2 schema validation test
    
    * Use semantic equality for JSON-backed persistence fields
---
 ...ithJdbcBasePersistenceImplPostgresSchemaIT.java | 114 +++++++++++++++++++++
 ...nagerWithJdbcBasePersistenceImplSchemaTest.java |  45 --------
 ...toreManagerWithJdbcBasePersistenceImplTest.java |  67 ++++++------
 .../relational/jdbc/ResultSetIteratorTest.java     |  25 ++++-
 .../{H2SchemaVersions.java => SchemaVersions.java} |  45 ++++----
 .../jdbc/SimpleRelationalJdbcConfiguration.java    |  59 +++++++++++
 .../polaris/core/entity/PolarisBaseEntity.java     |   8 +-
 .../core/policy/PolarisPolicyMappingRecord.java    |   5 +-
 .../persistence/PolarisObjectMapperUtilTest.java   |  22 ++++
 .../BasePolarisMetaStoreManagerTest.java           |   2 +-
 10 files changed, 282 insertions(+), 110 deletions(-)

diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplPostgresSchemaIT.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplPostgresSchemaIT.java
new file mode 100644
index 0000000000..23500a019b
--- /dev/null
+++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplPostgresSchemaIT.java
@@ -0,0 +1,114 @@
+/*
+ * 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.persistence.relational.jdbc;
+
+import static org.apache.polaris.containerspec.ContainerSpecHelper.containerSpecHelper;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.stream.Stream;
+import javax.sql.DataSource;
+import org.apache.polaris.test.commons.PostgresRelationalJdbcLifeCycleManagement;
+import org.junit.jupiter.params.Parameter;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.postgresql.ds.PGSimpleDataSource;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.postgresql.PostgreSQLContainer;
+
+/**
+ * Runs {@link org.apache.polaris.core.persistence.BasePolarisMetaStoreManagerTest} integration
+ * tests against every PostgreSQL schema version on the classpath.
+ */
+@Testcontainers
+@ParameterizedClass
+@MethodSource("schemaVersions")
+public class AtomicMetastoreManagerWithJdbcBasePersistenceImplPostgresSchemaIT
+    extends AtomicMetastoreManagerWithJdbcBasePersistenceImplTest {
+
+  @Container
+  private static final PostgreSQLContainer POSTGRES =
+      new PostgreSQLContainer(
+          containerSpecHelper("postgres", PostgresRelationalJdbcLifeCycleManagement.class)
+              .dockerImageName(null)
+              .asCompatibleSubstituteFor("postgres"));
+
+  @Parameter int schemaVersion;
+
+  private DataSource dataSource;
+
+  static Stream<Integer> schemaVersions() {
+    return SchemaVersions.discoverAsStream(DatabaseType.POSTGRES);
+  }
+
+  @Override
+  protected DatabaseType databaseType() {
+    return DatabaseType.POSTGRES;
+  }
+
+  @Override
+  public int schemaVersion() {
+    return schemaVersion;
+  }
+
+  @Override
+  protected DataSource createDataSource() {
+    if (dataSource == null) {
+      dataSource = createPostgresDataSource(schemaVersion());
+    }
+    return dataSource;
+  }
+
+  private static DataSource createPostgresDataSource(int schemaVersion) {
+    String databaseName = "polaris_schema_v" + schemaVersion;
+    createDatabaseIfNotExists(databaseName);
+
+    PGSimpleDataSource postgresDataSource = new PGSimpleDataSource();
+    postgresDataSource.setURL(jdbcUrlForDatabase(databaseName));
+    postgresDataSource.setUser(POSTGRES.getUsername());
+    postgresDataSource.setPassword(POSTGRES.getPassword());
+    return postgresDataSource;
+  }
+
+  private static void createDatabaseIfNotExists(String databaseName) {
+    try (Connection connection =
+            DriverManager.getConnection(
+                POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword());
+        Statement statement = connection.createStatement()) {
+      statement.execute("CREATE DATABASE " + databaseName);
+    } catch (SQLException e) {
+      // 42P04 is PostgreSQL's duplicate_database error; ignore when the schema DB already exists.
+      if (!"42P04".equals(e.getSQLState())) {
+        throw new RuntimeException("Failed to create database " + databaseName, e);
+      }
+    }
+  }
+
+  private static String jdbcUrlForDatabase(String databaseName) {
+    String baseUrl = POSTGRES.getJdbcUrl();
+    int lastSlash = baseUrl.lastIndexOf('/');
+    if (lastSlash < 0) {
+      throw new IllegalStateException("Unexpected JDBC URL: " + baseUrl);
+    }
+    return baseUrl.substring(0, lastSlash + 1) + databaseName;
+  }
+}
diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplSchemaTest.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplSchemaTest.java
deleted file mode 100644
index 58e30de267..0000000000
--- a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplSchemaTest.java
+++ /dev/null
@@ -1,45 +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.persistence.relational.jdbc;
-
-import java.util.stream.Stream;
-import org.junit.jupiter.params.Parameter;
-import org.junit.jupiter.params.ParameterizedClass;
-import org.junit.jupiter.params.provider.MethodSource;
-
-/**
- * Runs {@link org.apache.polaris.core.persistence.BasePolarisMetaStoreManagerTest} integration
- * tests against every H2 schema version on the classpath.
- */
-@ParameterizedClass
-@MethodSource("schemaVersions")
-public class AtomicMetastoreManagerWithJdbcBasePersistenceImplSchemaTest
-    extends AtomicMetastoreManagerWithJdbcBasePersistenceImplTest {
-
-  @Parameter int schemaVersion;
-
-  static Stream<Integer> schemaVersions() {
-    return H2SchemaVersions.discoverAsStream();
-  }
-
-  @Override
-  public int schemaVersion() {
-    return schemaVersion;
-  }
-}
diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java
index 54199d286b..3a78ca39ba 100644
--- a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java
+++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/AtomicMetastoreManagerWithJdbcBasePersistenceImplTest.java
@@ -20,6 +20,7 @@ package org.apache.polaris.persistence.relational.jdbc;
 
 import static org.apache.polaris.core.persistence.PrincipalSecretsGenerator.RANDOM_SECRETS;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.sql.SQLException;
 import java.util.List;
@@ -48,30 +49,49 @@ import org.mockito.Mockito;
 public abstract class AtomicMetastoreManagerWithJdbcBasePersistenceImplTest
     extends BasePolarisMetaStoreManagerTest {
 
-  public DataSource createH2DataSource() {
-    return JdbcConnectionPool.create(
-        String.format("jdbc:h2:file:./build/test_data/polaris/db_%s", schemaVersion()), "sa", "");
+  protected DatabaseType databaseType() {
+    return DatabaseType.H2;
   }
 
   public abstract int schemaVersion();
 
+  protected DataSource createDataSource() {
+    return JdbcConnectionPool.create(
+        String.format(
+            "jdbc:h2:file:./build/test_data/polaris/db_%s_%d",
+            databaseType().getDisplayName(), schemaVersion()),
+        "sa",
+        "");
+  }
+
+  protected InputStream openSchemaScript() {
+    ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
+    String resource =
+        String.format("%s/schema-v%d.sql", databaseType().getDisplayName(), schemaVersion());
+    InputStream scriptStream = classLoader.getResourceAsStream(resource);
+    if (scriptStream == null) {
+      throw new IllegalStateException("Schema resource not found: " + resource);
+    }
+    return scriptStream;
+  }
+
   @Override
   protected PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() {
     PolarisDiagnostics diagServices = new PolarisDefaultDiagServiceImpl();
     DatasourceOperations datasourceOperations;
     try {
       datasourceOperations =
-          new DatasourceOperations(createH2DataSource(), new H2JdbcConfiguration());
-      ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
-      InputStream scriptStream =
-          classLoader.getResourceAsStream(
-              String.format(
-                  "%s/schema-v%s.sql", DatabaseType.H2.getDisplayName(), schemaVersion()));
-      datasourceOperations.executeScript(scriptStream);
-    } catch (SQLException e) {
+          new DatasourceOperations(
+              createDataSource(),
+              SimpleRelationalJdbcConfiguration.forDatabaseType(databaseType()));
+      try (InputStream scriptStream = openSchemaScript()) {
+        datasourceOperations.executeScript(scriptStream);
+      }
+    } catch (SQLException | IOException e) {
       throw new RuntimeException(
           String.format(
-              "Error executing %s script: %s", DatabaseType.H2.getDisplayName(), e.getMessage()),
+              "Error executing %s schema-v%d script: %s",
+              databaseType(), schemaVersion(), e.getMessage()),
           e);
     }
 
@@ -165,27 +185,4 @@ public abstract class AtomicMetastoreManagerWithJdbcBasePersistenceImplTest
       return getPropertiesAsMap().get(PolarisEntityConstants.ENTITY_BASE_LOCATION);
     }
   }
-
-  private static class H2JdbcConfiguration implements RelationalJdbcConfiguration {
-
-    @Override
-    public Optional<Integer> maxRetries() {
-      return Optional.of(2);
-    }
-
-    @Override
-    public Optional<Long> maxDurationInMs() {
-      return Optional.of(100L);
-    }
-
-    @Override
-    public Optional<Long> initialDelayInMs() {
-      return Optional.of(100L);
-    }
-
-    @Override
-    public Optional<String> databaseType() {
-      return Optional.of("h2");
-    }
-  }
 }
diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/ResultSetIteratorTest.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/ResultSetIteratorTest.java
index 36b0d8fdb5..be2897b0eb 100644
--- a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/ResultSetIteratorTest.java
+++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/ResultSetIteratorTest.java
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.when;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.List;
+import java.util.Map;
 import java.util.NoSuchElementException;
 import org.apache.polaris.persistence.relational.jdbc.models.Converter;
 import org.junit.jupiter.api.Test;
@@ -37,10 +38,9 @@ public class ResultSetIteratorTest {
   @Test
   public void testIteration() throws SQLException {
     ResultSet resultSet = mock(ResultSet.class);
-    Converter<String> converter = mock(Converter.class);
+    Converter<String> converter = converterReturning("first", "second");
 
     when(resultSet.next()).thenReturn(true, true, false);
-    when(converter.fromResultSet(resultSet)).thenReturn("first").thenReturn("second");
 
     ResultSetIterator<String> iterator = new ResultSetIterator<>(resultSet, converter);
 
@@ -56,7 +56,7 @@ public class ResultSetIteratorTest {
   @Test
   public void testNextThrowsWhenExhausted() throws SQLException {
     ResultSet resultSet = mock(ResultSet.class);
-    Converter<String> converter = mock(Converter.class);
+    Converter<String> converter = converterReturning();
 
     when(resultSet.next()).thenReturn(false);
 
@@ -69,10 +69,9 @@ public class ResultSetIteratorTest {
   @Test
   public void testToStream() throws SQLException {
     ResultSet resultSet = mock(ResultSet.class);
-    Converter<String> converter = mock(Converter.class);
+    Converter<String> converter = converterReturning("first", "second");
 
     when(resultSet.next()).thenReturn(true, true, false);
-    when(converter.fromResultSet(resultSet)).thenReturn("first").thenReturn("second");
 
     ResultSetIterator<String> iterator = new ResultSetIterator<>(resultSet, converter);
 
@@ -80,4 +79,20 @@ public class ResultSetIteratorTest {
 
     assertEquals(List.of("first", "second"), results);
   }
+
+  private static Converter<String> converterReturning(String... values) {
+    return new Converter<String>() {
+      private int index = 0;
+
+      @Override
+      public String fromResultSet(ResultSet rs) {
+        return values[index++];
+      }
+
+      @Override
+      public Map<String, Object> toMap(DatabaseType databaseType) {
+        throw new UnsupportedOperationException("read-only converter stub");
+      }
+    };
+  }
 }
diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/H2SchemaVersions.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/SchemaVersions.java
similarity index 52%
rename from persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/H2SchemaVersions.java
rename to persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/SchemaVersions.java
index 6103b8dda1..ddebc385e6 100644
--- a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/H2SchemaVersions.java
+++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/SchemaVersions.java
@@ -24,27 +24,27 @@ import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.stream.Stream;
 
-/** Discovers H2 schema versions available on the test classpath. */
-final class H2SchemaVersions {
+/** Discovers JDBC schema versions available on the test classpath for a {@link DatabaseType}. */
+final class SchemaVersions {
 
-  private H2SchemaVersions() {}
+  private SchemaVersions() {}
 
   /**
-   * Returns sorted schema versions for which {@code h2/schema-vN.sql} exists on the classpath.
+   * Returns sorted schema versions for which {@code <database>/schema-vN.sql} exists on the
+   * classpath.
    *
-   * <p>Every version from {@code 0} through {@link DatabaseType#H2}{@linkplain
-   * DatabaseType#getLatestSchemaVersion() latest} must be present. {@code schema-v0.sql} lives in
-   * test resources to cover legacy bootstrap behavior; newer H2 schema files live in main
-   * resources.
+   * <p>Every version from the type's first schema version through {@link
+   * DatabaseType#getLatestSchemaVersion()} must be present. H2 {@code schema-v0.sql} lives in test
+   * resources to cover legacy bootstrap behavior; production schema files live in main resources.
    */
-  static SortedSet<Integer> discover() {
-    ClassLoader classLoader = H2SchemaVersions.class.getClassLoader();
-    int latestVersion = DatabaseType.H2.getLatestSchemaVersion();
+  static SortedSet<Integer> discover(DatabaseType databaseType) {
+    ClassLoader classLoader = SchemaVersions.class.getClassLoader();
+    int firstVersion = firstSchemaVersion(databaseType);
+    int latestVersion = databaseType.getLatestSchemaVersion();
     SortedSet<Integer> versions = new TreeSet<>();
     List<Integer> missingVersions = new ArrayList<>();
-    for (int version = 0; version <= latestVersion; version++) {
-      String resource =
-          String.format("%s/schema-v%d.sql", DatabaseType.H2.getDisplayName(), version);
+    for (int version = firstVersion; version <= latestVersion; version++) {
+      String resource = String.format("%s/schema-v%d.sql", databaseType.getDisplayName(), version);
       if (classLoader.getResource(resource) == null) {
         missingVersions.add(version);
       } else {
@@ -54,20 +54,29 @@ final class H2SchemaVersions {
     if (!missingVersions.isEmpty()) {
       throw new IllegalStateException(
           String.format(
-              "Missing H2 schema resource(s) %s for version(s) %s (expected versions 0-%d)",
+              "Missing %s schema resource(s) %s for version(s) %s (expected versions %d-%d)",
+              databaseType,
               missingVersions.stream()
                   .map(
                       version ->
                           String.format(
-                              "%s/schema-v%d.sql", DatabaseType.H2.getDisplayName(), version))
+                              "%s/schema-v%d.sql", databaseType.getDisplayName(), version))
                   .toList(),
               missingVersions,
+              firstVersion,
               latestVersion));
     }
     return versions;
   }
 
-  static Stream<Integer> discoverAsStream() {
-    return discover().stream();
+  static Stream<Integer> discoverAsStream(DatabaseType databaseType) {
+    return discover(databaseType).stream();
+  }
+
+  private static int firstSchemaVersion(DatabaseType databaseType) {
+    return switch (databaseType) {
+      case H2 -> 0;
+      case POSTGRES, COCKROACHDB -> 1;
+    };
   }
 }
diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/SimpleRelationalJdbcConfiguration.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/SimpleRelationalJdbcConfiguration.java
new file mode 100644
index 0000000000..bbc28c12d8
--- /dev/null
+++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/SimpleRelationalJdbcConfiguration.java
@@ -0,0 +1,59 @@
+/*
+ * 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.persistence.relational.jdbc;
+
+import java.util.Optional;
+
+/** Minimal {@link RelationalJdbcConfiguration} for JDBC persistence integration tests. */
+final class SimpleRelationalJdbcConfiguration implements RelationalJdbcConfiguration {
+
+  private final String databaseType;
+
+  private SimpleRelationalJdbcConfiguration(String databaseType) {
+    this.databaseType = databaseType;
+  }
+
+  static SimpleRelationalJdbcConfiguration forDatabaseType(DatabaseType databaseType) {
+    return switch (databaseType) {
+      case H2 -> new SimpleRelationalJdbcConfiguration("h2");
+      case POSTGRES -> new SimpleRelationalJdbcConfiguration("postgresql");
+      case COCKROACHDB -> new SimpleRelationalJdbcConfiguration("cockroachdb");
+    };
+  }
+
+  @Override
+  public Optional<Integer> maxRetries() {
+    return Optional.of(2);
+  }
+
+  @Override
+  public Optional<Long> maxDurationInMs() {
+    return Optional.of(100L);
+  }
+
+  @Override
+  public Optional<Long> initialDelayInMs() {
+    return Optional.of(100L);
+  }
+
+  @Override
+  public Optional<String> databaseType() {
+    return Optional.of(databaseType);
+  }
+}
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisBaseEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisBaseEntity.java
index a7c59ade77..1c42de1d42 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisBaseEntity.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/PolarisBaseEntity.java
@@ -343,8 +343,8 @@ public class PolarisBaseEntity extends PolarisEntityCore {
         && toPurgeTimestamp == that.toPurgeTimestamp
         && lastUpdateTimestamp == that.lastUpdateTimestamp
         && grantRecordsVersion == that.grantRecordsVersion
-        && Objects.equals(properties, that.properties)
-        && Objects.equals(internalProperties, that.internalProperties);
+        && Objects.equals(getPropertiesAsMap(), that.getPropertiesAsMap())
+        && Objects.equals(getInternalPropertiesAsMap(), that.getInternalPropertiesAsMap());
   }
 
   @Override
@@ -362,8 +362,8 @@ public class PolarisBaseEntity extends PolarisEntityCore {
         purgeTimestamp,
         toPurgeTimestamp,
         lastUpdateTimestamp,
-        properties,
-        internalProperties,
+        getPropertiesAsMap(),
+        getInternalPropertiesAsMap(),
         grantRecordsVersion);
   }
 
diff --git a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java
index 4b13896d22..5112895525 100644
--- a/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java
+++ b/polaris-core/src/main/java/org/apache/polaris/core/policy/PolarisPolicyMappingRecord.java
@@ -205,11 +205,12 @@ public class PolarisPolicyMappingRecord {
         && policyCatalogId == that.policyCatalogId
         && policyId == that.policyId
         && policyTypeCode == that.policyTypeCode
-        && Objects.equals(parameters, that.parameters);
+        && Objects.equals(getParametersAsMap(), that.getParametersAsMap());
   }
 
   @Override
   public int hashCode() {
-    return java.util.Objects.hash(targetId, policyId, policyCatalogId, policyTypeCode, parameters);
+    return Objects.hash(
+        targetCatalogId, targetId, policyCatalogId, policyId, policyTypeCode, getParametersAsMap());
   }
 }
diff --git a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java
index 62cb51319a..93af87ea13 100644
--- a/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java
+++ b/polaris-core/src/test/java/org/apache/polaris/core/persistence/PolarisObjectMapperUtilTest.java
@@ -134,4 +134,26 @@ class PolarisObjectMapperUtilTest {
         PolarisObjectMapperUtil.parseTaskState(entity);
     Assertions.assertThat(state).isNull();
   }
+
+  @Test
+  void entityEqualsIgnoresJsonFormatting() {
+    PolarisBaseEntity compact =
+        new PolarisBaseEntity.Builder()
+            .catalogId(1L)
+            .id(2L)
+            .typeCode(PolarisEntityType.TASK.getCode())
+            .subTypeCode(PolarisEntitySubType.NULL_SUBTYPE.getCode())
+            .parentId(0L)
+            .name("entity")
+            .properties("{\"key\":\"value\"}")
+            .internalProperties("{\"internal\":true}")
+            .build();
+    PolarisBaseEntity spaced =
+        new PolarisBaseEntity.Builder(compact)
+            .properties("{\"key\": \"value\"}")
+            .internalProperties("{\"internal\": true}")
+            .build();
+
+    Assertions.assertThat(compact).isEqualTo(spaced);
+  }
 }
diff --git a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
index 5bda228ab1..fb35ea1c67 100644
--- a/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
+++ b/polaris-core/src/testFixtures/java/org/apache/polaris/core/persistence/BasePolarisMetaStoreManagerTest.java
@@ -129,7 +129,7 @@ public abstract class BasePolarisMetaStoreManagerTest {
         .isNotNull()
         .hasSize(2)
         .extracting(PolarisEntity::toCore)
-        .containsExactly(PolarisEntity.toCore(task1), PolarisEntity.toCore(task2));
+        .containsExactlyInAnyOrder(PolarisEntity.toCore(task1), PolarisEntity.toCore(task2));
 
     Assertions.assertThat(createdEntities).containsExactlyInAnyOrderElementsOf(listedEntities);
   }