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

singhpk234 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 8f9ff62a99 RELATIONAL-JDBC: Add support for cockroach DB (#3352)
8f9ff62a99 is described below

commit 8f9ff62a993e6fa990677d26a499676a01593291
Author: Prashant Singh <[email protected]>
AuthorDate: Mon Mar 9 11:23:51 2026 -0700

    RELATIONAL-JDBC: Add support for cockroach DB (#3352)
    
    CockroachDB Support:
    - Added COCKROACHDB to DatabaseType enum (maps to "cockroachdb" directory)
    - Created separate schema directory: cockroachdb/schema-v4.sql
    - CockroachDB schema v4 based on PostgreSQL schema v4 (includes all tables)
    - Uses INT4 explicitly for integer columns (required for CockroachDB JDBC 
driver)
    
    Database Type Detection and Validation:
    - Implemented DatabaseType.inferFromConnection() to detect database from 
JDBC metadata
    - If database-type is configured: uses configured type and validates 
against connection
    - If configured type mismatches connection: throws IllegalStateException 
with clear error
    - If no configured type: auto-detects from connection (CockroachDB, 
PostgreSQL, H2)
    - Validation ensures configuration matches actual database
    
    Configuration:
    - New property: polaris.persistence.relational.jdbc.database-type
    - Supported values: "postgresql", "cockroachdb", "h2"
    - If set, configuration is authoritative and validated against connection
    - If not set, auto-detects from JDBC connection metadata
    
    ---------
    
    Co-authored-by: sathesuraj
    Co-authored-by: Prashant Kumar Singh <[email protected]>
---
 .../persistence/relational/jdbc/DatabaseType.java  | 119 +++++++-
 .../relational/jdbc/DatasourceOperations.java      |  15 +-
 .../relational/jdbc/JdbcBootstrapUtils.java        |  26 +-
 .../jdbc/JdbcMetaStoreManagerFactory.java          |   1 +
 .../jdbc/RelationalJdbcConfiguration.java          |   6 +
 .../src/main/resources/cockroachdb/schema-v4.sql   | 308 +++++++++++++++++++++
 ...toreManagerWithJdbcBasePersistenceImplTest.java |   5 +
 .../relational/jdbc/JdbcBootstrapUtilsTest.java    |  46 ++-
 .../jdbc/MetricsReportPersistenceTest.java         |   5 +
 .../RelationalJdbcIdempotencyStorePostgresIT.java  |   5 +
 .../relational/jdbc/CockroachJdbcAdminProfile.java |  47 ++++
 .../jdbc/CockroachJdbcBootstrapCommandTest.java    |  42 +++
 .../jdbc/CockroachJdbcPurgeCommandTest.java        |  27 +-
 .../cockroach/CockroachApplicationIT.java          |  20 +-
 .../cockroach/CockroachManagementServiceIT.java    |  20 +-
 .../cockroach/CockroachPolicyServiceIT.java        |  20 +-
 .../cockroach/CockroachRestCatalogIT.java          |  20 +-
 .../relational/cockroach/CockroachViewFileIT.java  |  20 +-
 runtime/test-common/build.gradle.kts               |   1 +
 ...CockroachRelationalJdbcLifeCycleManagement.java |  93 +++++++
 .../commons/CockroachRelationalJdbcProfile.java    |  29 +-
 .../test/commons/Dockerfile-cockroachdb-version    |  14 +-
 ...smallrye-polaris_persistence_relational_jdbc.md |   1 +
 23 files changed, 771 insertions(+), 119 deletions(-)

diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java
 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java
index 71a33db211..cc104e1f39 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java
+++ 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatabaseType.java
@@ -19,10 +19,21 @@
 package org.apache.polaris.persistence.relational.jdbc;
 
 import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.SQLException;
 import java.util.Locale;
 
+/**
+ * Database types supported by Polaris relational JDBC persistence.
+ *
+ * <p>CockroachDB uses the PostgreSQL wire protocol but has its own schema 
resources in the
+ * "cockroachdb" directory. While CockroachDB is PostgreSQL-compatible, having 
separate schemas
+ * allows for CockroachDB-specific optimizations and avoids experimental ALTER 
operations that
+ * CockroachDB doesn't fully support.
+ */
 public enum DatabaseType {
   POSTGRES("postgres"),
+  COCKROACHDB("cockroachdb"),
   H2("h2");
 
   private final String displayName; // Store the user-friendly name
@@ -36,28 +47,128 @@ public enum DatabaseType {
     return displayName;
   }
 
+  /**
+   * Returns the latest schema version available for this database type. This 
is used as the default
+   * schema version for new installations.
+   */
+  public int getLatestSchemaVersion() {
+    return switch (this) {
+      case POSTGRES -> 4; // PostgreSQL has schemas v1, v2, v3, v4
+      case COCKROACHDB -> 4; // CockroachDB schema version kept in sync with 
PostgreSQL
+      case H2 -> 4; // H2 uses same schemas as PostgreSQL
+    };
+  }
+
   public static DatabaseType fromDisplayName(String displayName) {
     return switch (displayName.toLowerCase(Locale.ROOT)) {
       case "h2" -> DatabaseType.H2;
       case "postgresql" -> DatabaseType.POSTGRES;
+      case "cockroachdb" -> DatabaseType.COCKROACHDB;
       default -> throw new IllegalStateException("Unsupported DatabaseType: '" 
+ displayName + "'");
     };
   }
 
+  /**
+   * Determines the database type from JDBC connection metadata and validates 
against configured
+   * type.
+   *
+   * <p>Logic:
+   *
+   * <ul>
+   *   <li>If configuredType is provided: uses it as the authoritative type 
and validates it matches
+   *       what the connection reports. Throws an exception if there's a 
mismatch.
+   *   <li>If configuredType is null: infers the type from connection metadata.
+   * </ul>
+   *
+   * @param connection JDBC connection to inspect
+   * @param configuredType Explicitly configured database type (may be null)
+   * @return The database type (configured if provided, otherwise inferred)
+   * @throws IllegalStateException if configured type doesn't match connection 
metadata
+   */
+  public static DatabaseType inferFromConnection(
+      Connection connection, DatabaseType configuredType) {
+    try {
+      var metaData = connection.getMetaData();
+      String productName = 
metaData.getDatabaseProductName().toLowerCase(Locale.ROOT);
+
+      // Infer database type from connection metadata
+      DatabaseType inferredType = null;
+
+      if (productName.contains("cockroach")) {
+        inferredType = DatabaseType.COCKROACHDB;
+      } else if (productName.contains("postgresql")) {
+        inferredType = DatabaseType.POSTGRES;
+      } else if (productName.contains("h2")) {
+        inferredType = DatabaseType.H2;
+      }
+
+      // If a type was explicitly configured, use it and validate
+      if (configuredType != null) {
+        if (inferredType != null && inferredType != configuredType) {
+          // Special case: CockroachDB uses PostgreSQL JDBC driver and wire 
protocol
+          // So configured=COCKROACHDB with inferred=POSTGRES is valid
+          boolean isValidCockroachConfig =
+              configuredType == DatabaseType.COCKROACHDB && inferredType == 
DatabaseType.POSTGRES;
+
+          if (!isValidCockroachConfig) {
+            throw new IllegalStateException(
+                String.format(
+                    "Configured database type '%s' does not match detected 
type '%s' from connection (product: '%s'). "
+                        + "Please check your 
polaris.persistence.relational.jdbc.database-type configuration.",
+                    configuredType, inferredType, productName));
+          }
+        }
+        // Use configured type (either it matches, it's valid CockroachDB, or 
we couldn't infer)
+        return configuredType;
+      }
+
+      // No configured type - use inferred type
+      if (inferredType != null) {
+        return inferredType;
+      }
+
+      // Couldn't infer and no configured type
+      throw new IllegalStateException(
+          "Cannot infer database type from product name: '"
+              + productName
+              + "'. "
+              + "Please set polaris.persistence.relational.jdbc.database-type 
explicitly.");
+
+    } catch (SQLException e) {
+      // If we can't get metadata, must have a configured type
+      if (configuredType != null) {
+        return configuredType;
+      }
+      throw new IllegalStateException(
+          "Cannot determine database type: unable to read connection 
metadata", e);
+    }
+  }
+
   /**
    * Open an InputStream that contains data from an init script. This stream 
should be closed by the
    * caller.
    */
   public InputStream openInitScriptResource(int schemaVersion) {
-    // Preconditions check is simpler and more direct than a switch default
-    if (schemaVersion <= 0 || schemaVersion > 4) {
-      throw new IllegalArgumentException("Unknown or invalid schema version " 
+ schemaVersion);
+    // Validate schema version is within acceptable range for this database 
type
+    int latestVersion = getLatestSchemaVersion();
+    if (schemaVersion <= 0 || schemaVersion > latestVersion) {
+      throw new IllegalArgumentException(
+          String.format(
+              "Invalid schema version %d for database type %s. Valid range: 
1-%d",
+              schemaVersion, this, latestVersion));
     }
 
     final String resourceName =
         String.format("%s/schema-v%d.sql", this.getDisplayName(), 
schemaVersion);
 
     ClassLoader classLoader = DatasourceOperations.class.getClassLoader();
-    return classLoader.getResourceAsStream(resourceName);
+    InputStream stream = classLoader.getResourceAsStream(resourceName);
+    if (stream == null) {
+      throw new IllegalStateException(
+          String.format(
+              "Schema resource not found: %s (database type: %s, version: %d)",
+              resourceName, this, schemaVersion));
+    }
+    return stream;
   }
 }
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java
 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java
index 5e1b2785d9..4006ef28af 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java
+++ 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/DatasourceOperations.java
@@ -76,8 +76,17 @@ public class DatasourceOperations {
     this.datasource = datasource;
     this.relationalJdbcConfiguration = relationalJdbcConfiguration;
     try (Connection connection = this.datasource.getConnection()) {
-      String productName = connection.getMetaData().getDatabaseProductName();
-      this.databaseType = DatabaseType.fromDisplayName(productName);
+      // Get explicitly configured database type, if any
+      DatabaseType configuredType =
+          relationalJdbcConfiguration
+              .databaseType()
+              .map(DatabaseType::fromDisplayName)
+              .orElse(null);
+
+      // Infer database type from connection, falling back to configured type
+      this.databaseType = DatabaseType.inferFromConnection(connection, 
configuredType);
+
+      LOGGER.info("Detected database type: {}", databaseType);
     }
   }
 
@@ -403,7 +412,7 @@ public class DatasourceOperations {
 
   public boolean isRelationDoesNotExist(SQLException e) {
     return (RELATION_DOES_NOT_EXIST.equals(e.getSQLState())
-            && databaseType == DatabaseType.POSTGRES)
+            && (databaseType == DatabaseType.POSTGRES || databaseType == 
DatabaseType.COCKROACHDB))
         || ((H2_SCHEMA_DOES_NOT_EXIST.equals(e.getSQLState())
                 || H2_TABLE_DOES_NOT_EXIST.equals(e.getSQLState()))
             && databaseType == DatabaseType.H2);
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java
 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java
index 814417d1b8..3e0391ca79 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java
+++ 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtils.java
@@ -30,6 +30,7 @@ public class JdbcBootstrapUtils {
   /**
    * Determines the correct schema version to use for bootstrapping a realm.
    *
+   * @param databaseType The database type being used (determines latest 
available schema version).
    * @param currentSchemaVersion The current version of the database schema.
    * @param requiredSchemaVersion The requested schema version (-1 for 
auto-detection).
    * @param hasAlreadyBootstrappedRealms Flag indicating if any realms already 
exist.
@@ -37,7 +38,12 @@ public class JdbcBootstrapUtils {
    * @throws IllegalStateException if the combination of parameters represents 
an invalid state.
    */
   public static int getRealmBootstrapSchemaVersion(
-      int currentSchemaVersion, int requiredSchemaVersion, boolean 
hasAlreadyBootstrappedRealms) {
+      DatabaseType databaseType,
+      int currentSchemaVersion,
+      int requiredSchemaVersion,
+      boolean hasAlreadyBootstrappedRealms) {
+
+    int latestSchemaVersion = databaseType.getLatestSchemaVersion();
 
     // If versions already match, no change is needed.
     if (currentSchemaVersion == requiredSchemaVersion) {
@@ -52,23 +58,27 @@ public class JdbcBootstrapUtils {
           return 1;
         }
       } else {
-        // A truly fresh start. Default to v3 for auto-detection, otherwise 
use the specified
-        // version.
-        return requiredSchemaVersion == -1 ? 3 : requiredSchemaVersion;
+        // A truly fresh start. Default to latest version for this database 
type for auto-detection,
+        // otherwise use the specified version.
+        return requiredSchemaVersion == -1 ? latestSchemaVersion : 
requiredSchemaVersion;
       }
     }
 
     // Handle auto-detection on an existing installation (current version > 0).
     if (requiredSchemaVersion == -1) {
-      // Use the current version if realms already exist; otherwise, use v3 
for the new realm.
-      return hasAlreadyBootstrappedRealms ? currentSchemaVersion : 3;
+      // Use the current version if realms already exist; otherwise, use 
latest version for the new
+      // realm.
+      return hasAlreadyBootstrappedRealms ? currentSchemaVersion : 
latestSchemaVersion;
     }
 
     // Any other combination is an unhandled or invalid migration path.
     throw new IllegalStateException(
         String.format(
-            "Cannot determine bootstrap schema version. Current: %d, Required: 
%d, Bootstrapped: %b",
-            currentSchemaVersion, requiredSchemaVersion, 
hasAlreadyBootstrappedRealms));
+            "Cannot determine bootstrap schema version. Current: %d, Required: 
%d, Bootstrapped: %b, Database: %s",
+            currentSchemaVersion,
+            requiredSchemaVersion,
+            hasAlreadyBootstrappedRealms,
+            databaseType));
   }
 
   /**
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java
 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java
index 58947bd182..032934bae1 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java
+++ 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java
@@ -160,6 +160,7 @@ public class JdbcMetaStoreManagerFactory implements 
MetaStoreManagerFactory {
         int requestedSchemaVersion = 
JdbcBootstrapUtils.getRequestedSchemaVersion(bootstrapOptions);
         int effectiveSchemaVersion =
             JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+                datasourceOperations.getDatabaseType(),
                 currentSchemaVersion,
                 requestedSchemaVersion,
                 
JdbcBasePersistenceImpl.entityTableExists(datasourceOperations));
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
index 22e389a086..adfcae5ab1 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
@@ -29,4 +29,10 @@ public interface RelationalJdbcConfiguration {
 
   // initial delay
   Optional<Long> initialDelayInMs();
+
+  /**
+   * Explicitly configured database type. If not specified, the database type 
will be inferred from
+   * the JDBC connection metadata. Supported values: "postgresql", 
"cockroachdb", "h2"
+   */
+  Optional<String> databaseType();
 }
diff --git 
a/persistence/relational-jdbc/src/main/resources/cockroachdb/schema-v4.sql 
b/persistence/relational-jdbc/src/main/resources/cockroachdb/schema-v4.sql
new file mode 100644
index 0000000000..b982bae991
--- /dev/null
+++ b/persistence/relational-jdbc/src/main/resources/cockroachdb/schema-v4.sql
@@ -0,0 +1,308 @@
+--
+-- 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.
+
+-- CockroachDB schema v4 (matching PostgreSQL schema v4)
+-- Schema version is kept in sync with PostgreSQL to ensure correct column 
selection in ModelEntity.
+-- Changes from v3:
+--  * Added `idempotency_records` table for REST idempotency
+--  * Added `scan_metrics_report` table
+--  * Added `commit_metrics_report` table
+-- Features:
+--  * Uses INT4 explicitly for all integer columns (required for CockroachDB 
JDBC driver)
+--  * Includes all tables: version, entities, grant_records, 
principal_authentication_data,
+--    policy_mapping_record, events, idempotency_records, scan_metrics_report, 
commit_metrics_report
+--  * Compatible with PostgreSQL wire protocol
+
+CREATE SCHEMA IF NOT EXISTS POLARIS_SCHEMA;
+SET search_path TO POLARIS_SCHEMA;
+
+CREATE TABLE IF NOT EXISTS version (
+    version_key TEXT PRIMARY KEY,
+    version_value INT4 NOT NULL
+);
+INSERT INTO version (version_key, version_value)
+VALUES ('version', 4)
+ON CONFLICT (version_key) DO UPDATE
+SET version_value = EXCLUDED.version_value;
+COMMENT ON TABLE version IS 'the version of the JDBC schema in use';
+
+CREATE TABLE IF NOT EXISTS entities (
+    realm_id TEXT NOT NULL,
+    catalog_id BIGINT NOT NULL,
+    id BIGINT NOT NULL,
+    parent_id BIGINT NOT NULL,
+    name TEXT NOT NULL,
+    entity_version INT4 NOT NULL,
+    type_code INT4 NOT NULL,
+    sub_type_code INT4 NOT NULL,
+    create_timestamp BIGINT NOT NULL,
+    drop_timestamp BIGINT NOT NULL,
+    purge_timestamp BIGINT NOT NULL,
+    to_purge_timestamp BIGINT NOT NULL,
+    last_update_timestamp BIGINT NOT NULL,
+    properties JSONB not null default '{}'::JSONB,
+    internal_properties JSONB not null default '{}'::JSONB,
+    grant_records_version INT4 NOT NULL,
+    location_without_scheme TEXT,
+    PRIMARY KEY (realm_id, id),
+    CONSTRAINT constraint_name UNIQUE (realm_id, catalog_id, parent_id, 
type_code, name)
+);
+
+-- TODO: create indexes based on all query pattern.
+CREATE INDEX IF NOT EXISTS idx_entities ON entities (realm_id, catalog_id, id);
+CREATE INDEX IF NOT EXISTS idx_locations
+    ON entities USING btree (realm_id, parent_id, location_without_scheme)
+    WHERE location_without_scheme IS NOT NULL;
+
+COMMENT ON TABLE entities IS 'all the entities';
+
+COMMENT ON COLUMN entities.realm_id IS 'realm_id used for multi-tenancy';
+COMMENT ON COLUMN entities.catalog_id IS 'catalog id';
+COMMENT ON COLUMN entities.id IS 'entity id';
+COMMENT ON COLUMN entities.parent_id IS 'entity id of parent';
+COMMENT ON COLUMN entities.name IS 'entity name';
+COMMENT ON COLUMN entities.entity_version IS 'version of the entity';
+COMMENT ON COLUMN entities.type_code IS 'type code';
+COMMENT ON COLUMN entities.sub_type_code IS 'sub type of entity';
+COMMENT ON COLUMN entities.create_timestamp IS 'creation time of entity';
+COMMENT ON COLUMN entities.drop_timestamp IS 'time of drop of entity';
+COMMENT ON COLUMN entities.purge_timestamp IS 'time to start purging entity';
+COMMENT ON COLUMN entities.last_update_timestamp IS 'last time the entity is 
touched';
+COMMENT ON COLUMN entities.properties IS 'entities properties json';
+COMMENT ON COLUMN entities.internal_properties IS 'entities internal 
properties json';
+COMMENT ON COLUMN entities.grant_records_version IS 'the version of grant 
records change on the entity';
+
+CREATE TABLE IF NOT EXISTS grant_records (
+    realm_id TEXT NOT NULL,
+    securable_catalog_id BIGINT NOT NULL,
+    securable_id BIGINT NOT NULL,
+    grantee_catalog_id BIGINT NOT NULL,
+    grantee_id BIGINT NOT NULL,
+    privilege_code INT4,
+    PRIMARY KEY (realm_id, securable_catalog_id, securable_id, 
grantee_catalog_id, grantee_id, privilege_code)
+);
+
+COMMENT ON TABLE grant_records IS 'grant records for entities';
+
+COMMENT ON COLUMN grant_records.securable_catalog_id IS 'catalog id of the 
securable';
+COMMENT ON COLUMN grant_records.securable_id IS 'entity id of the securable';
+COMMENT ON COLUMN grant_records.grantee_catalog_id IS 'catalog id of the 
grantee';
+COMMENT ON COLUMN grant_records.grantee_id IS 'id of the grantee';
+COMMENT ON COLUMN grant_records.privilege_code IS 'privilege code';
+
+CREATE TABLE IF NOT EXISTS principal_authentication_data (
+    realm_id TEXT NOT NULL,
+    principal_id BIGINT NOT NULL,
+    principal_client_id VARCHAR(255) NOT NULL,
+    main_secret_hash VARCHAR(255) NOT NULL,
+    secondary_secret_hash VARCHAR(255) NOT NULL,
+    secret_salt VARCHAR(255) NOT NULL,
+    PRIMARY KEY (realm_id, principal_client_id)
+);
+
+COMMENT ON TABLE principal_authentication_data IS 'authentication data for 
client';
+
+CREATE TABLE IF NOT EXISTS policy_mapping_record (
+    realm_id TEXT NOT NULL,
+    target_catalog_id BIGINT NOT NULL,
+    target_id BIGINT NOT NULL,
+    policy_type_code INT4 NOT NULL,
+    policy_catalog_id BIGINT NOT NULL,
+    policy_id BIGINT NOT NULL,
+    parameters JSONB NOT NULL DEFAULT '{}'::JSONB,
+    PRIMARY KEY (realm_id, target_catalog_id, target_id, policy_type_code, 
policy_catalog_id, policy_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_policy_mapping_record ON policy_mapping_record 
(realm_id, policy_type_code, policy_catalog_id, policy_id, target_catalog_id, 
target_id);
+
+CREATE TABLE IF NOT EXISTS events (
+    realm_id TEXT NOT NULL,
+    catalog_id TEXT NOT NULL,
+    event_id TEXT NOT NULL,
+    request_id TEXT,
+    event_type TEXT NOT NULL,
+    timestamp_ms BIGINT NOT NULL,
+    principal_name TEXT,
+    resource_type TEXT NOT NULL,
+    resource_identifier TEXT NOT NULL,
+    additional_properties JSONB NOT NULL DEFAULT '{}'::JSONB,
+    PRIMARY KEY (event_id)
+);
+
+-- Idempotency records (key-only idempotency; durable replay)
+CREATE TABLE IF NOT EXISTS idempotency_records (
+    realm_id TEXT NOT NULL,
+    idempotency_key TEXT NOT NULL,
+    operation_type TEXT NOT NULL,
+    resource_id TEXT NOT NULL,            -- normalized request-derived 
resource identifier (not a generated entity id)
+
+    -- Finalization/replay
+    http_status INT4,                    -- NULL while IN_PROGRESS; set only 
on finalized 2xx/terminal 4xx
+    error_subtype TEXT,                  -- optional: e.g., already_exists, 
namespace_not_empty, idempotency_replay_failed
+    response_summary TEXT,               -- minimal body to reproduce 
equivalent response (JSON string)
+    response_headers TEXT,               -- small whitelisted headers to 
replay (JSON string)
+    finalized_at TIMESTAMP,              -- when http_status was written
+
+    -- Liveness/ops
+    created_at TIMESTAMP NOT NULL,
+    updated_at TIMESTAMP NOT NULL,
+    heartbeat_at TIMESTAMP,              -- updated by owner while IN_PROGRESS
+    executor_id TEXT,                    -- owner pod/worker id
+    expires_at TIMESTAMP,
+
+    PRIMARY KEY (realm_id, idempotency_key)
+);
+
+-- Helpful indexes
+CREATE INDEX IF NOT EXISTS idx_idemp_realm_expires
+    ON idempotency_records (realm_id, expires_at);
+
+-- ============================================================================
+-- SCAN METRICS REPORT TABLE
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS scan_metrics_report (
+    report_id TEXT NOT NULL,
+    realm_id TEXT NOT NULL,
+    catalog_id BIGINT NOT NULL,
+    table_id BIGINT NOT NULL,
+
+    -- Report metadata
+    timestamp_ms BIGINT NOT NULL,
+    principal_name TEXT,
+    request_id TEXT,
+
+    -- Trace correlation
+    otel_trace_id TEXT,
+    otel_span_id TEXT,
+    report_trace_id TEXT,
+
+    -- Scan context
+    snapshot_id BIGINT,
+    schema_id INT4,
+    filter_expression TEXT,
+    projected_field_ids TEXT,
+    projected_field_names TEXT,
+
+    -- Scan metrics
+    result_data_files BIGINT DEFAULT 0,
+    result_delete_files BIGINT DEFAULT 0,
+    total_file_size_bytes BIGINT DEFAULT 0,
+    total_data_manifests BIGINT DEFAULT 0,
+    total_delete_manifests BIGINT DEFAULT 0,
+    scanned_data_manifests BIGINT DEFAULT 0,
+    scanned_delete_manifests BIGINT DEFAULT 0,
+    skipped_data_manifests BIGINT DEFAULT 0,
+    skipped_delete_manifests BIGINT DEFAULT 0,
+    skipped_data_files BIGINT DEFAULT 0,
+    skipped_delete_files BIGINT DEFAULT 0,
+    total_planning_duration_ms BIGINT DEFAULT 0,
+
+    -- Equality/positional delete metrics
+    equality_delete_files BIGINT DEFAULT 0,
+    positional_delete_files BIGINT DEFAULT 0,
+    indexed_delete_files BIGINT DEFAULT 0,
+    total_delete_file_size_bytes BIGINT DEFAULT 0,
+
+    -- Additional metadata (for extensibility)
+    metadata JSONB DEFAULT '{}'::JSONB,
+
+    PRIMARY KEY (realm_id, report_id)
+);
+
+COMMENT ON TABLE scan_metrics_report IS 'Scan metrics reports as first-class 
entities';
+
+-- Index for retention cleanup by timestamp
+CREATE INDEX IF NOT EXISTS idx_scan_report_timestamp ON 
scan_metrics_report(realm_id, timestamp_ms);
+
+-- Index for query lookups by catalog_id and table_id
+CREATE INDEX IF NOT EXISTS idx_scan_report_lookup ON 
scan_metrics_report(realm_id, catalog_id, table_id, timestamp_ms);
+
+-- ============================================================================
+-- COMMIT METRICS REPORT TABLE
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS commit_metrics_report (
+    report_id TEXT NOT NULL,
+    realm_id TEXT NOT NULL,
+    catalog_id BIGINT NOT NULL,
+    table_id BIGINT NOT NULL,
+
+    -- Report metadata
+    timestamp_ms BIGINT NOT NULL,
+    principal_name TEXT,
+    request_id TEXT,
+
+    -- Trace correlation
+    otel_trace_id TEXT,
+    otel_span_id TEXT,
+    report_trace_id TEXT,
+
+    -- Commit context
+    snapshot_id BIGINT NOT NULL,
+    sequence_number BIGINT,
+    operation TEXT NOT NULL,
+
+    -- File metrics
+    added_data_files BIGINT DEFAULT 0,
+    removed_data_files BIGINT DEFAULT 0,
+    total_data_files BIGINT DEFAULT 0,
+    added_delete_files BIGINT DEFAULT 0,
+    removed_delete_files BIGINT DEFAULT 0,
+    total_delete_files BIGINT DEFAULT 0,
+
+    -- Equality delete files
+    added_equality_delete_files BIGINT DEFAULT 0,
+    removed_equality_delete_files BIGINT DEFAULT 0,
+
+    -- Positional delete files
+    added_positional_delete_files BIGINT DEFAULT 0,
+    removed_positional_delete_files BIGINT DEFAULT 0,
+
+    -- Record metrics
+    added_records BIGINT DEFAULT 0,
+    removed_records BIGINT DEFAULT 0,
+    total_records BIGINT DEFAULT 0,
+
+    -- Size metrics
+    added_file_size_bytes BIGINT DEFAULT 0,
+    removed_file_size_bytes BIGINT DEFAULT 0,
+    total_file_size_bytes BIGINT DEFAULT 0,
+
+    -- Duration and attempts
+    total_duration_ms BIGINT DEFAULT 0,
+    attempts INT4 DEFAULT 1,
+
+    -- Additional metadata (for extensibility)
+    metadata JSONB DEFAULT '{}'::JSONB,
+
+    PRIMARY KEY (realm_id, report_id)
+);
+
+COMMENT ON TABLE commit_metrics_report IS 'Commit metrics reports as 
first-class entities';
+
+-- Index for retention cleanup by timestamp
+CREATE INDEX IF NOT EXISTS idx_commit_report_timestamp ON 
commit_metrics_report(realm_id, timestamp_ms);
+
+-- Index for query lookups by catalog_id and table_id
+CREATE INDEX IF NOT EXISTS idx_commit_report_lookup ON 
commit_metrics_report(realm_id, catalog_id, table_id, timestamp_ms);
+
+-- INT4 type used directly in table definitions for CockroachDB JDBC 
compatibility
+-- CockroachDB requires explicit INT4 type declarations to correctly map 
columns to Java's Integer type.
+-- Using generic INTEGER or INT types causes type mapping failures in 
CockroachDB's JDBC driver.
+-- INT4 is equivalent to INTEGER in PostgreSQL, ensuring compatibility with 
both databases.
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 dedce84198..ec1bc59691 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
@@ -95,5 +95,10 @@ public abstract class 
AtomicMetastoreManagerWithJdbcBasePersistenceImplTest
     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/JdbcBootstrapUtilsTest.java
 
b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtilsTest.java
index 6a9eb95524..671baf8ac2 100644
--- 
a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtilsTest.java
+++ 
b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/JdbcBootstrapUtilsTest.java
@@ -44,9 +44,13 @@ class JdbcBootstrapUtilsTest {
 
     // Act & Assert
     assertEquals(
-        version, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(version, 
version, true));
+        version,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, version, version, true));
     assertEquals(
-        version, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(version, 
version, false));
+        version,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, version, version, false));
   }
 
   @Test
@@ -57,11 +61,17 @@ class JdbcBootstrapUtilsTest {
 
     // Act & Assert
     assertEquals(
-        3, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 
-1, hasRealms));
+        4,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, currentVersion, -1, hasRealms));
     assertEquals(
-        2, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 
2, hasRealms));
+        2,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, currentVersion, 2, hasRealms));
     assertEquals(
-        3, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 
3, hasRealms));
+        3,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, currentVersion, 3, hasRealms));
   }
 
   @Test
