diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25dd1a97da6..9261b1355b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,11 +33,17 @@ as necessary. Empty sections will not end in the release notes.
- Nessie commit author(s) and "signed off by" can now be configured for both Nessie clients and Iceberg
REST clients. More info on
[projectnessie.org](https://projectnessie.org/guides/iceberg-rest/#customizing-nessie-commit-author-et-al).
+- Introduce new `JDBC2` version store type, which is has the same functionality as the `JDBC` version
+ store type, but uses way less columns, which reduces storage overhead for example in PostgreSQL a lot.
### Changes
### Deprecations
+- The current version store type `JDBC` is deprecated, please migrate to the new `JDBC2` version store
+ type. Please use the [Nessie Server Admin Tool](https://projectnessie.org/nessie-latest/export_import)
+ to migrate from the `JDBC` version store type to `JDBC2`.
+
### Fixes
### Commits
diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index e195fa2a65a..bd0a11ba85d 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -91,6 +91,8 @@ dependencies {
api(project(":nessie-versioned-storage-inmemory-tests"))
api(project(":nessie-versioned-storage-jdbc"))
api(project(":nessie-versioned-storage-jdbc-tests"))
+ api(project(":nessie-versioned-storage-jdbc2"))
+ api(project(":nessie-versioned-storage-jdbc2-tests"))
api(project(":nessie-versioned-storage-mongodb"))
api(project(":nessie-versioned-storage-mongodb-tests"))
api(project(":nessie-versioned-storage-rocksdb"))
diff --git a/gradle/projects.main.properties b/gradle/projects.main.properties
index c082937c3e8..3617bdd4b4e 100644
--- a/gradle/projects.main.properties
+++ b/gradle/projects.main.properties
@@ -78,6 +78,8 @@ nessie-versioned-storage-inmemory=versioned/storage/inmemory
nessie-versioned-storage-inmemory-tests=versioned/storage/inmemory-tests
nessie-versioned-storage-jdbc=versioned/storage/jdbc
nessie-versioned-storage-jdbc-tests=versioned/storage/jdbc-tests
+nessie-versioned-storage-jdbc2=versioned/storage/jdbc2
+nessie-versioned-storage-jdbc2-tests=versioned/storage/jdbc2-tests
nessie-versioned-storage-mongodb=versioned/storage/mongodb
nessie-versioned-storage-mongodb-tests=versioned/storage/mongodb-tests
nessie-versioned-storage-rocksdb=versioned/storage/rocksdb
diff --git a/helm/nessie/values.yaml b/helm/nessie/values.yaml
index 3e066c45a27..705adca9060 100644
--- a/helm/nessie/values.yaml
+++ b/helm/nessie/values.yaml
@@ -28,11 +28,8 @@ imagePullSecrets: []
# `quarkus.log.category."io.smallrye.config".level: DEBUG`
logLevel: INFO
-# -- Which type of version store to use: IN_MEMORY, ROCKSDB, DYNAMODB, MONGODB, CASSANDRA, JDBC, BIGTABLE.
-# (Legacy version store types are: INMEMORY, ROCKS, DYNAMO, MONGO, TRANSACTIONAL. If you are using
-# one of these legacy version store types, migrate your existing repositories to the new version
-# store types using the nessie-server-admin-tool's export/import functionality; support for these
-# legacy version store types has been removed in Nessie 0.75.0.)
+# -- Which type of version store to use: IN_MEMORY, ROCKSDB, DYNAMODB, MONGODB, CASSANDRA, JDBC2, BIGTABLE.
+# Note: the version store type JDBC is deprecated, please use the Nessie Server Admin Tool to migrate to JDBC2.
versionStoreType: IN_MEMORY
# Cassandra settings. Only required when using CASSANDRA version store type; ignored otherwise.
diff --git a/servers/jax-rs-tests/build.gradle.kts b/servers/jax-rs-tests/build.gradle.kts
index abf359348ab..6d8b51fa088 100644
--- a/servers/jax-rs-tests/build.gradle.kts
+++ b/servers/jax-rs-tests/build.gradle.kts
@@ -41,7 +41,7 @@ dependencies {
compileOnly("com.fasterxml.jackson.core:jackson-annotations")
testImplementation(project(":nessie-versioned-storage-inmemory-tests"))
- testImplementation(project(":nessie-versioned-storage-jdbc-tests"))
+ testImplementation(project(":nessie-versioned-storage-jdbc2-tests"))
testRuntimeOnly(libs.agroal.pool)
testImplementation(project(":nessie-jaxrs-testextension"))
diff --git a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/TestRestH2Persist.java b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/TestRestH2Persist.java
index 658f8e97838..e661d8418c5 100644
--- a/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/TestRestH2Persist.java
+++ b/servers/jax-rs-tests/src/test/java/org/projectnessie/jaxrs/tests/TestRestH2Persist.java
@@ -15,7 +15,7 @@
*/
package org.projectnessie.jaxrs.tests;
-import org.projectnessie.versioned.storage.jdbctests.H2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.H2BackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
@NessieBackend(H2BackendTestFactory.class)
diff --git a/servers/quarkus-common/build.gradle.kts b/servers/quarkus-common/build.gradle.kts
index 3ed5278b61f..d552b0163bc 100644
--- a/servers/quarkus-common/build.gradle.kts
+++ b/servers/quarkus-common/build.gradle.kts
@@ -47,6 +47,7 @@ dependencies {
implementation(project(":nessie-versioned-storage-dynamodb"))
implementation(project(":nessie-versioned-storage-inmemory"))
implementation(project(":nessie-versioned-storage-jdbc"))
+ implementation(project(":nessie-versioned-storage-jdbc2"))
implementation(project(":nessie-versioned-storage-mongodb"))
implementation(project(":nessie-versioned-storage-rocksdb"))
implementation(project(":nessie-versioned-storage-store"))
diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusJdbcConfig.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusJdbcConfig.java
index e55942c435b..93e14322453 100644
--- a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusJdbcConfig.java
+++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/QuarkusJdbcConfig.java
@@ -19,10 +19,10 @@
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithName;
import java.util.Optional;
-import org.projectnessie.versioned.storage.jdbc.JdbcBackendBaseConfig;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendBaseConfig;
/**
- * Setting {@code nessie.version.store.type=JDBC} enables transactional/RDBMS as the version store
+ * Setting {@code nessie.version.store.type=JDBC2} enables transactional/RDBMS as the version store
* used by the Nessie server.
*
*
Configuration of the datastore will be done by Quarkus and depends on many factors, such as
@@ -34,7 +34,7 @@
*
For example, to configure a PostgresQL connection, the following configuration should be used:
*
*
- * - {@code nessie.version.store.type=JDBC}
+ *
- {@code nessie.version.store.type=JDBC2}
*
- {@code nessie.version.store.persist.jdbc.datasource=postgresql}
*
- {@code quarkus.datasource.postgresql.jdbc.url=jdbc:postgresql://localhost:5432/my_database}
*
- {@code quarkus.datasource.postgresql.username=}
@@ -46,7 +46,7 @@
*
To connect to a MariaDB database instead, the following configuration should be used:
*
*
- * - {@code nessie.version.store.type=JDBC}
+ *
- {@code nessie.version.store.type=JDBC2}
*
- {@code nessie.version.store.persist.jdbc.datasource=mariadb}
*
- {@code quarkus.datasource.mariadb.jdbc.url=jdbc:mariadb://localhost:3306/my_database}
*
- {@code quarkus.datasource.mariadb.username=}
@@ -57,7 +57,7 @@
*
To connect to a MySQL database instead, the following configuration should be used:
*
*
- * - {@code nessie.version.store.type=JDBC}
+ *
- {@code nessie.version.store.type=JDBC2}
*
- {@code nessie.version.store.persist.jdbc.datasource=mysql}
*
- {@code quarkus.datasource.mysql.jdbc.url=jdbc:mysql://localhost:3306/my_database}
*
- {@code quarkus.datasource.mysql.username=}
@@ -69,7 +69,7 @@
* H2 is not recommended for production):
*
*
- * - {@code nessie.version.store.type=JDBC}
+ *
- {@code nessie.version.store.type=JDBC2}
*
- {@code nessie.version.store.persist.jdbc.datasource=h2}
*
*
@@ -81,7 +81,7 @@
*/
@StaticInitSafe
@ConfigMapping(prefix = "nessie.version.store.persist.jdbc")
-public interface QuarkusJdbcConfig extends JdbcBackendBaseConfig {
+public interface QuarkusJdbcConfig extends Jdbc2BackendBaseConfig {
/**
* The name of the datasource to use. Must correspond to a configured datasource under {@code
diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/datasource/DataSourceActivator.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/datasource/DataSourceActivator.java
index a8d31806abc..dc592d34ddc 100644
--- a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/datasource/DataSourceActivator.java
+++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/config/datasource/DataSourceActivator.java
@@ -58,7 +58,8 @@ public ConfigValue getValue(ConfigSourceInterceptorContext context, String name)
}
static boolean isDataSourceActive(ConfigSourceInterceptorContext context, String name) {
- return versionStoreType(context) == VersionStoreType.JDBC
+ VersionStoreType type = versionStoreType(context);
+ return (type == VersionStoreType.JDBC || type == VersionStoreType.JDBC2)
&& dataSourceName(name).equals(activeDataSourceName(context));
}
diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/Jdbc2BackendBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/Jdbc2BackendBuilder.java
new file mode 100644
index 00000000000..81da352c5fc
--- /dev/null
+++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/Jdbc2BackendBuilder.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.quarkus.providers.storage;
+
+import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC2;
+
+import io.quarkus.agroal.runtime.UnconfiguredDataSource;
+import io.quarkus.arc.All;
+import io.quarkus.arc.InstanceHandle;
+import io.quarkus.datasource.common.runtime.DatabaseKind;
+import io.quarkus.datasource.runtime.DataSourceBuildTimeConfig;
+import io.quarkus.datasource.runtime.DataSourcesBuildTimeConfig;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.Dependent;
+import jakarta.inject.Inject;
+import java.util.List;
+import javax.sql.DataSource;
+import org.projectnessie.quarkus.config.QuarkusJdbcConfig;
+import org.projectnessie.quarkus.providers.versionstore.StoreType;
+import org.projectnessie.versioned.storage.common.persist.Backend;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendConfig;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@StoreType(JDBC2)
+@Dependent
+public class Jdbc2BackendBuilder implements BackendBuilder {
+
+ /**
+ * The name of the default datasource. Corresponds to the default map key in {@link
+ * DataSourcesBuildTimeConfig#dataSources()}.
+ */
+ public static final String DEFAULT_DATA_SOURCE_NAME = "";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Jdbc2BackendBuilder.class);
+
+ @Inject DataSourcesBuildTimeConfig dataSourcesConfig;
+
+ @Inject
+ @All
+ @SuppressWarnings("CdiInjectionPointsInspection")
+ List> dataSources;
+
+ @Inject QuarkusJdbcConfig config;
+
+ @PostConstruct
+ public void checkDataSourcesConfiguration() {
+ dataSourcesConfig.dataSources().forEach(this::checkDatabaseKind);
+ }
+
+ @Override
+ public Backend buildBackend() {
+ DataSource dataSource = selectDataSource();
+ Jdbc2BackendConfig c = Jdbc2BackendConfig.builder().from(config).dataSource(dataSource).build();
+ return new Jdbc2BackendFactory().buildBackend(c);
+ }
+
+ public static String unquoteDataSourceName(String dataSourceName) {
+ if (dataSourceName.startsWith("\"") && dataSourceName.endsWith("\"")) {
+ dataSourceName = dataSourceName.substring(1, dataSourceName.length() - 1);
+ }
+ return dataSourceName;
+ }
+
+ private void checkDatabaseKind(String dataSourceName, DataSourceBuildTimeConfig config) {
+ if (config.dbKind().isEmpty()) {
+ throw new IllegalArgumentException(
+ "Database kind not configured for datasource " + dataSourceName);
+ }
+ String databaseKind = config.dbKind().get();
+ if (!DatabaseKind.isPostgreSQL(databaseKind)
+ && !DatabaseKind.isH2(databaseKind)
+ && !DatabaseKind.isMariaDB(databaseKind)) {
+ throw new IllegalArgumentException(
+ "Database kind for datasource "
+ + dataSourceName
+ + " is configured to '"
+ + databaseKind
+ + "', which Nessie does not support yet; "
+ + "currently PostgreSQL, H2, MariaDB (and MySQL via MariaDB driver) are supported. "
+ + "Feel free to raise a pull request to support your database of choice.");
+ }
+ }
+
+ private DataSource selectDataSource() {
+ String dataSourceName =
+ config
+ .datasourceName()
+ .map(Jdbc2BackendBuilder::unquoteDataSourceName)
+ .orElse(DEFAULT_DATA_SOURCE_NAME);
+ DataSource dataSource = findDataSourceByName(dataSourceName);
+ if (dataSource instanceof UnconfiguredDataSource e) {
+ e.throwException();
+ }
+ if (dataSourceName.equals(DEFAULT_DATA_SOURCE_NAME)) {
+ LOGGER.warn(
+ "Using legacy datasource configuration under quarkus.datasource.*: "
+ + "please migrate to quarkus.datasource.postgresql.* and "
+ + "set nessie.version.store.persist.jdbc.datasource=postgresql");
+ } else {
+ LOGGER.info("Selected datasource: {}", dataSourceName);
+ }
+ return dataSource;
+ }
+
+ private DataSource findDataSourceByName(String dataSourceName) {
+ for (InstanceHandle handle : dataSources) {
+ String name = handle.getBean().getName();
+ name = name == null ? DEFAULT_DATA_SOURCE_NAME : unquoteDataSourceName(name);
+ if (name.equals(dataSourceName)) {
+ return handle.get();
+ }
+ }
+ throw new IllegalStateException("No datasource configured with name: " + dataSourceName);
+ }
+}
diff --git a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/JdbcBackendBuilder.java b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/JdbcBackendBuilder.java
index 3f4a78e597f..a344e1a3eb3 100644
--- a/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/JdbcBackendBuilder.java
+++ b/servers/quarkus-common/src/main/java/org/projectnessie/quarkus/providers/storage/JdbcBackendBuilder.java
@@ -65,7 +65,11 @@ public void checkDataSourcesConfiguration() {
@Override
public Backend buildBackend() {
DataSource dataSource = selectDataSource();
- JdbcBackendConfig c = JdbcBackendConfig.builder().from(config).dataSource(dataSource).build();
+ JdbcBackendConfig c =
+ JdbcBackendConfig.builder()
+ .datasourceName(config.datasourceName())
+ .dataSource(dataSource)
+ .build();
return new JdbcBackendFactory().buildBackend(c);
}
diff --git a/servers/quarkus-config/src/main/java/org/projectnessie/quarkus/config/VersionStoreConfig.java b/servers/quarkus-config/src/main/java/org/projectnessie/quarkus/config/VersionStoreConfig.java
index 80d721b8a05..44173ef6427 100644
--- a/servers/quarkus-config/src/main/java/org/projectnessie/quarkus/config/VersionStoreConfig.java
+++ b/servers/quarkus-config/src/main/java/org/projectnessie/quarkus/config/VersionStoreConfig.java
@@ -31,8 +31,11 @@ enum VersionStoreType {
DYNAMODB,
MONGODB,
CASSANDRA,
+ /** JDBC variant using many distinct columns. */
JDBC,
- BIGTABLE
+ /** JDBC variant using few columns, saves storage overhead for example in PostgreSQL. */
+ JDBC2,
+ BIGTABLE,
}
/** Sets which type of version store to use by Nessie. */
diff --git a/servers/quarkus-server/build.gradle.kts b/servers/quarkus-server/build.gradle.kts
index 883c61576ae..67f26be8edc 100644
--- a/servers/quarkus-server/build.gradle.kts
+++ b/servers/quarkus-server/build.gradle.kts
@@ -59,6 +59,7 @@ dependencies {
implementation(project(":nessie-versioned-spi"))
implementation(project(":nessie-notice"))
implementation(project(":nessie-versioned-storage-jdbc"))
+ implementation(project(":nessie-versioned-storage-jdbc2"))
implementation(libs.nessie.ui)
// Nessie internal Quarkus extension, currently only disables "non-indexed classes" (Jandex)
diff --git a/servers/quarkus-server/src/main/java/org/projectnessie/server/configchecks/ConfigChecks.java b/servers/quarkus-server/src/main/java/org/projectnessie/server/configchecks/ConfigChecks.java
index dd23b002dca..c06e6d34269 100644
--- a/servers/quarkus-server/src/main/java/org/projectnessie/server/configchecks/ConfigChecks.java
+++ b/servers/quarkus-server/src/main/java/org/projectnessie/server/configchecks/ConfigChecks.java
@@ -37,13 +37,15 @@ public class ConfigChecks {
@Inject QuarkusJdbcConfig jdbcConfig;
public void configCheck(@Observes StartupEvent event) {
- if (versionStoreConfig.getVersionStoreType() == VersionStoreConfig.VersionStoreType.IN_MEMORY) {
+ VersionStoreConfig.VersionStoreType versionStoreType = versionStoreConfig.getVersionStoreType();
+ if (versionStoreType == VersionStoreConfig.VersionStoreType.IN_MEMORY) {
LOGGER.warn(
"Configured version store type IN_MEMORY is only for testing purposes and experimentation, not for production use. "
+ "Data will be lost when the process is shut down. "
+ "Recommended action: Use a supported database, see https://projectnessie.org/nessie-latest/configuration/");
}
- if (versionStoreConfig.getVersionStoreType() == VersionStoreConfig.VersionStoreType.JDBC) {
+ if (versionStoreType == VersionStoreConfig.VersionStoreType.JDBC
+ || versionStoreType == VersionStoreConfig.VersionStoreType.JDBC2) {
if (jdbcConfig.datasourceName().isPresent()
&& jdbcConfig.datasourceName().get().equalsIgnoreCase("h2")) {
LOGGER.warn(
diff --git a/servers/quarkus-server/src/main/resources/application.properties b/servers/quarkus-server/src/main/resources/application.properties
index 3dfcd87305b..71300e50127 100644
--- a/servers/quarkus-server/src/main/resources/application.properties
+++ b/servers/quarkus-server/src/main/resources/application.properties
@@ -146,13 +146,8 @@ nessie.server.send-stacktrace-to-client=false
# nessie.server.authorization.rules.allow_listing_reflog=\
# op=='VIEW_REFLOG' && role=='admin_user'
-### which type of version store to use: IN_MEMORY, ROCKSDB, DYNAMODB, MONGODB, CASSANDRA, JDBC, BIGTABLE.
-# Note: legacy configuration in `nessie.version.store.advanced` is _not_ applied to the version
-# store types above. Use the config options starting with `nessie.version.store.persist`.
-#
-# Legacy version store types: INMEMORY, ROCKS, DYNAMO, MONGO, TRANSACTIONAL. If you are using
-# one of these legacy version store types, migrate your existing repositories to the new version
-# store types using the nessie-server-admin-tool's export/import functionality.
+### which type of version store to use: IN_MEMORY, ROCKSDB, DYNAMODB, MONGODB, CASSANDRA, JDBC2, BIGTABLE.
+# Note: the version store type JDBC is deprecated, please use the Nessie Server Admin Tool to migrate to JDBC2.
nessie.version.store.type=IN_MEMORY
# Object cache size as a value relative to the JVM's max heap size. The `cache-capacity-fraction-adjust-mb`
diff --git a/servers/quarkus-tests/build.gradle.kts b/servers/quarkus-tests/build.gradle.kts
index 7b906aba752..ce58a5d7680 100644
--- a/servers/quarkus-tests/build.gradle.kts
+++ b/servers/quarkus-tests/build.gradle.kts
@@ -33,6 +33,7 @@ dependencies {
implementation(project(":nessie-versioned-storage-cassandra-tests"))
implementation(project(":nessie-versioned-storage-dynamodb-tests"))
implementation(project(":nessie-versioned-storage-jdbc-tests"))
+ implementation(project(":nessie-versioned-storage-jdbc2-tests"))
implementation(project(":nessie-versioned-storage-mongodb-tests"))
implementation(project(":nessie-versioned-storage-rocksdb-tests"))
implementation(project(":nessie-versioned-storage-testextension"))
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MariaDBTestResourceLifecycleManager.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MariaDBTestResourceLifecycleManager.java
index 3be0c005420..8071a9727fd 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MariaDBTestResourceLifecycleManager.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MariaDBTestResourceLifecycleManager.java
@@ -19,7 +19,7 @@
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.Map;
import java.util.Optional;
-import org.projectnessie.versioned.storage.jdbctests.MariaDBBackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.MariaDBBackendTestFactory;
public class MariaDBTestResourceLifecycleManager
implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MySQLTestResourceLifecycleManager.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MySQLTestResourceLifecycleManager.java
index c945bb8baa1..02be0c91cea 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MySQLTestResourceLifecycleManager.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/MySQLTestResourceLifecycleManager.java
@@ -20,7 +20,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import org.projectnessie.versioned.storage.jdbctests.MySQLBackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.MySQLBackendTestFactory;
public class MySQLTestResourceLifecycleManager
implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/PostgresTestResourceLifecycleManager.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/PostgresTestResourceLifecycleManager.java
index 0a301d543d5..4d4df3db0f3 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/PostgresTestResourceLifecycleManager.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/PostgresTestResourceLifecycleManager.java
@@ -19,7 +19,7 @@
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import java.util.Map;
import java.util.Optional;
-import org.projectnessie.versioned.storage.jdbctests.PostgreSQLBackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.PostgreSQLBackendTestFactory;
public class PostgresTestResourceLifecycleManager
implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistH2.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistH2.java
index f9d8e198a1b..4ab695ec121 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistH2.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistH2.java
@@ -15,7 +15,7 @@
*/
package org.projectnessie.quarkus.tests.profiles;
-import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC;
+import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC2;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
@@ -26,7 +26,7 @@ public class QuarkusTestProfilePersistH2 extends BaseConfigProfile {
public Map getConfigOverrides() {
return ImmutableMap.builder()
.putAll(super.getConfigOverrides())
- .put("nessie.version.store.type", JDBC.name())
+ .put("nessie.version.store.type", JDBC2.name())
.put("nessie.version.store.persist.jdbc.datasource", "h2")
.build();
}
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMariaDB.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMariaDB.java
index 8ebc53e6e62..7b4b5c1f28a 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMariaDB.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMariaDB.java
@@ -15,7 +15,7 @@
*/
package org.projectnessie.quarkus.tests.profiles;
-import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC;
+import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -28,7 +28,7 @@ public class QuarkusTestProfilePersistMariaDB extends BaseConfigProfile {
public Map getConfigOverrides() {
return ImmutableMap.builder()
.putAll(super.getConfigOverrides())
- .put("nessie.version.store.type", JDBC.name())
+ .put("nessie.version.store.type", JDBC2.name())
.put("nessie.version.store.persist.jdbc.datasource", "mariadb")
.put("quarkus.datasource.mariadb.jdbc.extended-leak-report", "true")
.build();
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMySQL.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMySQL.java
index db871e663e7..9efe677a6ff 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMySQL.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistMySQL.java
@@ -15,7 +15,7 @@
*/
package org.projectnessie.quarkus.tests.profiles;
-import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC;
+import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -28,7 +28,7 @@ public class QuarkusTestProfilePersistMySQL extends BaseConfigProfile {
public Map getConfigOverrides() {
return ImmutableMap.builder()
.putAll(super.getConfigOverrides())
- .put("nessie.version.store.type", JDBC.name())
+ .put("nessie.version.store.type", JDBC2.name())
.put("nessie.version.store.persist.jdbc.datasource", "mysql")
.put("quarkus.datasource.mysql.jdbc.extended-leak-report", "true")
.build();
diff --git a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistPostgres.java b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistPostgres.java
index 7cb97e22475..6b69607d519 100644
--- a/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistPostgres.java
+++ b/servers/quarkus-tests/src/main/java/org/projectnessie/quarkus/tests/profiles/QuarkusTestProfilePersistPostgres.java
@@ -15,7 +15,7 @@
*/
package org.projectnessie.quarkus.tests.profiles;
-import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC;
+import static org.projectnessie.quarkus.config.VersionStoreConfig.VersionStoreType.JDBC2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -28,7 +28,7 @@ public class QuarkusTestProfilePersistPostgres extends BaseConfigProfile {
public Map getConfigOverrides() {
return ImmutableMap.builder()
.putAll(super.getConfigOverrides())
- .put("nessie.version.store.type", JDBC.name())
+ .put("nessie.version.store.type", JDBC2.name())
.put("nessie.version.store.persist.jdbc.datasource", "postgresql")
.put("quarkus.datasource.jdbc.extended-leak-report", "true")
.build();
diff --git a/servers/services-bench/build.gradle.kts b/servers/services-bench/build.gradle.kts
index 1e1fe4d83b1..378cf0ecf0f 100644
--- a/servers/services-bench/build.gradle.kts
+++ b/servers/services-bench/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
jmhRuntimeOnly(project(":nessie-versioned-storage-mongodb"))
jmhRuntimeOnly(project(":nessie-versioned-storage-dynamodb"))
jmhRuntimeOnly(project(":nessie-versioned-storage-jdbc"))
+ jmhRuntimeOnly(project(":nessie-versioned-storage-jdbc2"))
jmhRuntimeOnly(platform(libs.testcontainers.bom))
jmhRuntimeOnly("org.testcontainers:testcontainers")
jmhRuntimeOnly("org.testcontainers:cassandra")
diff --git a/servers/services/build.gradle.kts b/servers/services/build.gradle.kts
index ea32a7ee29a..1889fa3dbc8 100644
--- a/servers/services/build.gradle.kts
+++ b/servers/services/build.gradle.kts
@@ -65,6 +65,7 @@ dependencies {
testFixturesApi(project(":nessie-versioned-storage-testextension"))
testFixturesApi(project(":nessie-versioned-storage-inmemory-tests"))
testFixturesApi(project(":nessie-versioned-storage-jdbc-tests"))
+ testFixturesApi(project(":nessie-versioned-storage-jdbc2-tests"))
testFixturesApi(project(":nessie-services-config"))
testFixturesImplementation(libs.logback.classic)
intTestImplementation(project(":nessie-versioned-storage-cassandra-tests"))
diff --git a/servers/services/src/test/java/org/projectnessie/services/impl/TestApiImplsPersistH2.java b/servers/services/src/test/java/org/projectnessie/services/impl/TestApiImplsPersistH2.java
index 4796ddcacd8..78d42009d5f 100644
--- a/servers/services/src/test/java/org/projectnessie/services/impl/TestApiImplsPersistH2.java
+++ b/servers/services/src/test/java/org/projectnessie/services/impl/TestApiImplsPersistH2.java
@@ -16,7 +16,7 @@
package org.projectnessie.services.impl;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.projectnessie.versioned.storage.jdbctests.H2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.H2BackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
import org.projectnessie.versioned.storage.testextension.PersistExtension;
diff --git a/site/in-dev/configuration.md b/site/in-dev/configuration.md
index 8cfd7cfd3d0..e075c64d06c 100644
--- a/site/in-dev/configuration.md
+++ b/site/in-dev/configuration.md
@@ -189,20 +189,25 @@ Related Quarkus settings:
### Support for the database specific implementations
-| Database | Status | Configuration value for `nessie.version.store.type` | Notes |
-|------------------|--------------------------------------------------|---------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| "in memory" | only for development and local testing | `IN_MEMORY` | Do not use for any serious use case. |
-| RocksDB | production, single node only | `ROCKSDB` | |
-| Google BigTable | production | `BIGTABLE` | |
-| MongoDB | production | `MONGODB` | |
-| Amazon DynamoDB | beta, only tested against the simulator | `DYNAMODB` | Not recommended for use with Nessie Catalog (Iceberg REST) due to its restrictive row-size limit. |
-| PostgreSQL | production | `JDBC` | |
-| H2 | only for development and local testing | `JDBC` | Do not use for any serious use case. |
-| MariaDB | experimental, feedback welcome | `JDBC` | |
-| MySQL | experimental, feedback welcome | `JDBC` | Works by connecting the MariaDB driver to a MySQL server. |
-| CockroachDB | experimental, known issues | `JDBC` | Known to raise user-facing "write too old" errors under contention. |
-| Apache Cassandra | experimental, known issues | `CASSANDRA` | Known to raise user-facing errors due to Cassandra's concept of letting the driver timeout too early, or database timeouts. |
-| ScyllaDB | experimental, known issues | `CASSANDRA` | Known to raise user-facing errors due to Cassandra's concept of letting the driver timeout too early, or database timeouts. Known to be slow in container based testing. Unclear how good Scylla's LWT implementation performs. |
+| Database | Status | Configuration value for `nessie.version.store.type` | Notes |
+|------------------|--------------------------------------------------|-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| "in memory" | only for development and local testing | `IN_MEMORY` | Do not use for any serious use case. |
+| RocksDB | production, single node only | `ROCKSDB` | |
+| Google BigTable | production | `BIGTABLE` | |
+| MongoDB | production | `MONGODB` | |
+| Amazon DynamoDB | beta, only tested against the simulator | `DYNAMODB` | Not recommended for use with Nessie Catalog (Iceberg REST) due to its restrictive row-size limit. |
+| PostgreSQL | production | `JDBC2` & `JDBC` (deprecated) | |
+| H2 | only for development and local testing | `JDBC2` & `JDBC` (deprecated) | Do not use for any serious use case. |
+| MariaDB | experimental, feedback welcome | `JDBC2` & `JDBC` (deprecated) | |
+| MySQL | experimental, feedback welcome | `JDBC2` & `JDBC` (deprecated) | Works by connecting the MariaDB driver to a MySQL server. |
+| CockroachDB | experimental, known issues | `JDBC2` & `JDBC` (deprecated) | Known to raise user-facing "write too old" errors under contention. |
+| Apache Cassandra | experimental, known issues | `CASSANDRA` | Known to raise user-facing errors due to Cassandra's concept of letting the driver timeout too early, or database timeouts. |
+| ScyllaDB | experimental, known issues | `CASSANDRA` | Known to raise user-facing errors due to Cassandra's concept of letting the driver timeout too early, or database timeouts. Known to be slow in container based testing. Unclear how good Scylla's LWT implementation performs. |
+
+!!! warn
+ Prefer the `JDBC2` version store type over the `JDBC` version store type, because it has way less storage overhead.
+ The `JDBC` version store type is _deprecated for removal_, please use the
+ [Nessie Server Admin Tool](export_import.md) to migrate from the `JDBC` version store type to `JDBC2`.
!!! note
Relational databases are generally slower and tend to become a bottleneck when concurrent Nessie commits against
diff --git a/tools/doc-generator/doclet/build.gradle.kts b/tools/doc-generator/doclet/build.gradle.kts
index 9a5d879d6d8..07f4487ce85 100644
--- a/tools/doc-generator/doclet/build.gradle.kts
+++ b/tools/doc-generator/doclet/build.gradle.kts
@@ -34,6 +34,7 @@ val genProjectPaths =
":nessie-versioned-storage-dynamodb",
":nessie-versioned-storage-inmemory",
":nessie-versioned-storage-jdbc",
+ ":nessie-versioned-storage-jdbc2",
":nessie-versioned-storage-mongodb",
":nessie-versioned-storage-rocksdb"
)
diff --git a/tools/doc-generator/site-gen/build.gradle.kts b/tools/doc-generator/site-gen/build.gradle.kts
index a48d2681b0b..aa068e68922 100644
--- a/tools/doc-generator/site-gen/build.gradle.kts
+++ b/tools/doc-generator/site-gen/build.gradle.kts
@@ -42,6 +42,7 @@ val genProjectPaths = listOf(
":nessie-versioned-storage-dynamodb",
":nessie-versioned-storage-inmemory",
":nessie-versioned-storage-jdbc",
+ ":nessie-versioned-storage-jdbc2",
":nessie-versioned-storage-mongodb",
":nessie-versioned-storage-rocksdb",
":nessie-catalog-files-impl",
diff --git a/tools/server-admin/build.gradle.kts b/tools/server-admin/build.gradle.kts
index d3918444965..6270a88a347 100644
--- a/tools/server-admin/build.gradle.kts
+++ b/tools/server-admin/build.gradle.kts
@@ -60,6 +60,7 @@ dependencies {
implementation(project(":nessie-versioned-storage-dynamodb"))
implementation(project(":nessie-versioned-storage-inmemory"))
implementation(project(":nessie-versioned-storage-jdbc"))
+ implementation(project(":nessie-versioned-storage-jdbc2"))
implementation(project(":nessie-versioned-storage-mongodb"))
implementation(project(":nessie-versioned-storage-rocksdb"))
@@ -92,6 +93,7 @@ dependencies {
testFixturesApi(project(":nessie-versioned-tests"))
intTestImplementation(project(":nessie-versioned-storage-mongodb-tests"))
intTestImplementation(project(":nessie-versioned-storage-jdbc-tests"))
+ intTestImplementation(project(":nessie-versioned-storage-jdbc2-tests"))
intTestImplementation(project(":nessie-versioned-storage-cassandra-tests"))
intTestImplementation(project(":nessie-versioned-storage-bigtable-tests"))
intTestImplementation(project(":nessie-versioned-storage-dynamodb-tests"))
diff --git a/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/ITExportImport.java b/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/ITExportImport.java
index 436c5eb87b5..df8eddb0804 100644
--- a/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/ITExportImport.java
+++ b/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/ITExportImport.java
@@ -76,7 +76,8 @@
@ExtendWith({NessieServerAdminTestExtension.class, SoftAssertionsExtension.class})
public class ITExportImport {
- private static final String VERSION_STORES = "(ROCKSDB|DYNAMODB|MONGODB|CASSANDRA|JDBC|BIGTABLE)";
+ private static final String VERSION_STORES =
+ "(ROCKSDB|DYNAMODB|MONGODB|CASSANDRA|JDBC2|BIGTABLE)";
@InjectSoftAssertions private SoftAssertions soft;
diff --git a/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/NessieServerAdminTestBackends.java b/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/NessieServerAdminTestBackends.java
index 02ed2668410..701179901a3 100644
--- a/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/NessieServerAdminTestBackends.java
+++ b/tools/server-admin/src/intTest/java/org/projectnessie/tools/admin/cli/NessieServerAdminTestBackends.java
@@ -20,9 +20,9 @@
import org.projectnessie.versioned.storage.bigtabletests.BigTableBackendContainerTestFactory;
import org.projectnessie.versioned.storage.cassandratests.CassandraBackendTestFactory;
import org.projectnessie.versioned.storage.dynamodbtests.DynamoDBBackendTestFactory;
-import org.projectnessie.versioned.storage.jdbctests.MariaDBBackendTestFactory;
-import org.projectnessie.versioned.storage.jdbctests.MySQLBackendTestFactory;
-import org.projectnessie.versioned.storage.jdbctests.PostgreSQLBackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.MariaDBBackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.MySQLBackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.PostgreSQLBackendTestFactory;
import org.projectnessie.versioned.storage.mongodbtests.MongoDBBackendTestFactory;
import org.projectnessie.versioned.storage.testextension.BackendTestFactory;
@@ -85,7 +85,7 @@ BackendTestFactory backendFactory() {
Map quarkusConfig() {
return Map.of(
"nessie.version.store.type",
- VersionStoreType.JDBC.name(),
+ VersionStoreType.JDBC2.name(),
"nessie.version.store.persist.jdbc.datasource",
"postgresql");
}
@@ -101,7 +101,7 @@ BackendTestFactory backendFactory() {
Map quarkusConfig() {
return Map.of(
"nessie.version.store.type",
- VersionStoreType.JDBC.name(),
+ VersionStoreType.JDBC2.name(),
"nessie.version.store.persist.jdbc.datasource",
"mariadb");
}
@@ -117,7 +117,7 @@ BackendTestFactory backendFactory() {
Map quarkusConfig() {
return Map.of(
"nessie.version.store.type",
- VersionStoreType.JDBC.name(),
+ VersionStoreType.JDBC2.name(),
"nessie.version.store.persist.jdbc.datasource",
"mysql");
}
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBBackendFactory.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBBackendFactory.java
index 0b18c986a02..5a709273c00 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBBackendFactory.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBBackendFactory.java
@@ -15,9 +15,11 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.jdbctests.AbstractJdbcBackendTestFactory;
import org.projectnessie.versioned.storage.jdbctests.CockroachBackendTestFactory;
+@Disabled("Disabled in favor of jdbc2")
public class ITCockroachDBBackendFactory extends AbstractTestJdbcBackendFactory {
@Override
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBPersist.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBPersist.java
index 653cd7b6492..9193ac24145 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBPersist.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBPersist.java
@@ -15,9 +15,11 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
import org.projectnessie.versioned.storage.jdbctests.CockroachBackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
@NessieBackend(CockroachBackendTestFactory.class)
+@Disabled("Disabled in favor of jdbc2")
public class ITCockroachDBPersist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStore.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStore.java
index 717a5be9bae..268ff07364b 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStore.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStore.java
@@ -15,9 +15,11 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
import org.projectnessie.versioned.storage.jdbctests.CockroachBackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
@NessieBackend(CockroachBackendTestFactory.class)
+@Disabled("Disabled in favor of jdbc2")
public class ITCockroachDBVersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStoreCaching.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStoreCaching.java
index 09158c1b947..7fa6f585630 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStoreCaching.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITCockroachDBVersionStoreCaching.java
@@ -15,7 +15,9 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.testextension.NessiePersistCache;
@NessiePersistCache
+@Disabled("Disabled in favor of jdbc2")
public class ITCockroachDBVersionStoreCaching extends ITCockroachDBVersionStore {}
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLBackendFactory.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLBackendFactory.java
index defb5816f31..67e2bad5edb 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLBackendFactory.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLBackendFactory.java
@@ -15,9 +15,11 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.jdbctests.AbstractJdbcBackendTestFactory;
import org.projectnessie.versioned.storage.jdbctests.MySQLBackendTestFactory;
+@Disabled("Disabled in favor of jdbc2")
public class ITMySQLBackendFactory extends AbstractTestJdbcBackendFactory {
@Override
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLPersist.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLPersist.java
index 44a35f54bba..677c5326b8f 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLPersist.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLPersist.java
@@ -15,9 +15,11 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
import org.projectnessie.versioned.storage.jdbctests.MySQLBackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
@NessieBackend(MySQLBackendTestFactory.class)
+@Disabled("Disabled in favor of jdbc2")
public class ITMySQLPersist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLVersionStore.java b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLVersionStore.java
index 151e9b88e47..1162b4e142f 100644
--- a/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLVersionStore.java
+++ b/versioned/storage/jdbc/src/intTest/java/org/projectnessie/versioned/storage/jdbc/ITMySQLVersionStore.java
@@ -15,9 +15,11 @@
*/
package org.projectnessie.versioned.storage.jdbc;
+import org.junit.jupiter.api.Disabled;
import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
import org.projectnessie.versioned.storage.jdbctests.MySQLBackendTestFactory;
import org.projectnessie.versioned.storage.testextension.NessieBackend;
@NessieBackend(MySQLBackendTestFactory.class)
+@Disabled("Disabled in favor of jdbc2")
public class ITMySQLVersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc2-tests/build.gradle.kts b/versioned/storage/jdbc2-tests/build.gradle.kts
new file mode 100644
index 00000000000..240cf8c8dac
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/build.gradle.kts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.
+ */
+
+plugins {
+ id("nessie-conventions-server")
+ id("nessie-jacoco")
+}
+
+publishingHelper { mavenName = "Nessie - Storage - JDBC2 - Tests" }
+
+description = "Base test code for creating test backends using JDBC backends."
+
+dependencies {
+ implementation(project(":nessie-versioned-storage-jdbc2"))
+ implementation(project(":nessie-versioned-storage-common"))
+ implementation(project(":nessie-versioned-storage-testextension"))
+ implementation(project(":nessie-container-spec-helper"))
+
+ compileOnly(libs.jakarta.annotation.api)
+
+ compileOnly(libs.immutables.builder)
+ compileOnly(libs.immutables.value.annotations)
+ annotationProcessor(libs.immutables.value.processor)
+
+ implementation(libs.agroal.pool)
+
+ implementation(platform(libs.testcontainers.bom))
+ implementation("org.testcontainers:postgresql")
+ implementation("org.testcontainers:mysql")
+ implementation("org.testcontainers:mariadb")
+ implementation("org.testcontainers:cockroachdb")
+ implementation("org.testcontainers:mariadb")
+ implementation("org.testcontainers:mysql")
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/AbstractJdbc2BackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/AbstractJdbc2BackendTestFactory.java
new file mode 100644
index 00000000000..eb0ed1457f0
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/AbstractJdbc2BackendTestFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import static org.testcontainers.shaded.com.google.common.base.Preconditions.checkState;
+
+import java.sql.SQLException;
+import javax.sql.DataSource;
+import org.projectnessie.versioned.storage.common.persist.Backend;
+import org.projectnessie.versioned.storage.jdbc2.DatabaseSpecific;
+import org.projectnessie.versioned.storage.jdbc2.DatabaseSpecifics;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2Backend;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendConfig;
+import org.projectnessie.versioned.storage.testextension.BackendTestFactory;
+
+public abstract class AbstractJdbc2BackendTestFactory implements BackendTestFactory {
+
+ public abstract String jdbcUrl();
+
+ public abstract String jdbcUser();
+
+ public abstract String jdbcPass();
+
+ @Override
+ public Backend createNewBackend() throws SQLException {
+ checkState(jdbcUrl() != null, "Must set JDBC URL first");
+
+ DataSource dataSource =
+ DataSourceProducer.builder()
+ .jdbcUrl(jdbcUrl())
+ .jdbcUser(jdbcUser())
+ .jdbcPass(jdbcPass())
+ .build()
+ .createNewDataSource();
+
+ Jdbc2BackendConfig config = Jdbc2BackendConfig.builder().dataSource(dataSource).build();
+
+ DatabaseSpecific databaseSpecific = DatabaseSpecifics.detect(dataSource);
+ return new Jdbc2Backend(config, databaseSpecific, true);
+ }
+
+ @Override
+ public void start() throws Exception {}
+
+ @Override
+ public void stop() throws Exception {}
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/CockroachBackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/CockroachBackendTestFactory.java
new file mode 100644
index 00000000000..9de4e418a0d
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/CockroachBackendTestFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import jakarta.annotation.Nonnull;
+import java.util.Map;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory;
+import org.testcontainers.containers.CockroachContainer;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+
+public class CockroachBackendTestFactory extends ContainerBackendTestFactory {
+
+ @Override
+ public String getName() {
+ return Jdbc2BackendFactory.NAME + "-Cockroach";
+ }
+
+ @Nonnull
+ @Override
+ protected JdbcDatabaseContainer> createContainer() {
+ return new CockroachContainer(
+ dockerImage("cockroach").asCompatibleSubstituteFor("cockroachdb/cockroach"));
+ }
+
+ @Override
+ public Map getQuarkusConfig() {
+ return Map.of(
+ "quarkus.datasource.postgresql.jdbc.url",
+ jdbcUrl(),
+ "quarkus.datasource.postgresql.username",
+ jdbcUser(),
+ "quarkus.datasource.postgresql.password",
+ jdbcPass());
+ }
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/ContainerBackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/ContainerBackendTestFactory.java
new file mode 100644
index 00000000000..a135aff8199
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/ContainerBackendTestFactory.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import static org.testcontainers.shaded.com.google.common.base.Preconditions.checkState;
+
+import jakarta.annotation.Nonnull;
+import java.util.Optional;
+import org.projectnessie.nessie.testing.containerspec.ContainerSpecHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.ContainerLaunchException;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.utility.DockerImageName;
+
+public abstract class ContainerBackendTestFactory extends AbstractJdbc2BackendTestFactory {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ContainerBackendTestFactory.class);
+
+ private JdbcDatabaseContainer> container;
+
+ protected static DockerImageName dockerImage(String dbName) {
+ return ContainerSpecHelper.builder()
+ .name(dbName)
+ .containerClass(ContainerBackendTestFactory.class)
+ .build()
+ .dockerImageName(null);
+ }
+
+ @Override
+ public String jdbcUrl() {
+ checkState(container != null, "Container not started");
+ return container.getJdbcUrl();
+ }
+
+ @Override
+ public String jdbcUser() {
+ checkState(container != null, "Container not started");
+ return container.getUsername();
+ }
+
+ @Override
+ public String jdbcPass() {
+ checkState(container != null, "Container not started");
+ return container.getPassword();
+ }
+
+ @Override
+ public void start(Optional containerNetworkId) throws Exception {
+ if (container != null) {
+ throw new IllegalStateException("Already started");
+ }
+
+ for (int retry = 0; ; retry++) {
+ @SuppressWarnings("resource")
+ JdbcDatabaseContainer> c = createContainer().withLogConsumer(new Slf4jLogConsumer(LOGGER));
+ containerNetworkId.ifPresent(c::withNetworkMode);
+ try {
+ c.start();
+ container = c;
+ break;
+ } catch (ContainerLaunchException e) {
+ c.close();
+ if (e.getCause() != null && retry < 3) {
+ LOGGER.warn("Launch of container {} failed, will retry...", c.getDockerImageName(), e);
+ continue;
+ }
+ LOGGER.error("Launch of container {} failed", c.getDockerImageName(), e);
+ throw new RuntimeException(e);
+ }
+ }
+
+ super.start();
+ }
+
+ @Override
+ public void start() throws Exception {
+ start(Optional.empty());
+ }
+
+ @Override
+ public void stop() throws Exception {
+ try {
+ super.stop();
+ } finally {
+ try {
+ if (container != null) {
+ container.stop();
+ }
+ } finally {
+ container = null;
+ }
+ }
+ }
+
+ @Nonnull
+ protected abstract JdbcDatabaseContainer> createContainer();
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/DataSourceProducer.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/DataSourceProducer.java
new file mode 100644
index 00000000000..a0f1c795419
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/DataSourceProducer.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import io.agroal.api.AgroalDataSource;
+import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration.TransactionIsolation;
+import io.agroal.api.configuration.supplier.AgroalConnectionFactoryConfigurationSupplier;
+import io.agroal.api.configuration.supplier.AgroalConnectionPoolConfigurationSupplier;
+import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier;
+import io.agroal.api.security.NamePrincipal;
+import io.agroal.api.security.SimplePassword;
+import jakarta.annotation.Nullable;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import javax.sql.DataSource;
+import org.immutables.value.Value;
+
+@Value.Immutable
+public abstract class DataSourceProducer {
+
+ public static ImmutableDataSourceProducer.Builder builder() {
+ return ImmutableDataSourceProducer.builder();
+ }
+
+ @Value.Default
+ int initialSize() {
+ return 2;
+ }
+
+ @Value.Default
+ int maxSize() {
+ return 5;
+ }
+
+ @Value.Default
+ int minSize() {
+ return 2;
+ }
+
+ @Value.Default
+ int maxLifetimeMinutes() {
+ return 5;
+ }
+
+ @Value.Default
+ Duration maxLifetime() {
+ return Duration.of(maxLifetimeMinutes(), ChronoUnit.MINUTES);
+ }
+
+ @Value.Default
+ public int acquisitionTimeoutSeconds() {
+ return 20;
+ }
+
+ @Value.Default
+ Duration acquisitionTimeout() {
+ return Duration.of(acquisitionTimeoutSeconds(), ChronoUnit.SECONDS);
+ }
+
+ @Value.Default
+ TransactionIsolation transactionIsolation() {
+ return TransactionIsolation.READ_COMMITTED;
+ }
+
+ public abstract String jdbcUrl();
+
+ @Nullable
+ public abstract String jdbcUser();
+
+ @Nullable
+ public abstract String jdbcPass();
+
+ public DataSource createNewDataSource() throws SQLException {
+ AgroalDataSourceConfigurationSupplier dataSourceConfiguration =
+ new AgroalDataSourceConfigurationSupplier();
+ AgroalConnectionPoolConfigurationSupplier poolConfiguration =
+ dataSourceConfiguration.connectionPoolConfiguration();
+ AgroalConnectionFactoryConfigurationSupplier connectionFactoryConfiguration =
+ poolConfiguration.connectionFactoryConfiguration();
+
+ // configure pool
+ poolConfiguration
+ .initialSize(initialSize())
+ .maxSize(maxSize())
+ .minSize(minSize())
+ .maxLifetime(maxLifetime())
+ .acquisitionTimeout(acquisitionTimeout());
+
+ // configure supplier
+ connectionFactoryConfiguration.jdbcUrl(jdbcUrl());
+ if (jdbcUser() != null) {
+ connectionFactoryConfiguration.credential(new NamePrincipal(jdbcUser()));
+ connectionFactoryConfiguration.credential(new SimplePassword(jdbcPass()));
+ }
+ connectionFactoryConfiguration.jdbcTransactionIsolation(transactionIsolation());
+ connectionFactoryConfiguration.autoCommit(false);
+
+ return AgroalDataSource.from(dataSourceConfiguration.get());
+ }
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/H2BackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/H2BackendTestFactory.java
new file mode 100644
index 00000000000..a83c5233a3c
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/H2BackendTestFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import java.util.Map;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory;
+
+public final class H2BackendTestFactory extends AbstractJdbc2BackendTestFactory {
+
+ @Override
+ public String getName() {
+ return Jdbc2BackendFactory.NAME + "-H2";
+ }
+
+ @Override
+ public String jdbcUrl() {
+ return "jdbc:h2:mem:nessie;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH";
+ }
+
+ @Override
+ public String jdbcUser() {
+ return null;
+ }
+
+ @Override
+ public String jdbcPass() {
+ return null;
+ }
+
+ @Override
+ public Map getQuarkusConfig() {
+ return Map.of("quarkus.datasource.postgresql.jdbc.url", jdbcUrl());
+ }
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/MariaDBBackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/MariaDBBackendTestFactory.java
new file mode 100644
index 00000000000..4b8abfc93d3
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/MariaDBBackendTestFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import jakarta.annotation.Nonnull;
+import java.util.Map;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.MariaDBContainer;
+
+public class MariaDBBackendTestFactory extends ContainerBackendTestFactory {
+
+ @Override
+ public String getName() {
+ return Jdbc2BackendFactory.NAME + "-MariaDB";
+ }
+
+ @Nonnull
+ @Override
+ @SuppressWarnings("resource")
+ protected JdbcDatabaseContainer> createContainer() {
+ return new MariaDBContainer<>(dockerImage("mariadb").asCompatibleSubstituteFor("mariadb"))
+ .withUrlParam("useBulkStmtsForInserts", "false");
+ }
+
+ @Override
+ public Map getQuarkusConfig() {
+ return Map.of(
+ "quarkus.datasource.mariadb.jdbc.url",
+ jdbcUrl(),
+ "quarkus.datasource.mariadb.username",
+ jdbcUser(),
+ "quarkus.datasource.mariadb.password",
+ jdbcPass());
+ }
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/MySQLBackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/MySQLBackendTestFactory.java
new file mode 100644
index 00000000000..eafa3b0759f
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/MySQLBackendTestFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import jakarta.annotation.Nonnull;
+import java.util.Map;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.utility.DockerImageName;
+
+public class MySQLBackendTestFactory extends ContainerBackendTestFactory {
+
+ @Override
+ public String getName() {
+ return Jdbc2BackendFactory.NAME + "-MySQL";
+ }
+
+ @Nonnull
+ @Override
+ protected JdbcDatabaseContainer> createContainer() {
+ return new MariaDBDriverMySQLContainer(dockerImage("mysql"));
+ }
+
+ @Override
+ public Map getQuarkusConfig() {
+ return Map.of(
+ "quarkus.datasource.mysql.jdbc.url",
+ jdbcUrl(),
+ "quarkus.datasource.mysql.username",
+ jdbcUser(),
+ "quarkus.datasource.mysql.password",
+ jdbcPass());
+ }
+
+ private static class MariaDBDriverMySQLContainer
+ extends MySQLContainer {
+
+ MariaDBDriverMySQLContainer(DockerImageName dockerImage) {
+ super(dockerImage.asCompatibleSubstituteFor("mysql"));
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return "org.mariadb.jdbc.Driver";
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ return super.getJdbcUrl().replace("jdbc:mysql", "jdbc:mariadb");
+ }
+ }
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/PostgreSQLBackendTestFactory.java b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/PostgreSQLBackendTestFactory.java
new file mode 100644
index 00000000000..2b8e7ee1df8
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/java/org/projectnessie/versioned/storage/jdbc2tests/PostgreSQLBackendTestFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2tests;
+
+import jakarta.annotation.Nonnull;
+import java.util.Map;
+import org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.PostgreSQLContainer;
+
+public class PostgreSQLBackendTestFactory extends ContainerBackendTestFactory {
+
+ @Override
+ public String getName() {
+ return Jdbc2BackendFactory.NAME + "-Postgres";
+ }
+
+ @Nonnull
+ @Override
+ protected JdbcDatabaseContainer> createContainer() {
+ return new PostgreSQLContainer<>(dockerImage("postgres").asCompatibleSubstituteFor("postgres"));
+ }
+
+ @Override
+ public Map getQuarkusConfig() {
+ return Map.of(
+ "quarkus.datasource.postgresql.jdbc.url",
+ jdbcUrl(),
+ "quarkus.datasource.postgresql.username",
+ jdbcUser(),
+ "quarkus.datasource.postgresql.password",
+ jdbcPass());
+ }
+}
diff --git a/versioned/storage/jdbc2-tests/src/main/resources/META-INF/services/org.projectnessie.versioned.storage.testextension.BackendTestFactory b/versioned/storage/jdbc2-tests/src/main/resources/META-INF/services/org.projectnessie.versioned.storage.testextension.BackendTestFactory
new file mode 100644
index 00000000000..22ef21d2098
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/resources/META-INF/services/org.projectnessie.versioned.storage.testextension.BackendTestFactory
@@ -0,0 +1,20 @@
+#
+# Copyright (C) 2022 Dremio
+#
+# Licensed 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.
+#
+org.projectnessie.versioned.storage.jdbc2tests.PostgreSQLBackendTestFactory
+org.projectnessie.versioned.storage.jdbc2tests.CockroachBackendTestFactory
+org.projectnessie.versioned.storage.jdbc2tests.H2BackendTestFactory
+org.projectnessie.versioned.storage.jdbc2tests.MariaDBBackendTestFactory
+org.projectnessie.versioned.storage.jdbc2tests.MySQLBackendTestFactory
diff --git a/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-cockroach-version b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-cockroach-version
new file mode 100644
index 00000000000..b69c4c8d28b
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-cockroach-version
@@ -0,0 +1,3 @@
+# Dockerfile to provide the image name and tag to a test.
+# Version is managed by Renovate - do not edit.
+FROM cockroachdb/cockroach:v24.1.3
diff --git a/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-mariadb-version b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-mariadb-version
new file mode 100644
index 00000000000..92fb7d6e84e
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-mariadb-version
@@ -0,0 +1,3 @@
+# Dockerfile to provide the image name and tag to a test.
+# Version is managed by Renovate - do not edit.
+FROM mariadb:11.5.2
diff --git a/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-mysql-version b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-mysql-version
new file mode 100644
index 00000000000..2f887635a72
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-mysql-version
@@ -0,0 +1,3 @@
+# Dockerfile to provide the image name and tag to a test.
+# Version is managed by Renovate - do not edit.
+FROM mysql:9.0.1
diff --git a/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-postgres-version b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-postgres-version
new file mode 100644
index 00000000000..58cd81ab480
--- /dev/null
+++ b/versioned/storage/jdbc2-tests/src/main/resources/org/projectnessie/versioned/storage/jdbc2tests/Dockerfile-postgres-version
@@ -0,0 +1,3 @@
+# Dockerfile to provide the image name and tag to a test.
+# Version is managed by Renovate - do not edit.
+FROM postgres:16.4
diff --git a/versioned/storage/jdbc2/build.gradle.kts b/versioned/storage/jdbc2/build.gradle.kts
new file mode 100644
index 00000000000..c2018ed868c
--- /dev/null
+++ b/versioned/storage/jdbc2/build.gradle.kts
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.
+ */
+
+import org.apache.tools.ant.taskdefs.condition.Os
+
+plugins {
+ id("nessie-conventions-server")
+ id("nessie-jacoco")
+}
+
+publishingHelper { mavenName = "Nessie - Storage - JDBC2" }
+
+description =
+ "Storage implementation for JDBC, supports H2, PostgreSQL, CockroachDB, MariaDB and MySQL (via MariaDB Driver)."
+
+dependencies {
+ implementation(project(":nessie-versioned-storage-common"))
+ implementation(project(":nessie-versioned-storage-common-proto"))
+ implementation(project(":nessie-versioned-storage-common-serialize"))
+
+ compileOnly(libs.jakarta.validation.api)
+ compileOnly(libs.jakarta.annotation.api)
+
+ compileOnly(libs.errorprone.annotations)
+ implementation(libs.guava)
+ implementation(libs.agrona)
+ implementation(libs.slf4j.api)
+
+ compileOnly(libs.immutables.builder)
+ compileOnly(libs.immutables.value.annotations)
+ annotationProcessor(libs.immutables.value.processor)
+
+ testFixturesApi(project(":nessie-versioned-storage-jdbc2-tests"))
+ testFixturesApi(project(":nessie-versioned-storage-common"))
+ testFixturesApi(project(":nessie-versioned-storage-common-tests"))
+ testFixturesApi(project(":nessie-versioned-storage-testextension"))
+ testFixturesApi(project(":nessie-versioned-tests"))
+ testFixturesImplementation(libs.agroal.pool)
+ testRuntimeOnly(libs.h2)
+ intTestRuntimeOnly(libs.postgresql)
+ intTestRuntimeOnly(libs.mariadb.java.client)
+ intTestRuntimeOnly(platform(libs.testcontainers.bom))
+ intTestRuntimeOnly(libs.docker.java.api)
+ testFixturesImplementation(platform(libs.junit.bom))
+ testFixturesImplementation(libs.bundles.junit.testing)
+ testFixturesImplementation(libs.logback.classic)
+}
+
+// Testcontainers is not supported on Windows :(
+if (Os.isFamily(Os.FAMILY_WINDOWS)) {
+ tasks.named("intTest").configure { this.enabled = false }
+}
+
+// Issue w/ testcontainers/podman in GH workflows :(
+if (Os.isFamily(Os.FAMILY_MAC) && System.getenv("CI") != null) {
+ tasks.named("intTest").configure { this.enabled = false }
+}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBBackendFactory.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBBackendFactory.java
new file mode 100644
index 00000000000..56a7317b855
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBBackendFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.jdbc2tests.AbstractJdbc2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.CockroachBackendTestFactory;
+
+public class ITCockroachDBBackendFactory extends AbstractTestJdbc2BackendFactory {
+
+ @Override
+ protected AbstractJdbc2BackendTestFactory testFactory() {
+ return new CockroachBackendTestFactory();
+ }
+}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBPersist.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBPersist.java
new file mode 100644
index 00000000000..43c7ea4828c
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBPersist.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
+import org.projectnessie.versioned.storage.jdbc2tests.CockroachBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(CockroachBackendTestFactory.class)
+public class ITCockroachDBPersist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBVersionStore.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBVersionStore.java
new file mode 100644
index 00000000000..e5eaed121a7
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBVersionStore.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
+import org.projectnessie.versioned.storage.jdbc2tests.CockroachBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(CockroachBackendTestFactory.class)
+public class ITCockroachDBVersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBVersionStoreCaching.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBVersionStoreCaching.java
new file mode 100644
index 00000000000..d498f5fc151
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITCockroachDBVersionStoreCaching.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.testextension.NessiePersistCache;
+
+@NessiePersistCache
+public class ITCockroachDBVersionStoreCaching extends ITCockroachDBVersionStore {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBBackendFactory.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBBackendFactory.java
new file mode 100644
index 00000000000..838bcb3b522
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBBackendFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.jdbc2tests.AbstractJdbc2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.MariaDBBackendTestFactory;
+
+public class ITMariaDBBackendFactory extends AbstractTestJdbc2BackendFactory {
+
+ @Override
+ protected AbstractJdbc2BackendTestFactory testFactory() {
+ return new MariaDBBackendTestFactory();
+ }
+}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBPersist.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBPersist.java
new file mode 100644
index 00000000000..0db62e370e3
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBPersist.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
+import org.projectnessie.versioned.storage.jdbc2tests.MariaDBBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(MariaDBBackendTestFactory.class)
+public class ITMariaDBPersist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBVersionStore.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBVersionStore.java
new file mode 100644
index 00000000000..1362240ccd8
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMariaDBVersionStore.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
+import org.projectnessie.versioned.storage.jdbc2tests.MariaDBBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(MariaDBBackendTestFactory.class)
+public class ITMariaDBVersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLBackendFactory.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLBackendFactory.java
new file mode 100644
index 00000000000..57c81fc75c5
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLBackendFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.jdbc2tests.AbstractJdbc2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.MySQLBackendTestFactory;
+
+public class ITMySQLBackendFactory extends AbstractTestJdbc2BackendFactory {
+
+ @Override
+ protected AbstractJdbc2BackendTestFactory testFactory() {
+ return new MySQLBackendTestFactory();
+ }
+}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLPersist.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLPersist.java
new file mode 100644
index 00000000000..ff13f71a6ed
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLPersist.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
+import org.projectnessie.versioned.storage.jdbc2tests.MySQLBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(MySQLBackendTestFactory.class)
+public class ITMySQLPersist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLVersionStore.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLVersionStore.java
new file mode 100644
index 00000000000..e8a6ad779f0
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITMySQLVersionStore.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
+import org.projectnessie.versioned.storage.jdbc2tests.MySQLBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(MySQLBackendTestFactory.class)
+public class ITMySQLVersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLBackendFactory.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLBackendFactory.java
new file mode 100644
index 00000000000..7002084d5b7
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLBackendFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.jdbc2tests.AbstractJdbc2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.PostgreSQLBackendTestFactory;
+
+public class ITPostgreSQLBackendFactory extends AbstractTestJdbc2BackendFactory {
+
+ @Override
+ protected AbstractJdbc2BackendTestFactory testFactory() {
+ return new PostgreSQLBackendTestFactory();
+ }
+}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLPersist.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLPersist.java
new file mode 100644
index 00000000000..962a93dcf49
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLPersist.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
+import org.projectnessie.versioned.storage.jdbc2tests.PostgreSQLBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(PostgreSQLBackendTestFactory.class)
+public class ITPostgreSQLPersist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLVersionStore.java b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLVersionStore.java
new file mode 100644
index 00000000000..c98a8fcfc57
--- /dev/null
+++ b/versioned/storage/jdbc2/src/intTest/java/org/projectnessie/versioned/storage/jdbc2/ITPostgreSQLVersionStore.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
+import org.projectnessie.versioned.storage.jdbc2tests.PostgreSQLBackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(PostgreSQLBackendTestFactory.class)
+public class ITPostgreSQLVersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/AbstractJdbc2Persist.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/AbstractJdbc2Persist.java
new file mode 100644
index 00000000000..49d243caae5
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/AbstractJdbc2Persist.java
@@ -0,0 +1,667 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Arrays.stream;
+import static org.projectnessie.versioned.storage.common.persist.ObjTypes.objTypeByName;
+import static org.projectnessie.versioned.storage.common.util.Closing.closeMultiple;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2Serde.deserializeObjId;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2Serde.serializeObjId;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.ADD_REFERENCE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_ID;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_VALUE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_VERS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.DELETE_OBJ;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.DELETE_OBJ_CONDITIONAL;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.FETCH_OBJ_TYPE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.FIND_OBJS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.FIND_OBJS_TYPED;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.FIND_REFERENCES;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.MARK_REFERENCE_AS_DELETED;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.MAX_BATCH_SIZE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.PURGE_REFERENCE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.REFS_CREATED_AT_COND;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.REFS_EXTENDED_INFO_COND;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.SCAN_OBJS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.STORE_OBJ;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.UPDATE_REFERENCE_POINTER;
+import static org.projectnessie.versioned.storage.serialize.ProtoSerialization.serializeObj;
+import static org.projectnessie.versioned.storage.serialize.ProtoSerialization.serializePreviousPointers;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.AbstractIterator;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import java.lang.reflect.Array;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.agrona.collections.Hashing;
+import org.agrona.collections.Int2IntHashMap;
+import org.agrona.collections.Object2IntHashMap;
+import org.projectnessie.versioned.storage.common.config.StoreConfig;
+import org.projectnessie.versioned.storage.common.exceptions.ObjNotFoundException;
+import org.projectnessie.versioned.storage.common.exceptions.ObjTooLargeException;
+import org.projectnessie.versioned.storage.common.exceptions.RefAlreadyExistsException;
+import org.projectnessie.versioned.storage.common.exceptions.RefConditionFailedException;
+import org.projectnessie.versioned.storage.common.exceptions.RefNotFoundException;
+import org.projectnessie.versioned.storage.common.objtypes.UpdateableObj;
+import org.projectnessie.versioned.storage.common.persist.CloseableIterator;
+import org.projectnessie.versioned.storage.common.persist.Obj;
+import org.projectnessie.versioned.storage.common.persist.ObjId;
+import org.projectnessie.versioned.storage.common.persist.ObjType;
+import org.projectnessie.versioned.storage.common.persist.Persist;
+import org.projectnessie.versioned.storage.common.persist.Reference;
+import org.projectnessie.versioned.storage.serialize.ProtoSerialization;
+
+@SuppressWarnings({"SqlDialectInspection", "SqlNoDataSourceInspection"})
+abstract class AbstractJdbc2Persist implements Persist {
+
+ private final StoreConfig config;
+ private final DatabaseSpecific databaseSpecific;
+
+ AbstractJdbc2Persist(DatabaseSpecific databaseSpecific, StoreConfig config) {
+ this.config = config;
+ this.databaseSpecific = databaseSpecific;
+ }
+
+ @Nonnull
+ @Override
+ public String name() {
+ return Jdbc2BackendFactory.NAME;
+ }
+
+ @Override
+ @Nonnull
+ public StoreConfig config() {
+ return config;
+ }
+
+ protected final Reference findReference(@Nonnull Connection conn, @Nonnull String name) {
+ return findReferences(conn, new String[] {name})[0];
+ }
+
+ @Nonnull
+ protected final Reference[] findReferences(@Nonnull Connection conn, @Nonnull String[] names) {
+ Object2IntHashMap nameToIndex =
+ new Object2IntHashMap<>(200, Hashing.DEFAULT_LOAD_FACTOR, -1);
+ Reference[] r = new Reference[names.length];
+ List keys = new ArrayList<>();
+ for (int i = 0; i < names.length; i++) {
+ String name = names[i];
+ if (name != null) {
+ keys.add(name);
+ nameToIndex.put(name, i);
+ }
+ }
+
+ if (keys.isEmpty()) {
+ return r;
+ }
+
+ try (PreparedStatement ps =
+ conn.prepareStatement(sqlSelectMultiple(FIND_REFERENCES, keys.size()))) {
+ int idx = 1;
+ ps.setString(idx++, config.repositoryId());
+ for (String key : keys) {
+ ps.setString(idx++, key);
+ }
+ try (ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ Reference ref = Jdbc2Serde.deserializeReference(rs);
+ int i = nameToIndex.getValue(ref.name());
+ if (i != -1) {
+ r[i] = ref;
+ }
+ }
+ return r;
+ }
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ @Nonnull
+ protected final Reference addReference(@Nonnull Connection conn, @Nonnull Reference reference)
+ throws RefAlreadyExistsException {
+ checkArgument(!reference.deleted(), "Deleted references must not be added");
+
+ String sql = databaseSpecific.wrapInsert(ADD_REFERENCE);
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ ps.setString(1, config.repositoryId());
+ ps.setString(2, reference.name());
+ serializeObjId(ps, 3, reference.pointer(), databaseSpecific);
+ ps.setBoolean(4, reference.deleted());
+ if (reference.createdAtMicros() != 0L) {
+ ps.setLong(5, reference.createdAtMicros());
+ } else {
+ ps.setNull(5, Types.BIGINT);
+ }
+ serializeObjId(ps, 6, reference.extendedInfoObj(), databaseSpecific);
+ byte[] previous = serializePreviousPointers(reference.previousPointers());
+ if (previous != null) {
+ ps.setBytes(7, previous);
+ } else {
+ ps.setNull(7, Types.BINARY);
+ }
+
+ if (ps.executeUpdate() != 1) {
+ throw new RefAlreadyExistsException(fetchReference(reference.name()));
+ }
+
+ return reference;
+ } catch (SQLException e) {
+ if (databaseSpecific.isConstraintViolation(e)) {
+ throw new RefAlreadyExistsException(fetchReference(reference.name()));
+ }
+ throw unhandledSQLException(e);
+ }
+ }
+
+ @Nonnull
+ protected final Reference markReferenceAsDeleted(
+ @Nonnull Connection conn, @Nonnull Reference reference)
+ throws RefNotFoundException, RefConditionFailedException {
+ try (PreparedStatement ps =
+ conn.prepareStatement(referencesDml(MARK_REFERENCE_AS_DELETED, reference))) {
+ int idx = 1;
+ ps.setBoolean(idx++, true);
+ ps.setString(idx++, config().repositoryId());
+ ps.setString(idx++, reference.name());
+ serializeObjId(ps, idx++, reference.pointer(), databaseSpecific);
+ ps.setBoolean(idx++, false);
+ long createdAtMicros = reference.createdAtMicros();
+ if (createdAtMicros != 0L) {
+ ps.setLong(idx++, createdAtMicros);
+ }
+ ObjId extendedInfoObj = reference.extendedInfoObj();
+ if (extendedInfoObj != null) {
+ serializeObjId(ps, idx, extendedInfoObj, databaseSpecific);
+ }
+
+ if (ps.executeUpdate() != 1) {
+ Reference ref = findReference(conn, reference.name());
+ if (ref == null) {
+ throw new RefNotFoundException(reference);
+ }
+ throw new RefConditionFailedException(ref);
+ }
+
+ return reference.withDeleted(true);
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ protected final void purgeReference(@Nonnull Connection conn, @Nonnull Reference reference)
+ throws RefNotFoundException, RefConditionFailedException {
+ try (PreparedStatement ps = conn.prepareStatement(referencesDml(PURGE_REFERENCE, reference))) {
+ int idx = 1;
+ ps.setString(idx++, config().repositoryId());
+ ps.setString(idx++, reference.name());
+ serializeObjId(ps, idx++, reference.pointer(), databaseSpecific);
+ ps.setBoolean(idx++, true);
+ long createdAtMicros = reference.createdAtMicros();
+ if (createdAtMicros != 0L) {
+ ps.setLong(idx++, createdAtMicros);
+ }
+ ObjId extendedInfoObj = reference.extendedInfoObj();
+ if (extendedInfoObj != null) {
+ serializeObjId(ps, idx, extendedInfoObj, databaseSpecific);
+ }
+
+ if (ps.executeUpdate() != 1) {
+ Reference ref = findReference(conn, reference.name());
+ if (ref == null) {
+ throw new RefNotFoundException(reference);
+ }
+ throw new RefConditionFailedException(ref);
+ }
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ @Nonnull
+ protected final Reference updateReferencePointer(
+ @Nonnull Connection conn, @Nonnull Reference reference, @Nonnull ObjId newPointer)
+ throws RefNotFoundException, RefConditionFailedException {
+ try (PreparedStatement ps =
+ conn.prepareStatement(referencesDml(UPDATE_REFERENCE_POINTER, reference))) {
+ int idx = 1;
+ serializeObjId(ps, idx++, newPointer, databaseSpecific);
+ Reference updated = reference.forNewPointer(newPointer, config);
+ byte[] previous = serializePreviousPointers(updated.previousPointers());
+ if (previous != null) {
+ ps.setBytes(idx++, previous);
+ } else {
+ ps.setNull(idx++, Types.BINARY);
+ }
+
+ ps.setString(idx++, config().repositoryId());
+ ps.setString(idx++, reference.name());
+ serializeObjId(ps, idx++, reference.pointer(), databaseSpecific);
+ ps.setBoolean(idx++, false);
+ long createdAtMicros = reference.createdAtMicros();
+ if (createdAtMicros != 0L) {
+ ps.setLong(idx++, createdAtMicros);
+ }
+ ObjId extendedInfoObj = reference.extendedInfoObj();
+ if (extendedInfoObj != null) {
+ serializeObjId(ps, idx, extendedInfoObj, databaseSpecific);
+ }
+
+ if (ps.executeUpdate() != 1) {
+ Reference ref = findReference(conn, reference.name());
+ if (ref == null) {
+ throw new RefNotFoundException(reference);
+ }
+ throw new RefConditionFailedException(ref);
+ }
+
+ return updated;
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ private String referencesDml(String sql, Reference reference) {
+ String createdAtCond = reference.createdAtMicros() != 0L ? "=?" : " IS NULL";
+ String extendedInfoCond = reference.extendedInfoObj() != null ? "=?" : " IS NULL";
+ return sql.replace(REFS_CREATED_AT_COND, createdAtCond)
+ .replace(REFS_EXTENDED_INFO_COND, extendedInfoCond);
+ }
+
+ protected T fetchTypedObj(
+ Connection conn, ObjId id, ObjType type, Class typeClass) throws ObjNotFoundException {
+ T obj = fetchTypedObjsIfExist(conn, new ObjId[] {id}, type, typeClass)[0];
+
+ if (obj == null) {
+ throw new ObjNotFoundException(id);
+ }
+
+ return obj;
+ }
+
+ protected ObjType fetchObjType(@Nonnull Connection conn, @Nonnull ObjId id)
+ throws ObjNotFoundException {
+ try (PreparedStatement ps = conn.prepareStatement(sqlSelectMultiple(FETCH_OBJ_TYPE, 1))) {
+ ps.setString(1, config.repositoryId());
+ serializeObjId(ps, 2, id, databaseSpecific);
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ String objType = rs.getString(1);
+ return objTypeByName(objType);
+ }
+ }
+ throw new ObjNotFoundException(id);
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ @Nonnull
+ protected final T[] fetchTypedObjsIfExist(
+ @Nonnull Connection conn,
+ @Nonnull ObjId[] ids,
+ ObjType type,
+ @SuppressWarnings("unused") Class typeClass) {
+ Object2IntHashMap idToIndex =
+ new Object2IntHashMap<>(200, Hashing.DEFAULT_LOAD_FACTOR, -1);
+ @SuppressWarnings("unchecked")
+ T[] r = (T[]) Array.newInstance(typeClass, ids.length);
+ List keys = new ArrayList<>();
+ for (int i = 0; i < ids.length; i++) {
+ ObjId id = ids[i];
+ if (id != null) {
+ keys.add(id);
+ idToIndex.put(id, i);
+ }
+ }
+
+ if (keys.isEmpty()) {
+ return r;
+ }
+
+ String sql = type == null ? FIND_OBJS : FIND_OBJS_TYPED;
+ sql = sqlSelectMultiple(sql, keys.size());
+
+ try (PreparedStatement ps = conn.prepareStatement(sql)) {
+ int idx = 1;
+ ps.setString(idx++, config.repositoryId());
+ for (ObjId key : keys) {
+ serializeObjId(ps, idx++, key, databaseSpecific);
+ }
+ if (type != null) {
+ ps.setString(idx, type.shortName());
+ }
+
+ try (ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ Obj obj = deserializeObj(rs);
+ int i = idToIndex.getValue(obj.id());
+ if (i != -1) {
+ @SuppressWarnings("unchecked")
+ T typed = (T) obj;
+ r[i] = typed;
+ }
+ }
+
+ return r;
+ }
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ private Obj deserializeObj(ResultSet rs) throws SQLException {
+ ObjId id = deserializeObjId(rs, COL_OBJ_ID);
+ String versionToken = rs.getString(COL_OBJ_VERS);
+ byte[] serialized = rs.getBytes(COL_OBJ_VALUE);
+ return ProtoSerialization.deserializeObj(id, serialized, versionToken);
+ }
+
+ protected final boolean storeObj(
+ @Nonnull Connection conn, @Nonnull Obj obj, boolean ignoreSoftSizeRestrictions)
+ throws ObjTooLargeException {
+ return upsertObjs(conn, new Obj[] {obj}, ignoreSoftSizeRestrictions, true)[0];
+ }
+
+ @Nonnull
+ protected final boolean[] storeObjs(@Nonnull Connection conn, @Nonnull Obj[] objs)
+ throws ObjTooLargeException {
+ return upsertObjs(conn, objs, false, true);
+ }
+
+ protected final Void updateObj(@Nonnull Connection conn, @Nonnull Obj obj)
+ throws ObjTooLargeException {
+ updateObjs(conn, new Obj[] {obj});
+ return null;
+ }
+
+ protected final Void updateObjs(@Nonnull Connection conn, @Nonnull Obj[] objs)
+ throws ObjTooLargeException {
+ upsertObjs(conn, objs, false, false);
+ return null;
+ }
+
+ protected final boolean deleteConditional(@Nonnull Connection conn, @Nonnull UpdateableObj obj) {
+ try (PreparedStatement ps = conn.prepareStatement(DELETE_OBJ_CONDITIONAL)) {
+ ps.setString(1, config.repositoryId());
+ serializeObjId(ps, 2, obj.id(), databaseSpecific);
+ ps.setString(3, obj.type().shortName());
+ ps.setString(4, obj.versionToken());
+ return ps.executeUpdate() == 1;
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ protected final boolean updateConditional(
+ @Nonnull Connection conn, @Nonnull UpdateableObj expected, @Nonnull UpdateableObj newValue)
+ throws ObjTooLargeException {
+ ObjId id = expected.id();
+ checkArgument(id != null && id.equals(newValue.id()));
+ checkArgument(expected.type().equals(newValue.type()));
+ checkArgument(!expected.versionToken().equals(newValue.versionToken()));
+
+ // See comment in upsertObjs() why this is implemented this way.
+ return deleteConditional(conn, expected)
+ && upsertObjs(conn, new Obj[] {newValue}, false, true)[0];
+ }
+
+ @Nonnull
+ private boolean[] upsertObjs(
+ @Nonnull Connection conn,
+ @Nonnull Obj[] objs,
+ boolean ignoreSoftSizeRestrictions,
+ boolean insert)
+ throws ObjTooLargeException {
+ if (!insert) {
+ // Sadly an INSERT INTO ... ON CONFLICT DO UPDATE SET ... does not work with parameters in the
+ // UPDATE SET clause. Since the JDBC connection is configured with auto-commit=false, we can
+ // just DELETE the updates to be upserted and INSERT them again.
+ deleteObjs(
+ conn, stream(objs).map(obj -> obj == null ? null : obj.id()).toArray(ObjId[]::new));
+ }
+
+ try (PreparedStatement ps = conn.prepareStatement(databaseSpecific.wrapInsert(STORE_OBJ))) {
+ boolean[] r = new boolean[objs.length];
+
+ Int2IntHashMap batchIndexToObjIndex =
+ new Int2IntHashMap(objs.length * 2, Hashing.DEFAULT_LOAD_FACTOR, -1);
+
+ Consumer batchResultHandler =
+ updated -> {
+ for (int i = 0; i < updated.length; i++) {
+ if (updated[i] == 1) {
+ r[batchIndexToObjIndex.get(i)] = true;
+ } else if (updated[i] != 0) {
+ throw new IllegalStateException(
+ "driver returned unexpected value for a batch update: " + updated[i]);
+ }
+ }
+ };
+
+ int batchIndex = 0;
+ for (int i = 0; i < objs.length; i++) {
+ Obj obj = objs[i];
+ if (obj == null) {
+ continue;
+ }
+
+ ObjId id = obj.id();
+ ObjType type = obj.type();
+
+ int incrementalIndexSizeLimit =
+ ignoreSoftSizeRestrictions ? Integer.MAX_VALUE : effectiveIncrementalIndexSizeLimit();
+ int indexSizeLimit =
+ ignoreSoftSizeRestrictions ? Integer.MAX_VALUE : effectiveIndexSegmentSizeLimit();
+
+ checkArgument(id != null, "Obj to store must have a non-null ID");
+ // INSERT INTO objs2 (repo, obj_id, obj_type, obj_vers, obj_value) VALUES (?, ?, ?, ?, ?) ON
+ // CONFLICT DO NOTHING
+ ps.setString(1, config.repositoryId());
+ serializeObjId(ps, 2, id, databaseSpecific);
+ ps.setString(3, type.shortName());
+ Optional versionToken = UpdateableObj.extractVersionToken(obj);
+ if (versionToken.isPresent()) {
+ ps.setString(4, versionToken.get());
+ } else {
+ ps.setNull(4, Types.VARCHAR);
+ }
+ byte[] serialized = serializeObj(obj, incrementalIndexSizeLimit, indexSizeLimit, false);
+ ps.setBytes(5, serialized);
+
+ batchIndexToObjIndex.put(batchIndex++, i);
+ ps.addBatch();
+
+ if (batchIndex == MAX_BATCH_SIZE) {
+ batchIndex = 0;
+ batchResultHandler.accept(ps.executeBatch());
+ }
+ }
+
+ if (batchIndex > 0) {
+ batchResultHandler.accept(ps.executeBatch());
+ }
+
+ return r;
+ } catch (SQLException e) {
+ if (databaseSpecific.isConstraintViolation(e)) {
+ throw new UnsupportedOperationException(
+ "The database should support a functionality like PostgreSQL's "
+ + "'ON CONFLICT DO NOTHING' for INSERT statements. For H2, enable the "
+ + "PostgreSQL Compatibility Mode.");
+ }
+ throw unhandledSQLException(e);
+ }
+ }
+
+ protected final void deleteObj(@Nonnull Connection conn, @Nonnull ObjId id) {
+ try (PreparedStatement ps = conn.prepareStatement(DELETE_OBJ)) {
+ ps.setString(1, config.repositoryId());
+ serializeObjId(ps, 2, id, databaseSpecific);
+
+ ps.executeUpdate();
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ protected final void deleteObjs(@Nonnull Connection conn, @Nonnull ObjId[] ids) {
+ if (ids.length == 0) {
+ return;
+ }
+
+ try (PreparedStatement ps = conn.prepareStatement(DELETE_OBJ)) {
+ int batchSize = 0;
+
+ for (ObjId id : ids) {
+ if (id == null) {
+ continue;
+ }
+ ps.setString(1, config.repositoryId());
+ serializeObjId(ps, 2, id, databaseSpecific);
+ ps.addBatch();
+
+ if (++batchSize == MAX_BATCH_SIZE) {
+ batchSize = 0;
+ ps.executeBatch();
+ }
+ }
+
+ if (batchSize > 0) {
+ ps.executeBatch();
+ }
+
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ protected CloseableIterator scanAllObjects(Connection conn, Set returnedObjTypes) {
+ return new ScanAllObjectsIterator(conn, returnedObjTypes);
+ }
+
+ @VisibleForTesting
+ static String sqlSelectMultiple(String sql, int count) {
+ if (count == 1) {
+ return sql;
+ }
+ StringBuilder marks = new StringBuilder(sql.length() + 50);
+ int idx = sql.indexOf("(?)");
+ checkArgument(idx > 0, "SQL does not contain (?) placeholder: %s", sql);
+ marks.append(sql, 0, idx).append("(?");
+ for (int i = 1; i < count; i++) {
+ marks.append(",?");
+ }
+ marks.append(')').append(sql, idx + 3, sql.length());
+ return marks.toString();
+ }
+
+ @FunctionalInterface
+ interface ThrowingConsumer {
+ void accept(T t) throws SQLException;
+ }
+
+ private class ScanAllObjectsIterator extends ResultSetIterator {
+ ScanAllObjectsIterator(Connection conn, Set returnedObjTypes) {
+ super(
+ conn,
+ sqlSelectMultiple(SCAN_OBJS, returnedObjTypes.size()),
+ ps -> {
+ int idx = 1;
+ ps.setString(idx++, config.repositoryId());
+ for (ObjType returnedObjType : returnedObjTypes) {
+ ps.setString(idx++, returnedObjType.shortName());
+ }
+ });
+ }
+
+ @Override
+ protected Obj mapToObj(ResultSet rs) throws SQLException {
+ return deserializeObj(rs);
+ }
+ }
+
+ private abstract class ResultSetIterator extends AbstractIterator
+ implements CloseableIterator {
+
+ private final Connection conn;
+ private final PreparedStatement ps;
+ private final ResultSet rs;
+
+ @SuppressWarnings("SqlSourceToSinkFlow")
+ ResultSetIterator(Connection conn, String sql, ThrowingConsumer preparer) {
+ this.conn = conn;
+
+ try {
+ ps = conn.prepareStatement(sql);
+ preparer.accept(ps);
+ rs = ps.executeQuery();
+ } catch (SQLException e) {
+ try {
+ close();
+ } catch (Exception ex) {
+ e.addSuppressed(ex);
+ }
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ List c = new ArrayList<>();
+ c.add(rs);
+ c.add(ps);
+ c.add(conn);
+ try {
+ closeMultiple(c);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Nullable
+ @Override
+ protected R computeNext() {
+ try {
+ if (!rs.next()) {
+ return endOfData();
+ }
+
+ return mapToObj(rs);
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ protected abstract R mapToObj(ResultSet rs) throws SQLException;
+ }
+
+ protected RuntimeException unhandledSQLException(SQLException e) {
+ return Jdbc2Backend.unhandledSQLException(databaseSpecific, e);
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/DatabaseSpecific.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/DatabaseSpecific.java
new file mode 100644
index 00000000000..1b4bafcbe35
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/DatabaseSpecific.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import java.sql.SQLException;
+import java.util.Map;
+
+public interface DatabaseSpecific {
+
+ Map columnTypes();
+
+ Map columnTypeIds();
+
+ boolean isConstraintViolation(SQLException e);
+
+ boolean isRetryTransaction(SQLException e);
+
+ boolean isAlreadyExists(SQLException e);
+
+ String wrapInsert(String sql);
+
+ String primaryKeyCol(String col, Jdbc2ColumnType columnType);
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/DatabaseSpecifics.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/DatabaseSpecifics.java
new file mode 100644
index 00000000000..bdc42289b1c
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/DatabaseSpecifics.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import jakarta.annotation.Nonnull;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.EnumMap;
+import java.util.Locale;
+import java.util.Map;
+import javax.sql.DataSource;
+
+public final class DatabaseSpecifics {
+ private DatabaseSpecifics() {}
+
+ // Use 'ucs_basic' collation for PostgreSQL, otherwise multiple spaces would be collapses and
+ // result in wrong reference listings. Assume the following reference names
+ // 'ref- 1'
+ // 'ref- 2'
+ // 'ref- 3'
+ // 'ref- 8'
+ // 'ref- 9'
+ // 'ref- 10'
+ // 'ref- 11'
+ // 'ref- 19'
+ // 'ref- 20'
+ // 'ref- 21'
+ // With ucs_basic, the above (expected) order is maintained, but the default behavior could
+ // choose a collation in which 'ref- 2' is sorted _after_ 'ref- 19', which is unexpected
+ // and wrong for Nessie.
+ public static final DatabaseSpecific POSTGRESQL_DATABASE_SPECIFIC =
+ new BasePostgresDatabaseSpecific("VARCHAR COLLATE ucs_basic", Types.BINARY);
+
+ public static final DatabaseSpecific COCKROACH_DATABASE_SPECIFIC =
+ new BasePostgresDatabaseSpecific("VARCHAR", Types.VARBINARY);
+
+ public static final DatabaseSpecific H2_DATABASE_SPECIFIC =
+ new BasePostgresDatabaseSpecific("VARCHAR", Types.VARBINARY);
+
+ public static final DatabaseSpecific MARIADB_DATABASE_SPECIFIC = new MariaDBDatabaseSpecific();
+
+ public static DatabaseSpecific detect(DataSource dataSource) {
+ try (Connection conn = dataSource.getConnection()) {
+ return detect(conn);
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Nonnull
+ private static DatabaseSpecific detect(Connection conn) {
+ try {
+ String productName = conn.getMetaData().getDatabaseProductName().toLowerCase(Locale.ROOT);
+ switch (productName) {
+ case "h2":
+ return H2_DATABASE_SPECIFIC;
+ case "postgresql":
+ try (ResultSet rs = conn.getMetaData().getSchemas(conn.getCatalog(), "crdb_internal")) {
+ if (rs.next()) {
+ return COCKROACH_DATABASE_SPECIFIC;
+ } else {
+ return POSTGRESQL_DATABASE_SPECIFIC;
+ }
+ }
+ case "mysql":
+ case "mariadb":
+ return MARIADB_DATABASE_SPECIFIC;
+ default:
+ throw new IllegalStateException(
+ "Could not select specifics to use for database product '" + productName + "'");
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ static class BasePostgresDatabaseSpecific implements DatabaseSpecific {
+
+ /** Integrity constraint violation error code, as returned by H2, Postgres & Cockroach. */
+ private static final String CONSTRAINT_VIOLATION_SQL_CODE = "23505";
+
+ /** Deadlock error, returned by Postgres. */
+ private static final String DEADLOCK_SQL_STATE_POSTGRES = "40P01";
+
+ /** Already exists error, returned by Postgres, H2 and Cockroach. */
+ private static final String ALREADY_EXISTS_STATE_POSTGRES = "42P07";
+
+ /**
+ * Cockroach "retry, write too old" error, see Cockroach's
+ * Transaction Retry Error Reference, and Postgres may return a "deadlock" error.
+ */
+ private static final String RETRY_SQL_STATE_COCKROACH = "40001";
+
+ private final Map typeMap;
+ private final Map typeIdMap;
+
+ BasePostgresDatabaseSpecific(String varcharType, int objIdType) {
+ typeMap = new EnumMap<>(Jdbc2ColumnType.class);
+ typeIdMap = new EnumMap<>(Jdbc2ColumnType.class);
+ typeMap.put(Jdbc2ColumnType.NAME, varcharType);
+ typeIdMap.put(Jdbc2ColumnType.NAME, Types.VARCHAR);
+ typeMap.put(Jdbc2ColumnType.OBJ_ID, "BYTEA");
+ typeIdMap.put(Jdbc2ColumnType.OBJ_ID, objIdType);
+ typeMap.put(Jdbc2ColumnType.BOOL, "BOOLEAN");
+ typeIdMap.put(Jdbc2ColumnType.BOOL, Types.BOOLEAN);
+ typeMap.put(Jdbc2ColumnType.VARBINARY, "BYTEA");
+ typeIdMap.put(Jdbc2ColumnType.VARBINARY, Types.BINARY);
+ typeMap.put(Jdbc2ColumnType.BIGINT, "BIGINT");
+ typeIdMap.put(Jdbc2ColumnType.BIGINT, Types.BIGINT);
+ typeMap.put(Jdbc2ColumnType.VARCHAR, varcharType);
+ typeIdMap.put(Jdbc2ColumnType.VARCHAR, Types.VARCHAR);
+ }
+
+ @Override
+ public Map columnTypes() {
+ return typeMap;
+ }
+
+ @Override
+ public Map columnTypeIds() {
+ return typeIdMap;
+ }
+
+ @Override
+ public boolean isConstraintViolation(SQLException e) {
+ return CONSTRAINT_VIOLATION_SQL_CODE.equals(e.getSQLState());
+ }
+
+ @Override
+ public boolean isRetryTransaction(SQLException e) {
+ if (e.getSQLState() == null) {
+ return false;
+ }
+ switch (e.getSQLState()) {
+ case DEADLOCK_SQL_STATE_POSTGRES:
+ case RETRY_SQL_STATE_COCKROACH:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isAlreadyExists(SQLException e) {
+ return ALREADY_EXISTS_STATE_POSTGRES.equals(e.getSQLState());
+ }
+
+ @Override
+ public String wrapInsert(String sql) {
+ return sql + " ON CONFLICT DO NOTHING";
+ }
+
+ @Override
+ public String primaryKeyCol(String col, Jdbc2ColumnType columnType) {
+ return col;
+ }
+ }
+
+ static class MariaDBDatabaseSpecific implements DatabaseSpecific {
+
+ private static final String OBJ_ID = "TINYBLOB";
+ private static final String VARCHAR = "VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin";
+
+ private static final String MYSQL_CONSTRAINT_VIOLATION_SQL_STATE = "23000";
+ private static final String MYSQL_LOCK_DEADLOCK_SQL_STATE = "40001";
+ private static final String MYSQL_ALREADY_EXISTS_SQL_STATE = "42S01";
+
+ private final Map typeMap;
+ private final Map typeIdMap;
+
+ MariaDBDatabaseSpecific() {
+ typeMap = new EnumMap<>(Jdbc2ColumnType.class);
+ typeIdMap = new EnumMap<>(Jdbc2ColumnType.class);
+ typeMap.put(Jdbc2ColumnType.NAME, VARCHAR);
+ typeIdMap.put(Jdbc2ColumnType.NAME, Types.VARCHAR);
+ typeMap.put(Jdbc2ColumnType.OBJ_ID, OBJ_ID);
+ typeIdMap.put(Jdbc2ColumnType.OBJ_ID, Types.VARBINARY);
+ typeMap.put(Jdbc2ColumnType.BOOL, "BIT(1)");
+ typeIdMap.put(Jdbc2ColumnType.BOOL, Types.BIT);
+ typeMap.put(Jdbc2ColumnType.VARBINARY, "BLOB");
+ typeIdMap.put(Jdbc2ColumnType.VARBINARY, Types.BLOB);
+ typeMap.put(Jdbc2ColumnType.BIGINT, "BIGINT");
+ typeIdMap.put(Jdbc2ColumnType.BIGINT, Types.BIGINT);
+ typeMap.put(Jdbc2ColumnType.VARCHAR, VARCHAR);
+ typeIdMap.put(Jdbc2ColumnType.VARCHAR, Types.VARCHAR);
+ }
+
+ @Override
+ public Map columnTypes() {
+ return typeMap;
+ }
+
+ @Override
+ public Map columnTypeIds() {
+ return typeIdMap;
+ }
+
+ @Override
+ public boolean isConstraintViolation(SQLException e) {
+ return MYSQL_CONSTRAINT_VIOLATION_SQL_STATE.equals(e.getSQLState());
+ }
+
+ @Override
+ public boolean isRetryTransaction(SQLException e) {
+ return MYSQL_LOCK_DEADLOCK_SQL_STATE.equals(e.getSQLState());
+ }
+
+ @Override
+ public boolean isAlreadyExists(SQLException e) {
+ return MYSQL_ALREADY_EXISTS_SQL_STATE.equals(e.getSQLState());
+ }
+
+ @Override
+ public String wrapInsert(String sql) {
+ return sql.replace("INSERT INTO", "INSERT IGNORE INTO");
+ }
+
+ @Override
+ public String primaryKeyCol(String col, Jdbc2ColumnType columnType) {
+ switch (columnType) {
+ case OBJ_ID:
+ return col + "(255)";
+ default:
+ return col;
+ }
+ }
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Backend.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Backend.java
new file mode 100644
index 00000000000..a0b4ddd5e0e
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Backend.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static org.projectnessie.versioned.storage.jdbc2.AbstractJdbc2Persist.sqlSelectMultiple;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2ColumnType.BIGINT;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2ColumnType.BOOL;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2ColumnType.NAME;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2ColumnType.OBJ_ID;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2ColumnType.VARBINARY;
+import static org.projectnessie.versioned.storage.jdbc2.Jdbc2ColumnType.VARCHAR;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_ID;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_TYPE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_VALUE;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_OBJ_VERS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_CREATED_AT;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_DELETED;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_EXTENDED_INFO;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_NAME;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_POINTER;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_PREVIOUS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REPO_ID;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.ERASE_OBJS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.ERASE_REFS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.TABLE_OBJS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.TABLE_REFS;
+
+import com.google.common.collect.ImmutableMap;
+import jakarta.annotation.Nonnull;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.sql.DataSource;
+import org.projectnessie.versioned.storage.common.exceptions.UnknownOperationResultException;
+import org.projectnessie.versioned.storage.common.persist.Backend;
+import org.projectnessie.versioned.storage.common.persist.PersistFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class Jdbc2Backend implements Backend {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Jdbc2Backend.class);
+
+ private static final int MAX_CREATE_TABLE_RECURSION_DEPTH = 3;
+
+ private final DatabaseSpecific databaseSpecific;
+ private final DataSource dataSource;
+ private final boolean closeDataSource;
+ private final String createTableRefsSql;
+ private final String createTableObjsSql;
+
+ @SuppressWarnings("removal")
+ public Jdbc2Backend(
+ @Nonnull Jdbc2BackendConfig config,
+ @Nonnull DatabaseSpecific databaseSpecific,
+ boolean closeDataSource) {
+ this.dataSource = config.dataSource();
+ this.databaseSpecific = databaseSpecific;
+ this.closeDataSource = closeDataSource;
+ createTableRefsSql = buildCreateTableRefsSql(databaseSpecific);
+ createTableObjsSql = buildCreateTableObjsSql(databaseSpecific);
+ if (config.catalog().isPresent()) {
+ LOGGER.warn(
+ "Configuration 'nessie.version.store.persist.jdbc.catalog' is now obsolete, please remove it. "
+ + "The catalog must be specified directly in the JDBC URL using the option 'quarkus.datasource.{}.jdbc.url'",
+ config.datasourceName().orElse("postgresql"));
+ }
+ if (config.schema().isPresent()) {
+ LOGGER.warn(
+ "Configuration 'nessie.version.store.persist.jdbc.schema' is now obsolete, please remove it. "
+ + "The schema must be specified directly in the JDBC URL using the option 'quarkus.datasource.{}.jdbc.url'",
+ config.datasourceName().orElse("postgresql"));
+ }
+ }
+
+ private String buildCreateTableRefsSql(DatabaseSpecific databaseSpecific) {
+ Map columnTypes = databaseSpecific.columnTypes();
+ return "CREATE TABLE "
+ + TABLE_REFS
+ + "\n (\n "
+ + COL_REPO_ID
+ + " "
+ + columnTypes.get(NAME)
+ + ",\n "
+ + COL_REFS_NAME
+ + " "
+ + columnTypes.get(NAME)
+ + ",\n "
+ + COL_REFS_POINTER
+ + " "
+ + columnTypes.get(OBJ_ID)
+ + ",\n "
+ + COL_REFS_DELETED
+ + " "
+ + columnTypes.get(BOOL)
+ + ",\n "
+ + COL_REFS_CREATED_AT
+ + " "
+ + columnTypes.get(BIGINT)
+ + " DEFAULT 0,\n "
+ + COL_REFS_EXTENDED_INFO
+ + " "
+ + columnTypes.get(OBJ_ID)
+ + ",\n "
+ + COL_REFS_PREVIOUS
+ + " "
+ + columnTypes.get(VARBINARY)
+ + ",\n PRIMARY KEY ("
+ + COL_REPO_ID
+ + ", "
+ + COL_REFS_NAME
+ + ")\n )";
+ }
+
+ private String buildCreateTableObjsSql(DatabaseSpecific databaseSpecific) {
+ Map columnTypes = databaseSpecific.columnTypes();
+ return "CREATE TABLE "
+ + TABLE_OBJS
+ + "\n (\n "
+ + COL_REPO_ID
+ + " "
+ + columnTypes.get(NAME)
+ + ",\n "
+ + COL_OBJ_ID
+ + " "
+ + columnTypes.get(OBJ_ID)
+ + ",\n "
+ + COL_OBJ_TYPE
+ + " "
+ + columnTypes.get(NAME)
+ + ",\n "
+ + COL_OBJ_VERS
+ + " "
+ + columnTypes.get(VARCHAR)
+ + ",\n "
+ + COL_OBJ_VALUE
+ + " "
+ + columnTypes.get(VARBINARY)
+ + ",\n PRIMARY KEY ("
+ + COL_REPO_ID
+ + ", "
+ + databaseSpecific.primaryKeyCol(COL_OBJ_ID, OBJ_ID)
+ + ")\n )";
+ }
+
+ DatabaseSpecific databaseSpecific() {
+ return databaseSpecific;
+ }
+
+ @Override
+ public void close() {
+ if (closeDataSource) {
+ try {
+ if (dataSource instanceof AutoCloseable) {
+ ((AutoCloseable) dataSource).close();
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @Nonnull
+ @Override
+ public PersistFactory createFactory() {
+ return new Jdbc2PersistFactory(this);
+ }
+
+ Connection borrowConnection() throws SQLException {
+ Connection c = dataSource.getConnection();
+ c.setAutoCommit(false);
+ return c;
+ }
+
+ @Override
+ public Optional setupSchema() {
+ try (Connection conn = borrowConnection()) {
+ Integer nameTypeId = databaseSpecific.columnTypeIds().get(NAME);
+ Integer objIdTypeId = databaseSpecific.columnTypeIds().get(OBJ_ID);
+ createTableIfNotExists(
+ 0,
+ conn,
+ TABLE_REFS,
+ createTableRefsSql,
+ Stream.of(
+ COL_REPO_ID,
+ COL_REFS_NAME,
+ COL_REFS_POINTER,
+ COL_REFS_DELETED,
+ COL_REFS_CREATED_AT,
+ COL_REFS_EXTENDED_INFO,
+ COL_REFS_PREVIOUS)
+ .collect(Collectors.toSet()),
+ ImmutableMap.of(COL_REPO_ID, nameTypeId, COL_REFS_NAME, nameTypeId));
+ createTableIfNotExists(
+ 0,
+ conn,
+ TABLE_OBJS,
+ createTableObjsSql,
+ Stream.of(COL_REPO_ID, COL_OBJ_ID, COL_OBJ_TYPE, COL_OBJ_VERS, COL_OBJ_VALUE)
+ .collect(Collectors.toSet()),
+ ImmutableMap.of(COL_REPO_ID, nameTypeId, COL_OBJ_ID, objIdTypeId));
+ StringBuilder info = new StringBuilder();
+ String s = conn.getCatalog();
+ if (s != null && !s.isEmpty()) {
+ info.append("catalog: ").append(s);
+ }
+ s = conn.getSchema();
+ if (s != null && !s.isEmpty()) {
+ if (info.length() > 0) {
+ info.append(", ");
+ }
+ info.append("schema: ").append(s);
+ }
+ return Optional.of(info.toString());
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void createTableIfNotExists(
+ int depth,
+ Connection conn,
+ String tableName,
+ String createTable,
+ Set expectedColumns,
+ Map expectedPrimaryKey)
+ throws SQLException {
+
+ try (Statement st = conn.createStatement()) {
+ if (conn.getMetaData().storesLowerCaseIdentifiers()) {
+ tableName = tableName.toLowerCase(Locale.ROOT);
+ } else if (conn.getMetaData().storesUpperCaseIdentifiers()) {
+ tableName = tableName.toUpperCase(Locale.ROOT);
+ }
+
+ String catalog = conn.getCatalog();
+ String schema = conn.getSchema();
+
+ try (ResultSet rs = conn.getMetaData().getTables(catalog, schema, tableName, null)) {
+ if (rs.next()) {
+ // table already exists
+
+ Map primaryKey = new LinkedHashMap<>();
+ Map columns = new LinkedHashMap<>();
+
+ try (ResultSet cols = conn.getMetaData().getColumns(catalog, schema, tableName, null)) {
+ while (cols.next()) {
+ String colName = cols.getString("COLUMN_NAME").toLowerCase(Locale.ROOT);
+ columns.put(colName, cols.getInt("DATA_TYPE"));
+ }
+ }
+ try (ResultSet cols = conn.getMetaData().getPrimaryKeys(catalog, schema, tableName)) {
+ while (cols.next()) {
+ String colName = cols.getString("COLUMN_NAME").toLowerCase(Locale.ROOT);
+ int colType = columns.get(colName);
+ primaryKey.put(colName.toLowerCase(Locale.ROOT), colType);
+ }
+ }
+
+ checkState(
+ primaryKey.equals(expectedPrimaryKey),
+ "Expected primary key columns %s do not match existing primary key columns %s for table '%s'. DDL template:\n%s",
+ expectedPrimaryKey.keySet(),
+ primaryKey.keySet(),
+ tableName,
+ createTable);
+ Set missingColumns = new HashSet<>(expectedColumns);
+ missingColumns.removeAll(columns.keySet());
+ if (!missingColumns.isEmpty()) {
+ throw new IllegalStateException(
+ format(
+ "The database table %s is missing mandatory columns %s.%nFound columns : %s%nExpected columns : %s%nDDL template:\n%s",
+ tableName,
+ sortedColumnNames(missingColumns),
+ sortedColumnNames(columns.keySet()),
+ sortedColumnNames(expectedColumns),
+ createTable));
+ }
+
+ // Existing table looks compatible
+ return;
+ }
+ }
+
+ try {
+ st.executeUpdate(createTable);
+ } catch (SQLException e) {
+ if (!databaseSpecific.isAlreadyExists(e)) {
+ throw e;
+ }
+
+ if (depth >= MAX_CREATE_TABLE_RECURSION_DEPTH) {
+ throw e;
+ }
+
+ // table was created by another process, try again to check the schema
+ createTableIfNotExists(
+ depth + 1, conn, tableName, createTable, expectedColumns, expectedPrimaryKey);
+ }
+ }
+ }
+
+ private static String sortedColumnNames(Collection> input) {
+ return input.stream().map(Object::toString).sorted().collect(Collectors.joining(","));
+ }
+
+ @Override
+ public void eraseRepositories(Set repositoryIds) {
+ if (repositoryIds == null || repositoryIds.isEmpty()) {
+ return;
+ }
+
+ try (Connection conn = borrowConnection()) {
+
+ try (PreparedStatement ps =
+ conn.prepareStatement(sqlSelectMultiple(ERASE_REFS, repositoryIds.size()))) {
+ int i = 1;
+ for (String repositoryId : repositoryIds) {
+ ps.setString(i++, repositoryId);
+ }
+ ps.executeUpdate();
+ }
+ try (PreparedStatement ps =
+ conn.prepareStatement(sqlSelectMultiple(ERASE_OBJS, repositoryIds.size()))) {
+ int i = 1;
+ for (String repositoryId : repositoryIds) {
+ ps.setString(i++, repositoryId);
+ }
+ ps.executeUpdate();
+ }
+ conn.commit();
+ } catch (SQLException e) {
+ throw unhandledSQLException(databaseSpecific, e);
+ }
+ }
+
+ static RuntimeException unhandledSQLException(DatabaseSpecific databaseSpecific, SQLException e) {
+ if (databaseSpecific.isRetryTransaction(e)) {
+ return new UnknownOperationResultException("Unhandled SQL exception", e);
+ }
+ return new RuntimeException("Unhandled SQL exception", e);
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendBaseConfig.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendBaseConfig.java
new file mode 100644
index 00000000000..8eb457cc84a
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendBaseConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import java.util.Optional;
+
+public interface Jdbc2BackendBaseConfig {
+
+ Optional datasourceName();
+
+ /**
+ * The JDBC catalog name.
+ *
+ * @deprecated This setting has never worked as expected and is now ineffective. The catalog must
+ * be specified directly in the JDBC URL using the option {@code
+ * quarkus.datasource.*.jdbc.url}.
+ */
+ @Deprecated(forRemoval = true)
+ Optional catalog();
+
+ /**
+ * The JDBC schema name.
+ *
+ * @deprecated This setting has never worked as expected and is now ineffective. The schema must
+ * be specified directly in the JDBC URL using the option {@code
+ * quarkus.datasource.*.jdbc.url}.
+ */
+ @Deprecated(forRemoval = true)
+ Optional schema();
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendConfig.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendConfig.java
new file mode 100644
index 00000000000..dbe4aa1a4d6
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendConfig.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import javax.sql.DataSource;
+import org.immutables.value.Value;
+
+@Value.Immutable
+public interface Jdbc2BackendConfig extends Jdbc2BackendBaseConfig {
+
+ DataSource dataSource();
+
+ static ImmutableJdbc2BackendConfig.Builder builder() {
+ return ImmutableJdbc2BackendConfig.builder();
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendFactory.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendFactory.java
new file mode 100644
index 00000000000..f8dc4767992
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2BackendFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import jakarta.annotation.Nonnull;
+import org.projectnessie.versioned.storage.common.persist.BackendFactory;
+
+public class Jdbc2BackendFactory implements BackendFactory {
+
+ public static final String NAME = "JDBC";
+
+ @Override
+ @Nonnull
+ public String name() {
+ return NAME;
+ }
+
+ @Override
+ @Nonnull
+ public Jdbc2BackendConfig newConfigInstance() {
+ return Jdbc2BackendConfig.builder().build();
+ }
+
+ @Override
+ @Nonnull
+ public Jdbc2Backend buildBackend(@Nonnull Jdbc2BackendConfig config) {
+ DatabaseSpecific databaseSpecific = DatabaseSpecifics.detect(config.dataSource());
+ return new Jdbc2Backend(config, databaseSpecific, false);
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2ColumnType.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2ColumnType.java
new file mode 100644
index 00000000000..00f5d4a9a8c
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2ColumnType.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+public enum Jdbc2ColumnType {
+ // 0
+ NAME(),
+ // 1
+ OBJ_ID(),
+ // 2
+ BOOL(),
+ // 3
+ VARBINARY(),
+ // 4
+ BIGINT(),
+ // 5
+ VARCHAR(),
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Persist.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Persist.java
new file mode 100644
index 00000000000..798653c2d07
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Persist.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import static java.util.Collections.singleton;
+
+import jakarta.annotation.Nonnull;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Set;
+import org.projectnessie.versioned.storage.common.config.StoreConfig;
+import org.projectnessie.versioned.storage.common.exceptions.ObjNotFoundException;
+import org.projectnessie.versioned.storage.common.exceptions.ObjTooLargeException;
+import org.projectnessie.versioned.storage.common.exceptions.RefAlreadyExistsException;
+import org.projectnessie.versioned.storage.common.exceptions.RefConditionFailedException;
+import org.projectnessie.versioned.storage.common.exceptions.RefNotFoundException;
+import org.projectnessie.versioned.storage.common.objtypes.UpdateableObj;
+import org.projectnessie.versioned.storage.common.persist.CloseableIterator;
+import org.projectnessie.versioned.storage.common.persist.Obj;
+import org.projectnessie.versioned.storage.common.persist.ObjId;
+import org.projectnessie.versioned.storage.common.persist.ObjType;
+import org.projectnessie.versioned.storage.common.persist.Reference;
+
+class Jdbc2Persist extends AbstractJdbc2Persist {
+
+ private final Jdbc2Backend backend;
+
+ Jdbc2Persist(Jdbc2Backend backend, StoreConfig config) {
+ super(backend.databaseSpecific(), config);
+ this.backend = backend;
+ }
+
+ @FunctionalInterface
+ interface SQLRunnable {
+ R run(Connection conn) throws SQLException;
+ }
+
+ @FunctionalInterface
+ interface SQLRunnableException {
+ R run(Connection conn) throws E;
+ }
+
+ @FunctionalInterface
+ interface SQLRunnableExceptions {
+ R run(Connection conn) throws E1, E2;
+ }
+
+ @FunctionalInterface
+ interface SQLRunnableVoid {
+ void run(Connection conn);
+ }
+
+ private void withConnectionVoid(SQLRunnableVoid runnable) {
+ withConnection(
+ false,
+ conn -> {
+ runnable.run(conn);
+ return null;
+ });
+ }
+
+ private R withConnection(boolean readOnly, SQLRunnable runnable) {
+ try (Connection conn = backend.borrowConnection()) {
+ boolean ok = false;
+ R r;
+ try {
+ r = runnable.run(conn);
+ ok = true;
+ } finally {
+ if (!readOnly) {
+ if (ok) {
+ conn.commit();
+ } else {
+ conn.rollback();
+ }
+ }
+ }
+ return r;
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ private R withConnectionException(
+ boolean readOnly, SQLRunnableException runnable) throws E {
+ try (Connection conn = backend.borrowConnection()) {
+ boolean ok = false;
+ R r;
+ try {
+ r = runnable.run(conn);
+ ok = true;
+ } finally {
+ if (!readOnly) {
+ if (ok) {
+ conn.commit();
+ } else {
+ conn.rollback();
+ }
+ }
+ }
+ return r;
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ private R withConnectionExceptions(
+ SQLRunnableExceptions runnable) throws E1, E2 {
+ try (Connection conn = backend.borrowConnection()) {
+ boolean ok = false;
+ R r;
+ try {
+ r = runnable.run(conn);
+ ok = true;
+ } finally {
+ if (ok) {
+ conn.commit();
+ } else {
+ conn.rollback();
+ }
+ }
+ return r;
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+
+ @Override
+ public Reference fetchReference(@Nonnull String name) {
+ return withConnection(true, conn -> super.findReference(conn, name));
+ }
+
+ @Override
+ @Nonnull
+ public Reference[] fetchReferences(@Nonnull String[] names) {
+ return withConnection(true, conn -> super.findReferences(conn, names));
+ }
+
+ @Override
+ @Nonnull
+ public Reference addReference(@Nonnull Reference reference) throws RefAlreadyExistsException {
+ return withConnectionException(false, conn -> super.addReference(conn, reference));
+ }
+
+ @Override
+ @Nonnull
+ public Reference markReferenceAsDeleted(@Nonnull Reference reference)
+ throws RefNotFoundException, RefConditionFailedException {
+ return withConnectionExceptions(
+ (SQLRunnableExceptions)
+ conn -> super.markReferenceAsDeleted(conn, reference));
+ }
+
+ @Override
+ public void purgeReference(@Nonnull Reference reference)
+ throws RefNotFoundException, RefConditionFailedException {
+ withConnectionExceptions(
+ (SQLRunnableExceptions)
+ conn -> {
+ super.purgeReference(conn, reference);
+ return null;
+ });
+ }
+
+ @Override
+ @Nonnull
+ public Reference updateReferencePointer(@Nonnull Reference reference, @Nonnull ObjId newPointer)
+ throws RefNotFoundException, RefConditionFailedException {
+ return withConnectionExceptions(
+ (SQLRunnableExceptions)
+ conn -> super.updateReferencePointer(conn, reference, newPointer));
+ }
+
+ @Override
+ @Nonnull
+ public T fetchTypedObj(
+ @Nonnull ObjId id, ObjType type, @Nonnull Class typeClass) throws ObjNotFoundException {
+ return withConnectionException(true, conn -> super.fetchTypedObj(conn, id, type, typeClass));
+ }
+
+ @Override
+ @Nonnull
+ public ObjType fetchObjType(@Nonnull ObjId id) throws ObjNotFoundException {
+ return withConnectionException(true, conn -> super.fetchObjType(conn, id));
+ }
+
+ @Override
+ public T[] fetchTypedObjsIfExist(
+ @Nonnull ObjId[] ids, ObjType type, @Nonnull Class typeClass) {
+ return withConnectionException(
+ true, conn -> super.fetchTypedObjsIfExist(conn, ids, type, typeClass));
+ }
+
+ @Override
+ public boolean storeObj(@Nonnull Obj obj, boolean ignoreSoftSizeRestrictions)
+ throws ObjTooLargeException {
+ return withConnectionException(
+ false, conn -> super.storeObj(conn, obj, ignoreSoftSizeRestrictions));
+ }
+
+ @Override
+ @Nonnull
+ public boolean[] storeObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
+ return withConnectionException(false, conn -> super.storeObjs(conn, objs));
+ }
+
+ @Override
+ public void deleteObj(@Nonnull ObjId id) {
+ withConnectionVoid(conn -> super.deleteObj(conn, id));
+ }
+
+ @Override
+ public void deleteObjs(@Nonnull ObjId[] ids) {
+ withConnectionVoid(conn -> super.deleteObjs(conn, ids));
+ }
+
+ @Override
+ public void upsertObj(@Nonnull Obj obj) throws ObjTooLargeException {
+ withConnectionException(false, conn -> super.updateObj(conn, obj));
+ }
+
+ @Override
+ public void upsertObjs(@Nonnull Obj[] objs) throws ObjTooLargeException {
+ withConnectionException(false, conn -> super.updateObjs(conn, objs));
+ }
+
+ @Override
+ public boolean deleteConditional(@Nonnull UpdateableObj obj) {
+ return withConnectionException(false, conn -> super.deleteConditional(conn, obj));
+ }
+
+ @Override
+ public boolean updateConditional(@Nonnull UpdateableObj expected, @Nonnull UpdateableObj newValue)
+ throws ObjTooLargeException {
+ return withConnectionException(
+ false, conn -> super.updateConditional(conn, expected, newValue));
+ }
+
+ @Override
+ public void erase() {
+ backend.eraseRepositories(singleton(config().repositoryId()));
+ }
+
+ @Nonnull
+ @Override
+ public CloseableIterator scanAllObjects(@Nonnull Set returnedObjTypes) {
+ try {
+ return super.scanAllObjects(backend.borrowConnection(), returnedObjTypes);
+ } catch (SQLException e) {
+ throw unhandledSQLException(e);
+ }
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2PersistFactory.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2PersistFactory.java
new file mode 100644
index 00000000000..e51af758c4d
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2PersistFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import jakarta.annotation.Nonnull;
+import org.projectnessie.versioned.storage.common.config.StoreConfig;
+import org.projectnessie.versioned.storage.common.persist.Persist;
+import org.projectnessie.versioned.storage.common.persist.PersistFactory;
+
+final class Jdbc2PersistFactory implements PersistFactory {
+
+ private final Jdbc2Backend backend;
+
+ Jdbc2PersistFactory(Jdbc2Backend backend) {
+ this.backend = backend;
+ }
+
+ @Override
+ @Nonnull
+ public Persist newPersist(@Nonnull StoreConfig config) {
+ return new Jdbc2Persist(backend, config);
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Serde.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Serde.java
new file mode 100644
index 00000000000..5dde54635c8
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/Jdbc2Serde.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import static java.util.Collections.emptyList;
+import static org.projectnessie.versioned.storage.common.persist.ObjId.objIdFromByteArray;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_CREATED_AT;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_DELETED;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_EXTENDED_INFO;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_NAME;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_POINTER;
+import static org.projectnessie.versioned.storage.serialize.ProtoSerialization.deserializePreviousPointers;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+import org.projectnessie.versioned.storage.common.persist.ObjId;
+import org.projectnessie.versioned.storage.common.persist.Reference;
+
+public class Jdbc2Serde {
+
+ public static ObjId deserializeObjId(ResultSet rs, String col) throws SQLException {
+ byte[] s = rs.getBytes(col);
+ return s != null ? objIdFromByteArray(s) : null;
+ }
+
+ public static void serializeObjId(
+ PreparedStatement ps, int col, ObjId value, DatabaseSpecific databaseSpecific)
+ throws SQLException {
+ if (value != null) {
+ ps.setBytes(col, value.asByteArray());
+ } else {
+ ps.setNull(col, databaseSpecific.columnTypeIds().get(Jdbc2ColumnType.OBJ_ID));
+ }
+ }
+
+ public static Reference deserializeReference(ResultSet rs) throws SQLException {
+ byte[] prevBytes = rs.getBytes(6);
+ List previousPointers =
+ prevBytes != null ? deserializePreviousPointers(prevBytes) : emptyList();
+ return Reference.reference(
+ rs.getString(COL_REFS_NAME),
+ deserializeObjId(rs, COL_REFS_POINTER),
+ rs.getBoolean(COL_REFS_DELETED),
+ rs.getLong(COL_REFS_CREATED_AT),
+ deserializeObjId(rs, COL_REFS_EXTENDED_INFO),
+ previousPointers);
+ }
+}
diff --git a/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/SqlConstants.java b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/SqlConstants.java
new file mode 100644
index 00000000000..a1ecfcabc87
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/java/org/projectnessie/versioned/storage/jdbc2/SqlConstants.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+final class SqlConstants {
+
+ static final int MAX_BATCH_SIZE = 50;
+
+ static final String TABLE_REFS = "refs2";
+ static final String TABLE_OBJS = "objs2";
+
+ static final String COL_REPO_ID = "repo";
+ static final String COL_OBJ_TYPE = "obj_type";
+ static final String COL_OBJ_ID = "obj_id";
+ static final String COL_OBJ_VERS = "obj_vers";
+ static final String COL_OBJ_VALUE = "obj_value";
+
+ static final String ERASE_OBJS =
+ "DELETE FROM " + TABLE_OBJS + " WHERE " + COL_REPO_ID + " IN (?)";
+ static final String ERASE_REFS =
+ "DELETE FROM " + TABLE_REFS + " WHERE " + COL_REPO_ID + " IN (?)";
+ static final String DELETE_OBJ =
+ "DELETE FROM " + TABLE_OBJS + " WHERE " + COL_REPO_ID + "=? AND " + COL_OBJ_ID + "=?";
+ static final String DELETE_OBJ_CONDITIONAL =
+ "DELETE FROM "
+ + TABLE_OBJS
+ + " WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_OBJ_ID
+ + "=? AND "
+ + COL_OBJ_TYPE
+ + "=? AND "
+ + COL_OBJ_VERS
+ + "=?";
+
+ static final String COL_REFS_NAME = "ref_name";
+ static final String COL_REFS_POINTER = "pointer";
+ static final String COL_REFS_DELETED = "deleted";
+ static final String COL_REFS_CREATED_AT = "created_at";
+ static final String COL_REFS_EXTENDED_INFO = "ext_info";
+ static final String COL_REFS_PREVIOUS = "prev_ptr";
+ static final String REFS_CREATED_AT_COND = "_REFS_CREATED_AT_";
+ static final String REFS_EXTENDED_INFO_COND = "_REFS_EXTENDED_INFO_";
+ static final String UPDATE_REFERENCE_POINTER =
+ "UPDATE "
+ + TABLE_REFS
+ + " SET "
+ + COL_REFS_POINTER
+ + "=?, "
+ + COL_REFS_PREVIOUS
+ + "=? WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_REFS_NAME
+ + "=? AND "
+ + COL_REFS_POINTER
+ + "=? AND "
+ + COL_REFS_DELETED
+ + "=? AND "
+ + COL_REFS_CREATED_AT
+ + REFS_CREATED_AT_COND
+ + " AND "
+ + COL_REFS_EXTENDED_INFO
+ + REFS_EXTENDED_INFO_COND;
+ static final String PURGE_REFERENCE =
+ "DELETE FROM "
+ + TABLE_REFS
+ + " WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_REFS_NAME
+ + "=? AND "
+ + COL_REFS_POINTER
+ + "=? AND "
+ + COL_REFS_DELETED
+ + "=? AND "
+ + COL_REFS_CREATED_AT
+ + REFS_CREATED_AT_COND
+ + " AND "
+ + COL_REFS_EXTENDED_INFO
+ + REFS_EXTENDED_INFO_COND;
+ static final String MARK_REFERENCE_AS_DELETED =
+ "UPDATE "
+ + TABLE_REFS
+ + " SET "
+ + COL_REFS_DELETED
+ + "=? WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_REFS_NAME
+ + "=? AND "
+ + COL_REFS_POINTER
+ + "=? AND "
+ + COL_REFS_DELETED
+ + "=? AND "
+ + COL_REFS_CREATED_AT
+ + REFS_CREATED_AT_COND
+ + " AND "
+ + COL_REFS_EXTENDED_INFO
+ + REFS_EXTENDED_INFO_COND;
+ static final String ADD_REFERENCE =
+ "INSERT INTO "
+ + TABLE_REFS
+ + " ("
+ + COL_REPO_ID
+ + ", "
+ + COL_REFS_NAME
+ + ", "
+ + COL_REFS_POINTER
+ + ", "
+ + COL_REFS_DELETED
+ + ", "
+ + COL_REFS_CREATED_AT
+ + ", "
+ + COL_REFS_EXTENDED_INFO
+ + ", "
+ + COL_REFS_PREVIOUS
+ + ") VALUES (?, ?, ?, ?, ?, ?, ?)";
+ static final String FIND_REFERENCES =
+ "SELECT "
+ + COL_REFS_NAME
+ + ", "
+ + COL_REFS_POINTER
+ + ", "
+ + COL_REFS_DELETED
+ + ", "
+ + COL_REFS_CREATED_AT
+ + ", "
+ + COL_REFS_EXTENDED_INFO
+ + ", "
+ + COL_REFS_PREVIOUS
+ + " FROM "
+ + TABLE_REFS
+ + " WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_REFS_NAME
+ + " IN (?)";
+
+ static final String COLS_OBJS_ALL_NAMES =
+ COL_OBJ_ID + ", " + COL_OBJ_TYPE + ", " + COL_OBJ_VERS + ", " + COL_OBJ_VALUE;
+
+ static final String FETCH_OBJ_TYPE =
+ "SELECT "
+ + COL_OBJ_TYPE
+ + " FROM "
+ + TABLE_OBJS
+ + " WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_OBJ_ID
+ + " IN (?)";
+
+ static final String FIND_OBJS =
+ "SELECT "
+ + COLS_OBJS_ALL_NAMES
+ + " FROM "
+ + TABLE_OBJS
+ + " WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_OBJ_ID
+ + " IN (?)";
+
+ static final String STORE_OBJ =
+ "INSERT INTO "
+ + TABLE_OBJS
+ + " ("
+ + COL_REPO_ID
+ + ", "
+ + COLS_OBJS_ALL_NAMES
+ + ") VALUES (?, ?, ?, ?, ?)";
+
+ static final String FIND_OBJS_TYPED = FIND_OBJS + " AND " + COL_OBJ_TYPE + "=?";
+
+ static final String SCAN_OBJS =
+ "SELECT "
+ + COLS_OBJS_ALL_NAMES
+ + " FROM "
+ + TABLE_OBJS
+ + " WHERE "
+ + COL_REPO_ID
+ + "=? AND "
+ + COL_OBJ_TYPE
+ + " IN (?)";
+
+ private SqlConstants() {}
+}
diff --git a/versioned/storage/jdbc2/src/main/resources/META-INF/services/org.projectnessie.versioned.storage.common.persist.BackendFactory b/versioned/storage/jdbc2/src/main/resources/META-INF/services/org.projectnessie.versioned.storage.common.persist.BackendFactory
new file mode 100644
index 00000000000..128b47eaf91
--- /dev/null
+++ b/versioned/storage/jdbc2/src/main/resources/META-INF/services/org.projectnessie.versioned.storage.common.persist.BackendFactory
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2022 Dremio
+#
+# Licensed 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.
+#
+org.projectnessie.versioned.storage.jdbc2.Jdbc2BackendFactory
diff --git a/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2BackendFactory.java b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2BackendFactory.java
new file mode 100644
index 00000000000..31819359acd
--- /dev/null
+++ b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2BackendFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.jdbc2tests.AbstractJdbc2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.H2BackendTestFactory;
+
+public class TestH2BackendFactory extends AbstractTestJdbc2BackendFactory {
+
+ @Override
+ protected AbstractJdbc2BackendTestFactory testFactory() {
+ return new H2BackendTestFactory();
+ }
+}
diff --git a/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2CachingPersist.java b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2CachingPersist.java
new file mode 100644
index 00000000000..ae5e178642e
--- /dev/null
+++ b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2CachingPersist.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.testextension.NessiePersistCache;
+
+@NessiePersistCache
+public class TestH2CachingPersist extends TestH2Persist {}
diff --git a/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2Persist.java b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2Persist.java
new file mode 100644
index 00000000000..8c197d6b993
--- /dev/null
+++ b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2Persist.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractPersistTests;
+import org.projectnessie.versioned.storage.jdbc2tests.H2BackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(H2BackendTestFactory.class)
+public class TestH2Persist extends AbstractPersistTests {}
diff --git a/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2VersionStore.java b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2VersionStore.java
new file mode 100644
index 00000000000..8b7e3c60d6d
--- /dev/null
+++ b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2VersionStore.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.commontests.AbstractVersionStoreTests;
+import org.projectnessie.versioned.storage.jdbc2tests.H2BackendTestFactory;
+import org.projectnessie.versioned.storage.testextension.NessieBackend;
+
+@NessieBackend(H2BackendTestFactory.class)
+public class TestH2VersionStore extends AbstractVersionStoreTests {}
diff --git a/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2VersionStoreCaching.java b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2VersionStoreCaching.java
new file mode 100644
index 00000000000..dbc6f3f0110
--- /dev/null
+++ b/versioned/storage/jdbc2/src/test/java/org/projectnessie/versioned/storage/jdbc2/TestH2VersionStoreCaching.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import org.projectnessie.versioned.storage.testextension.NessiePersistCache;
+
+@NessiePersistCache
+public class TestH2VersionStoreCaching extends TestH2VersionStore {}
diff --git a/versioned/storage/jdbc2/src/testFixtures/java/org/projectnessie/versioned/storage/jdbc2/AbstractTestJdbc2BackendFactory.java b/versioned/storage/jdbc2/src/testFixtures/java/org/projectnessie/versioned/storage/jdbc2/AbstractTestJdbc2BackendFactory.java
new file mode 100644
index 00000000000..2a24bc1132e
--- /dev/null
+++ b/versioned/storage/jdbc2/src/testFixtures/java/org/projectnessie/versioned/storage/jdbc2/AbstractTestJdbc2BackendFactory.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2023 Dremio
+ *
+ * Licensed 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.projectnessie.versioned.storage.jdbc2;
+
+import static org.projectnessie.versioned.storage.common.logic.Logics.repositoryLogic;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REFS_NAME;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.COL_REPO_ID;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.TABLE_OBJS;
+import static org.projectnessie.versioned.storage.jdbc2.SqlConstants.TABLE_REFS;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import javax.sql.DataSource;
+import org.assertj.core.api.SoftAssertions;
+import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
+import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.projectnessie.versioned.storage.common.config.StoreConfig;
+import org.projectnessie.versioned.storage.common.logic.RepositoryDescription;
+import org.projectnessie.versioned.storage.common.logic.RepositoryLogic;
+import org.projectnessie.versioned.storage.common.persist.Backend;
+import org.projectnessie.versioned.storage.common.persist.BackendFactory;
+import org.projectnessie.versioned.storage.common.persist.Persist;
+import org.projectnessie.versioned.storage.common.persist.PersistFactory;
+import org.projectnessie.versioned.storage.common.persist.PersistLoader;
+import org.projectnessie.versioned.storage.jdbc2tests.AbstractJdbc2BackendTestFactory;
+import org.projectnessie.versioned.storage.jdbc2tests.DataSourceProducer;
+
+@SuppressWarnings("SqlDialectInspection")
+@ExtendWith(SoftAssertionsExtension.class)
+public abstract class AbstractTestJdbc2BackendFactory {
+ @InjectSoftAssertions protected SoftAssertions soft;
+
+ protected abstract AbstractJdbc2BackendTestFactory testFactory();
+
+ @Test
+ public void productionLike() throws Exception {
+ AbstractJdbc2BackendTestFactory testFactory = testFactory();
+ testFactory.start();
+ try {
+
+ DataSource dataSource =
+ DataSourceProducer.builder()
+ .jdbcUrl(testFactory.jdbcUrl())
+ .jdbcUser(testFactory.jdbcUser())
+ .jdbcPass(testFactory.jdbcPass())
+ .build()
+ .createNewDataSource();
+ try {
+ BackendFactory factory =
+ PersistLoader.findFactoryByName(Jdbc2BackendFactory.NAME);
+ soft.assertThat(factory).isNotNull().isInstanceOf(Jdbc2BackendFactory.class);
+ RepositoryDescription repoDesc;
+
+ try (Backend backend =
+ factory.buildBackend(Jdbc2BackendConfig.builder().dataSource(dataSource).build())) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ backend.setupSchema();
+ PersistFactory persistFactory = backend.createFactory();
+ soft.assertThat(persistFactory).isNotNull().isInstanceOf(Jdbc2PersistFactory.class);
+ Persist persist = persistFactory.newPersist(StoreConfig.Adjustable.empty());
+ soft.assertThat(persist).isNotNull().isInstanceOf(Jdbc2Persist.class);
+
+ RepositoryLogic repositoryLogic = repositoryLogic(persist);
+ repositoryLogic.initialize("initializeAgain");
+ repoDesc = repositoryLogic.fetchRepositoryDescription();
+ soft.assertThat(repoDesc).isNotNull();
+ }
+
+ try (Backend backend =
+ factory.buildBackend(Jdbc2BackendConfig.builder().dataSource(dataSource).build())) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ backend.setupSchema();
+ PersistFactory persistFactory = backend.createFactory();
+ soft.assertThat(persistFactory).isNotNull().isInstanceOf(Jdbc2PersistFactory.class);
+ Persist persist = persistFactory.newPersist(StoreConfig.Adjustable.empty());
+ soft.assertThat(persist).isNotNull().isInstanceOf(Jdbc2Persist.class);
+
+ RepositoryLogic repositoryLogic = repositoryLogic(persist);
+ repositoryLogic.initialize("initializeAgain");
+ soft.assertThat(repositoryLogic.fetchRepositoryDescription()).isEqualTo(repoDesc);
+ }
+ } finally {
+ ((AutoCloseable) dataSource).close();
+ }
+ } finally {
+ testFactory.stop();
+ }
+ }
+
+ @Test
+ public void backendTestFactory() throws Exception {
+ AbstractJdbc2BackendTestFactory testFactory = testFactory();
+ testFactory.start();
+ try {
+
+ // Need to keep one connection alive for H2-in-memory, so H2 does _not_ drop out data during
+ // the test execution.
+ DataSource dataSource =
+ DataSourceProducer.builder()
+ .jdbcUrl(testFactory.jdbcUrl())
+ .jdbcUser(testFactory.jdbcUser())
+ .jdbcPass(testFactory.jdbcPass())
+ .build()
+ .createNewDataSource();
+ try (Connection keepAliveForH2 = dataSource.getConnection()) {
+
+ RepositoryDescription repoDesc;
+
+ try (Backend backend = testFactory.createNewBackend()) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ backend.setupSchema();
+ PersistFactory persistFactory = backend.createFactory();
+ soft.assertThat(persistFactory).isNotNull().isInstanceOf(Jdbc2PersistFactory.class);
+ Persist persist = persistFactory.newPersist(StoreConfig.Adjustable.empty());
+ soft.assertThat(persist).isNotNull().isInstanceOf(Jdbc2Persist.class);
+
+ RepositoryLogic repositoryLogic = repositoryLogic(persist);
+ repositoryLogic.initialize("initializeAgain");
+ repoDesc = repositoryLogic.fetchRepositoryDescription();
+ soft.assertThat(repoDesc).isNotNull();
+ }
+
+ try (Backend backend = testFactory.createNewBackend()) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ backend.setupSchema();
+ PersistFactory persistFactory = backend.createFactory();
+ soft.assertThat(persistFactory).isNotNull().isInstanceOf(Jdbc2PersistFactory.class);
+ Persist persist = persistFactory.newPersist(StoreConfig.Adjustable.empty());
+ soft.assertThat(persist).isNotNull().isInstanceOf(Jdbc2Persist.class);
+
+ RepositoryLogic repositoryLogic = repositoryLogic(persist);
+ repositoryLogic.initialize("initializeAgain");
+ soft.assertThat(repositoryLogic.fetchRepositoryDescription()).isEqualTo(repoDesc);
+ }
+ } finally {
+ ((AutoCloseable) dataSource).close();
+ }
+ } finally {
+ testFactory.stop();
+ }
+ }
+
+ @Test
+ public void incompatibleTableSchema() throws Exception {
+ AbstractJdbc2BackendTestFactory testFactory = testFactory();
+ testFactory.start();
+ try {
+ DataSource dataSource =
+ DataSourceProducer.builder()
+ .jdbcUrl(testFactory.jdbcUrl())
+ .jdbcUrl(testFactory.jdbcUrl())
+ .jdbcUser(testFactory.jdbcUser())
+ .jdbcPass(testFactory.jdbcPass())
+ .build()
+ .createNewDataSource();
+ try {
+ BackendFactory factory =
+ PersistLoader.findFactoryByName(Jdbc2BackendFactory.NAME);
+ soft.assertThat(factory).isNotNull().isInstanceOf(Jdbc2BackendFactory.class);
+ try (Backend backend =
+ factory.buildBackend(Jdbc2BackendConfig.builder().dataSource(dataSource).build())) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ backend.setupSchema();
+ }
+
+ try (Connection conn = dataSource.getConnection();
+ Statement st = conn.createStatement()) {
+ dropTables(conn, st);
+
+ st.executeUpdate("CREATE TABLE " + TABLE_REFS + " (foobarbaz VARCHAR(255) PRIMARY KEY)");
+ }
+
+ try (Backend backend =
+ factory.buildBackend(Jdbc2BackendConfig.builder().dataSource(dataSource).build())) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ soft.assertThatIllegalStateException()
+ .isThrownBy(backend::setupSchema)
+ .withMessageStartingWith(
+ "Expected primary key columns ["
+ + COL_REPO_ID
+ + ", "
+ + COL_REFS_NAME
+ + "] do not match existing primary key columns [foobarbaz] for table '"
+ + TABLE_REFS
+ + "'. DDL template:\nCREATE TABLE "
+ + TABLE_REFS);
+ }
+
+ try (Connection conn = dataSource.getConnection();
+ Statement st = conn.createStatement()) {
+ dropTables(conn, st);
+
+ st.executeUpdate(
+ "CREATE TABLE "
+ + TABLE_REFS
+ + " ("
+ + COL_REPO_ID
+ + " VARCHAR(255), "
+ + COL_REFS_NAME
+ + " VARCHAR(255), meep VARCHAR(255), boo VARCHAR(255), PRIMARY KEY ("
+ + COL_REPO_ID
+ + ", "
+ + COL_REFS_NAME
+ + "))");
+ }
+
+ try (Backend backend =
+ factory.buildBackend(Jdbc2BackendConfig.builder().dataSource(dataSource).build())) {
+ soft.assertThat(backend).isNotNull().isInstanceOf(Jdbc2Backend.class);
+ soft.assertThatIllegalStateException()
+ .isThrownBy(backend::setupSchema)
+ .withMessageStartingWith(
+ "The database table "
+ + TABLE_REFS
+ + " is missing mandatory columns created_at,deleted,ext_info,pointer,prev_ptr.\n"
+ + "Found columns : boo,meep,ref_name,repo\n"
+ + "Expected columns : ")
+ .withMessageContaining("DDL template:\nCREATE TABLE " + TABLE_REFS);
+ }
+ } finally {
+ ((AutoCloseable) dataSource).close();
+ }
+ } finally {
+ testFactory.stop();
+ }
+ }
+
+ private static void dropTables(Connection conn, Statement st) throws SQLException {
+ try {
+ st.executeUpdate("DROP TABLE " + TABLE_REFS);
+ conn.commit();
+ } catch (SQLException ignore) {
+ conn.rollback();
+ }
+ try {
+ st.executeUpdate("DROP TABLE " + TABLE_OBJS);
+ conn.commit();
+ } catch (SQLException ignore) {
+ conn.rollback();
+ }
+ }
+}
diff --git a/versioned/transfer/build.gradle.kts b/versioned/transfer/build.gradle.kts
index bd6f8e398a9..9e933cef4e1 100644
--- a/versioned/transfer/build.gradle.kts
+++ b/versioned/transfer/build.gradle.kts
@@ -72,6 +72,7 @@ dependencies {
intTestImplementation(project(":nessie-versioned-storage-cassandra"))
intTestImplementation(project(":nessie-versioned-storage-dynamodb"))
intTestImplementation(project(":nessie-versioned-storage-jdbc"))
+ intTestImplementation(project(":nessie-versioned-storage-jdbc2"))
intTestImplementation(project(":nessie-versioned-storage-mongodb"))
intTestImplementation(project(":nessie-versioned-storage-rocksdb"))