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/10/08 00:06:17 UTC

(polaris) branch main updated: Stop H2 init script from overwriting the recorded schema version (#5704)

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 1b13fd2077 Stop H2 init script from overwriting the recorded schema version (#5704)
1b13fd2077 is described below

commit 1b13fd207766456e81832d8d28cc223fc1d405ad
Author: Prithvi S <pr...@gmail.com>
AuthorDate: Thu Oct 8 05:36:12 2026 +0530

    Stop H2 init script from overwriting the recorded schema version (#5704)
    
    The H2 schema.sql used MERGE INTO version ... KEY, which unconditionally rewrote the schema version on every run. Running bootstrap against an older, unmigrated H2 database therefore stamped it with the current version, defeating validateSchemaCompatibility() and letting the server run against an unmigrated schema. The PostgreSQL and CockroachDB scripts already avoid this with ON CONFLICT DO NOTHING; switch H2 to an equivalent insert-if-absent so the version is seeded only when missing.
---
 CHANGELOG.md                                       |  5 +++
 .../src/main/resources/h2/schema.sql               | 10 ++++--
 .../jdbc/DatasourceOperationsSchemaTest.java       | 42 ++++++++++++++++++++++
 3 files changed, 54 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e05280ae8..3f88c7c889 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -109,6 +109,11 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
 
 ### Fixes
 
+- The H2 relational JDBC init script no longer overwrites the recorded schema version. It now seeds
+  the `version` table only when absent, matching the `ON CONFLICT DO NOTHING` behavior of the
+  PostgreSQL and CockroachDB scripts. Previously, running bootstrap against an older, unmigrated H2
+  database silently stamped it with the current version, which defeated the schema-compatibility
+  check and let the server run against an unmigrated schema.
 - NoSQL maintenance: realm purging now honors its own state filter consistently. `purgeRealms`
   keeps only realms in `PURGING`/`PURGED` state (and logs the rest as "will therefore not be
   purged"), but the direct `backend.deleteRealms(...)` call and the subsequent realm-status update
diff --git a/persistence/relational-jdbc/src/main/resources/h2/schema.sql b/persistence/relational-jdbc/src/main/resources/h2/schema.sql
index 4a203f2641..bae94c0838 100644
--- a/persistence/relational-jdbc/src/main/resources/h2/schema.sql
+++ b/persistence/relational-jdbc/src/main/resources/h2/schema.sql
@@ -22,9 +22,13 @@ CREATE TABLE IF NOT EXISTS version (
     version_value INTEGER NOT NULL
 );
 
-MERGE INTO version (version_key, version_value)
-    KEY (version_key)
-    VALUES ('version', 6);
+-- Seed the schema version only when it is absent. Unlike MERGE ... KEY, this never overwrites an
+-- existing value, matching the ON CONFLICT DO NOTHING semantics used by the PostgreSQL and
+-- CockroachDB scripts. Overwriting would let bootstrap stamp an older, unmigrated database as the
+-- current version and defeat the schema-compatibility check.
+INSERT INTO version (version_key, version_value)
+    SELECT 'version', 6
+    WHERE NOT EXISTS (SELECT 1 FROM version WHERE version_key = 'version');
 
 -- H2 supports COMMENT, but some modes may ignore it
 COMMENT ON TABLE version IS 'JDBC schema version; updated manually as part of upgrade procedures.';
diff --git a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperationsSchemaTest.java b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperationsSchemaTest.java
index a5952f2495..c92854e0e9 100644
--- a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperationsSchemaTest.java
+++ b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperationsSchemaTest.java
@@ -70,6 +70,18 @@ class DatasourceOperationsSchemaTest {
     }
   }
 
+  /** The schema version currently recorded in the VERSION table. */
+  private static Integer readSchemaVersion(DataSource dataSource) throws SQLException {
+    try (Connection connection = dataSource.getConnection();
+        PreparedStatement statement =
+            connection.prepareStatement(
+                "SELECT version_value FROM VERSION WHERE version_key = 'version'")) {
+      try (ResultSet resultSet = statement.executeQuery()) {
+        return resultSet.next() ? resultSet.getInt(1) : null;
+      }
+    }
+  }
+
   @Test
   void bootstrapsIntoDatasourceConfiguredSchema() throws SQLException {
     DataSource dataSource = createDataSource("custom_polaris");
@@ -132,6 +144,36 @@ class DatasourceOperationsSchemaTest {
         .hasMessageContaining("expected " + DatasourceOperations.CURRENT_SCHEMA_VERSION);
   }
 
+  @Test
+  void rerunningInitScriptDoesNotOverwriteExistingVersion() throws SQLException {
+    DataSource dataSource = createDataSource(null);
+    DatasourceOperations ops =
+        new DatasourceOperations(
+            dataSource, SimpleRelationalJdbcConfiguration.forDatabaseType(DatabaseType.H2));
+    ops.executeScript(openSchemaScript());
+
+    // Simulate an older, unmigrated database.
+    int staleVersion = DatasourceOperations.CURRENT_SCHEMA_VERSION - 1;
+    try (Connection connection = dataSource.getConnection();
+        PreparedStatement statement =
+            connection.prepareStatement(
+                "UPDATE VERSION SET version_value = ? WHERE version_key = 'version'")) {
+      statement.setInt(1, staleVersion);
+      statement.executeUpdate();
+    }
+
+    // Re-running the init script is what a bootstrap invocation does. It must not stamp the
+    // database with the current version, otherwise it would mask an unmigrated schema and defeat
+    // the compatibility check.
+    ops.executeScript(openSchemaScript());
+
+    assertThat(readSchemaVersion(dataSource)).isEqualTo(staleVersion);
+    assertThatThrownBy(ops::validateSchemaCompatibility)
+        .isInstanceOf(IllegalStateException.class)
+        .hasMessageContaining("Incompatible JDBC schema version " + staleVersion)
+        .hasMessageContaining("expected " + DatasourceOperations.CURRENT_SCHEMA_VERSION);
+  }
+
   @Test
   void validateSchemaCompatibility_throwsOnNewerVersion() throws SQLException {
     DataSource dataSource = createDataSource(null);