@@ -72,19 +82,31 @@ class JdbcBootstrapUtilsTest {
 
     // Act & Assert
     assertEquals(
-        1, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 
-1, hasRealms));
+        1,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, currentVersion, -1, hasRealms));
     assertEquals(
-        1, JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, 
1, hasRealms));
+        1,
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, currentVersion, 1, hasRealms));
   }
 
   @ParameterizedTest
-  @CsvSource({"2, true, 2", "3, true, 3", "2, false, 3", "3, false, 3"})
+  @CsvSource({
+    "2, true, 2",
+    "3, true, 3",
+    "4, true, 4",
+    "2, false, 4",
+    "3, false, 4",
+    "4, false, 4"
+  })
   void getVersion_whenExistingDbAndAutoDetect(
       int currentVersion, boolean hasRealms, int expectedVersion) {
     // Act & Assert
     assertEquals(
         expectedVersion,
-        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(currentVersion, -1, 
hasRealms));
+        JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
+            DatabaseType.POSTGRES, currentVersion, -1, hasRealms));
   }
 
   @Test
@@ -99,7 +121,7 @@ class JdbcBootstrapUtilsTest {
         IllegalStateException.class,
         () ->
             JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
-                currentVersion, invalidRequiredVersion, hasRealms));
+                DatabaseType.POSTGRES, currentVersion, invalidRequiredVersion, 
hasRealms));
   }
 
   @Test
@@ -113,13 +135,13 @@ class JdbcBootstrapUtilsTest {
         IllegalStateException.class,
         () ->
             JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
-                currentVersion, requiredVersion, true));
+                DatabaseType.POSTGRES, currentVersion, requiredVersion, true));
 
     assertThrows(
         IllegalStateException.class,
         () ->
             JdbcBootstrapUtils.getRealmBootstrapSchemaVersion(
-                currentVersion, requiredVersion, false));
+                DatabaseType.POSTGRES, currentVersion, requiredVersion, 
false));
   }
 
   @Nested
diff --git 
a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/MetricsReportPersistenceTest.java
 
b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/MetricsReportPersistenceTest.java
index 5d0e3ac144..f2ae29972f 100644
--- 
a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/MetricsReportPersistenceTest.java
+++ 
b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/MetricsReportPersistenceTest.java
@@ -197,5 +197,10 @@ class MetricsReportPersistenceTest {
     public Optional<Long> initialDelayInMs() {
       return Optional.of(10L);
     }
+
+    @Override
+    public Optional<String> databaseType() {
+      return Optional.empty();
+    }
   }
 }
diff --git 
a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/idempotency/RelationalJdbcIdempotencyStorePostgresIT.java
 
b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/idempotency/RelationalJdbcIdempotencyStorePostgresIT.java
index 82cbfc9bea..ee95671dcd 100644
--- 
a/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/idempotency/RelationalJdbcIdempotencyStorePostgresIT.java
+++ 
b/persistence/relational-jdbc/src/test/java/org/apache/polaris/persistence/relational/jdbc/idempotency/RelationalJdbcIdempotencyStorePostgresIT.java
@@ -77,6 +77,11 @@ public class RelationalJdbcIdempotencyStorePostgresIT {
           public Optional<Long> initialDelayInMs() {
             return Optional.of(100L);
           }
+
+          @Override
+          public Optional<String> databaseType() {
+            return Optional.empty();
+          }
         };
     DatasourceOperations ops = new DatasourceOperations(dataSource, cfg);
     try (InputStream is =
diff --git 
a/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcAdminProfile.java
 
b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcAdminProfile.java
new file mode 100644
index 0000000000..2b2e1d4d97
--- /dev/null
+++ 
b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcAdminProfile.java
@@ -0,0 +1,47 @@
+/*
+ * 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.admintool.relational.jdbc;
+
+import java.util.List;
+import java.util.Map;
+import 
org.apache.polaris.test.commons.CockroachRelationalJdbcLifeCycleManagement;
+import org.apache.polaris.test.commons.RelationalJdbcProfile;
+
+public class CockroachJdbcAdminProfile extends RelationalJdbcProfile {
+  @Override
+  public Map<String, String> getConfigOverrides() {
+    return Map.of(
+        "polaris.persistence.type",
+        "relational-jdbc",
+        "polaris.persistence.relational.jdbc.database-type",
+        "cockroachdb",
+        // These two options are required to "trigger" the enablement of JDBC 
data sources in
+        // admin-tool tests, but do not harm other tests.
+        "quarkus.datasource.active",
+        "true",
+        "quarkus.datasource.db-kind",
+        "postgresql");
+  }
+
+  @Override
+  public List<TestResourceEntry> testResources() {
+    return List.of(
+        new 
TestResourceEntry(CockroachRelationalJdbcLifeCycleManagement.class, Map.of()));
+  }
+}
diff --git 
a/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcBootstrapCommandTest.java
 
b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcBootstrapCommandTest.java
new file mode 100644
index 0000000000..c9fd056d6f
--- /dev/null
+++ 
b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcBootstrapCommandTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.admintool.relational.jdbc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.quarkus.test.junit.TestProfile;
+import io.quarkus.test.junit.main.LaunchResult;
+import io.quarkus.test.junit.main.QuarkusMainLauncher;
+import org.junit.jupiter.api.Test;
+
+@TestProfile(CockroachJdbcAdminProfile.class)
+public class CockroachJdbcBootstrapCommandTest extends 
RelationalJdbcBootstrapCommandTest {
+
+  @Override
+  @Test
+  public void testBootstrapFailsWhenAddingRealmWithDifferentSchemaVersion(
+      QuarkusMainLauncher launcher) {
+    // CockroachDB only has schema v4 (no v1, v2 or v3 schemas exist).
+    // Override to bootstrap with v4, which is the only version available for 
CockroachDB.
+    LaunchResult result1 =
+        launcher.launch("bootstrap", "-v", "4", "-r", "realm1", "-c", 
"realm1,root,s3cr3t");
+    assertThat(result1.exitCode()).isEqualTo(0);
+    assertThat(result1.getOutput()).contains("Bootstrap completed 
successfully.");
+  }
+}
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcPurgeCommandTest.java
similarity index 55%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcPurgeCommandTest.java
index 22e389a086..550eb779f4 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/admin/src/test/java/org/apache/polaris/admintool/relational/jdbc/CockroachJdbcPurgeCommandTest.java
@@ -16,17 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.admintool.relational.jdbc;
 
-import java.util.Optional;
+import com.google.common.collect.ImmutableMap;
+import io.quarkus.test.junit.TestProfile;
+import java.util.Map;
+import org.apache.polaris.admintool.PurgeCommandTestBase;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
-
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
+@TestProfile(CockroachJdbcPurgeCommandTest.Profile.class)
+public class CockroachJdbcPurgeCommandTest extends PurgeCommandTestBase {
+  public static class Profile extends CockroachJdbcAdminProfile {
+    @Override
+    public Map<String, String> getConfigOverrides() {
+      return ImmutableMap.<String, String>builder()
+          .putAll(super.getConfigOverrides())
+          .put("pre-bootstrap", "true")
+          .build();
+    }
+  }
 }
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachApplicationIT.java
similarity index 63%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachApplicationIT.java
index 22e389a086..2909b7cd5b 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachApplicationIT.java
@@ -16,17 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.service.it.relational.cockroach;
 
-import java.util.Optional;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.quarkus.test.junit.TestProfile;
+import org.apache.polaris.service.it.test.PolarisApplicationIntegrationTest;
+import org.apache.polaris.test.commons.CockroachRelationalJdbcProfile;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
-
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
-}
+@TestProfile(CockroachRelationalJdbcProfile.class)
+@QuarkusIntegrationTest
+public class CockroachApplicationIT extends PolarisApplicationIntegrationTest 
{}
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachManagementServiceIT.java
similarity index 62%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachManagementServiceIT.java
index 22e389a086..318b012234 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachManagementServiceIT.java
@@ -16,17 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.service.it.relational.cockroach;
 
-import java.util.Optional;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.quarkus.test.junit.TestProfile;
+import 
org.apache.polaris.service.it.test.PolarisManagementServiceIntegrationTest;
+import org.apache.polaris.test.commons.CockroachRelationalJdbcProfile;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
-
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
-}
+@TestProfile(CockroachRelationalJdbcProfile.class)
+@QuarkusIntegrationTest
+public class CockroachManagementServiceIT extends 
PolarisManagementServiceIntegrationTest {}
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachPolicyServiceIT.java
similarity index 63%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachPolicyServiceIT.java
index 22e389a086..c044c6fdfe 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachPolicyServiceIT.java
@@ -16,17 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.service.it.relational.cockroach;
 
-import java.util.Optional;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.quarkus.test.junit.TestProfile;
+import org.apache.polaris.service.it.test.PolarisPolicyServiceIntegrationTest;
+import org.apache.polaris.test.commons.CockroachRelationalJdbcProfile;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
-
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
-}
+@TestProfile(CockroachRelationalJdbcProfile.class)
+@QuarkusIntegrationTest
+public class CockroachPolicyServiceIT extends 
PolarisPolicyServiceIntegrationTest {}
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachRestCatalogIT.java
similarity index 63%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachRestCatalogIT.java
index 22e389a086..a8fa72c14c 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachRestCatalogIT.java
@@ -16,17 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.service.it.relational.cockroach;
 
-import java.util.Optional;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.quarkus.test.junit.TestProfile;
+import 
org.apache.polaris.service.it.test.PolarisRestCatalogFileIntegrationTest;
+import org.apache.polaris.test.commons.CockroachRelationalJdbcProfile;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
-
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
-}
+@TestProfile(CockroachRelationalJdbcProfile.class)
+@QuarkusIntegrationTest
+public class CockroachRestCatalogIT extends 
PolarisRestCatalogFileIntegrationTest {}
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachViewFileIT.java
similarity index 62%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachViewFileIT.java
index 22e389a086..80b0cd97d7 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/service/src/intTest/java/org/apache/polaris/service/it/relational/cockroach/CockroachViewFileIT.java
@@ -16,17 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.service.it.relational.cockroach;
 
-import java.util.Optional;
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.quarkus.test.junit.TestProfile;
+import 
org.apache.polaris.service.it.test.PolarisRestCatalogViewFileIntegrationTestBase;
+import org.apache.polaris.test.commons.CockroachRelationalJdbcProfile;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
-
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
-}
+@TestProfile(CockroachRelationalJdbcProfile.class)
+@QuarkusIntegrationTest
+public class CockroachViewFileIT extends 
PolarisRestCatalogViewFileIntegrationTestBase {}
diff --git a/runtime/test-common/build.gradle.kts 
b/runtime/test-common/build.gradle.kts
index 454040c815..ff22995eca 100644
--- a/runtime/test-common/build.gradle.kts
+++ b/runtime/test-common/build.gradle.kts
@@ -41,6 +41,7 @@ dependencies {
   implementation(platform(libs.testcontainers.bom))
   implementation("org.testcontainers:testcontainers")
   implementation("org.testcontainers:testcontainers-postgresql")
+  implementation("org.testcontainers:testcontainers-cockroachdb")
 
   implementation(libs.testcontainers.keycloak) {
     exclude(group = "org.keycloak", module = "keycloak-admin-client")
diff --git 
a/runtime/test-common/src/main/java/org/apache/polaris/test/commons/CockroachRelationalJdbcLifeCycleManagement.java
 
b/runtime/test-common/src/main/java/org/apache/polaris/test/commons/CockroachRelationalJdbcLifeCycleManagement.java
new file mode 100644
index 0000000000..6a1577c7ca
--- /dev/null
+++ 
b/runtime/test-common/src/main/java/org/apache/polaris/test/commons/CockroachRelationalJdbcLifeCycleManagement.java
@@ -0,0 +1,93 @@
+/*
+ * 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.test.commons;
+
+import static 
org.apache.polaris.containerspec.ContainerSpecHelper.containerSpecHelper;
+
+import io.quarkus.test.common.DevServicesContext;
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import java.util.Map;
+import org.testcontainers.cockroachdb.CockroachContainer;
+
+public class CockroachRelationalJdbcLifeCycleManagement
+    implements QuarkusTestResourceLifecycleManager, 
DevServicesContext.ContextAware {
+  public static final String INIT_SCRIPT = "init-script";
+
+  private CockroachContainer cockroach;
+  private String initScript;
+  private DevServicesContext context;
+
+  @Override
+  public void init(Map<String, String> initArgs) {
+    initScript = initArgs.get(INIT_SCRIPT);
+  }
+
+  @Override
+  @SuppressWarnings("resource")
+  public Map<String, String> start() {
+    cockroach =
+        new CockroachContainer(
+            containerSpecHelper("cockroachdb", 
CockroachRelationalJdbcLifeCycleManagement.class)
+                .dockerImageName(null)
+                .asCompatibleSubstituteFor("cockroachdb/cockroach"));
+
+    if (initScript != null) {
+      cockroach.withInitScript(initScript);
+    }
+
+    context.containerNetworkId().ifPresent(cockroach::withNetworkMode);
+    cockroach.start();
+
+    // CockroachDB uses PostgreSQL JDBC driver and wire protocol
+    // Explicitly configure database type as cockroachdb for proper 
identification
+    return Map.of(
+        "polaris.persistence.type",
+        "relational-jdbc",
+        "polaris.persistence.relational.jdbc.database-type",
+        "cockroachdb",
+        "polaris.persistence.relational.jdbc.max-retries",
+        "2",
+        "quarkus.datasource.db-kind",
+        "postgresql",
+        "quarkus.datasource.jdbc.url",
+        cockroach.getJdbcUrl(),
+        "quarkus.datasource.username",
+        cockroach.getUsername(),
+        "quarkus.datasource.password",
+        cockroach.getPassword(),
+        "quarkus.datasource.jdbc.initial-size",
+        "10");
+  }
+
+  @Override
+  public void stop() {
+    if (cockroach != null) {
+      try {
+        cockroach.stop();
+      } finally {
+        cockroach = null;
+      }
+    }
+  }
+
+  @Override
+  public void setIntegrationTestContext(DevServicesContext context) {
+    this.context = context;
+  }
+}
diff --git 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
 
b/runtime/test-common/src/main/java/org/apache/polaris/test/commons/CockroachRelationalJdbcProfile.java
similarity index 54%
copy from 
persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
copy to 
runtime/test-common/src/main/java/org/apache/polaris/test/commons/CockroachRelationalJdbcProfile.java
index 22e389a086..b86a0209b5 100644
--- 
a/persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/RelationalJdbcConfiguration.java
+++ 
b/runtime/test-common/src/main/java/org/apache/polaris/test/commons/CockroachRelationalJdbcProfile.java
@@ -16,17 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.polaris.persistence.relational.jdbc;
+package org.apache.polaris.test.commons;
 
-import java.util.Optional;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import java.util.List;
+import java.util.Map;
 
-public interface RelationalJdbcConfiguration {
-  // max retries before giving up
-  Optional<Integer> maxRetries();
+public class CockroachRelationalJdbcProfile implements QuarkusTestProfile {
+  @Override
+  public Map<String, String> getConfigOverrides() {
+    return Map.of(
+        "polaris.persistence.auto-bootstrap-types",
+        "relational-jdbc",
+        "polaris.persistence.relational.jdbc.database-type",
+        "cockroachdb");
+  }
 
-  // max retry duration
-  Optional<Long> maxDurationInMs();
-
-  // initial delay
-  Optional<Long> initialDelayInMs();
+  @Override
+  public List<TestResourceEntry> testResources() {
+    return List.of(
+        new QuarkusTestProfile.TestResourceEntry(
+            CockroachRelationalJdbcLifeCycleManagement.class, Map.of()));
+  }
 }
diff --git 
a/site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
 
b/runtime/test-common/src/main/resources/org/apache/polaris/test/commons/Dockerfile-cockroachdb-version
similarity index 65%
copy from 
site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
copy to 
runtime/test-common/src/main/resources/org/apache/polaris/test/commons/Dockerfile-cockroachdb-version
index 08f309567d..b74de1108c 100644
--- 
a/site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
+++ 
b/runtime/test-common/src/main/resources/org/apache/polaris/test/commons/Dockerfile-cockroachdb-version
@@ -1,4 +1,3 @@
----
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -17,14 +16,7 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-title: smallrye-polaris_persistence_relational_jdbc
-build:
-  list: never
-  render: never
----
 
-| Property | Default Value | Type | Description |
-|----------|---------------|------|-------------|
-| `polaris.persistence.relational.jdbc.max-retries` |  | `int` |  |
-| `polaris.persistence.relational.jdbc.max-duration-in-ms` |  | `long` |  |
-| `polaris.persistence.relational.jdbc.initial-delay-in-ms` |  | `long` |  |
+# Dockerfile to provide the image name and tag to a test.
+# Version is managed by Renovate - do not edit.
+FROM cockroachdb/cockroach:v24.3.0
diff --git 
a/site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
 
b/site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
index 08f309567d..e50f686542 100644
--- 
a/site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
+++ 
b/site/content/in-dev/unreleased/configuration/config-sections/smallrye-polaris_persistence_relational_jdbc.md
@@ -28,3 +28,4 @@ build:
 | `polaris.persistence.relational.jdbc.max-retries` |  | `int` |  |
 | `polaris.persistence.relational.jdbc.max-duration-in-ms` |  | `long` |  |
 | `polaris.persistence.relational.jdbc.initial-delay-in-ms` |  | `long` |  |
+| `polaris.persistence.relational.jdbc.database-type` |  | `string` |  |

Reply via email to