diff --git a/duckdb_java.def b/duckdb_java.def index eec6837c1..7e8f9e4b7 100644 --- a/duckdb_java.def +++ b/duckdb_java.def @@ -24,6 +24,8 @@ Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1register Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1stream Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1appender +Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref +Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute diff --git a/duckdb_java.exp b/duckdb_java.exp index 6f30fc7c5..91884e161 100644 --- a/duckdb_java.exp +++ b/duckdb_java.exp @@ -21,6 +21,8 @@ _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1register _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1stream _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1appender +_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref +_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute diff --git a/duckdb_java.map b/duckdb_java.map index 0f91c0486..18a345f58 100644 --- a/duckdb_java.map +++ b/duckdb_java.map @@ -23,6 +23,8 @@ DUCKDB_JAVA { Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1arrow_1stream; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1appender; + Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref; + Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute; diff --git a/src/jni/duckdb_java.cpp b/src/jni/duckdb_java.cpp index c4362883d..1759f4eaa 100644 --- a/src/jni/duckdb_java.cpp +++ b/src/jni/duckdb_java.cpp @@ -71,19 +71,35 @@ jobject _duckdb_jdbc_startup(JNIEnv *env, jclass, jbyteArray database_j, jboolea std::unique_ptr config = create_db_config(env, read_only, props); bool cache_instance = database != ":memory:" && !database.empty(); auto shared_db = instance_cache.GetOrCreateInstance(database, *config, cache_instance); - auto conn_holder = new ConnectionHolder(shared_db); + auto conn_ref = new ConnectionHolder(shared_db); - return env->NewDirectByteBuffer(conn_holder, 0); + return env->NewDirectByteBuffer(conn_ref, 0); } jobject _duckdb_jdbc_connect(JNIEnv *env, jclass, jobject conn_ref_buf) { - auto conn_ref = (ConnectionHolder *)env->GetDirectBufferAddress(conn_ref_buf); + auto conn_ref = get_connection_ref(env, conn_ref_buf); auto config = ClientConfig::GetConfig(*conn_ref->connection->context); auto conn = new ConnectionHolder(conn_ref->db); conn->connection->context->config = config; return env->NewDirectByteBuffer(conn, 0); } +jobject _duckdb_jdbc_create_db_ref(JNIEnv *env, jclass, jobject conn_ref_buf) { + auto conn_ref = get_connection_ref(env, conn_ref_buf); + auto db_ref = conn_ref->create_db_ref(); + return env->NewDirectByteBuffer(db_ref, 0); +} + +void _duckdb_jdbc_destroy_db_ref(JNIEnv *env, jclass, jobject db_ref_buf) { + if (nullptr == db_ref_buf) { + return; + } + auto db_ref = (DBHolder *)env->GetDirectBufferAddress(db_ref_buf); + if (db_ref) { + delete db_ref; + } +} + jstring _duckdb_jdbc_get_schema(JNIEnv *env, jclass, jobject conn_ref_buf) { auto conn_ref = get_connection(env, conn_ref_buf); if (!conn_ref) { @@ -163,6 +179,9 @@ jobject _duckdb_jdbc_query_progress(JNIEnv *env, jclass, jobject conn_ref_buf) { } void _duckdb_jdbc_disconnect(JNIEnv *env, jclass, jobject conn_ref_buf) { + if (nullptr == conn_ref_buf) { + return; + } auto conn_ref = (ConnectionHolder *)env->GetDirectBufferAddress(conn_ref_buf); if (conn_ref) { delete conn_ref; @@ -249,6 +268,9 @@ jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectA } void _duckdb_jdbc_release(JNIEnv *env, jclass, jobject stmt_ref_buf) { + if (nullptr == stmt_ref_buf) { + return; + } auto stmt_ref = (StatementHolder *)env->GetDirectBufferAddress(stmt_ref_buf); if (stmt_ref) { delete stmt_ref; @@ -256,6 +278,9 @@ void _duckdb_jdbc_release(JNIEnv *env, jclass, jobject stmt_ref_buf) { } void _duckdb_jdbc_free_result(JNIEnv *env, jclass, jobject res_ref_buf) { + if (nullptr == res_ref_buf) { + return; + } auto res_ref = (ResultHolder *)env->GetDirectBufferAddress(res_ref_buf); if (res_ref) { delete res_ref; diff --git a/src/jni/functions.cpp b/src/jni/functions.cpp index 58f232fac..73541f3b6 100644 --- a/src/jni/functions.cpp +++ b/src/jni/functions.cpp @@ -26,6 +26,27 @@ JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect(JNI } } +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref(JNIEnv * env, jclass param0, jobject param1) { + try { + return _duckdb_jdbc_create_db_ref(env, param0, param1); + } catch (const std::exception &e) { + duckdb::ErrorData error(e); + ThrowJNI(env, error.Message().c_str()); + + return nullptr; + } +} + +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref(JNIEnv * env, jclass param0, jobject param1) { + try { + return _duckdb_jdbc_destroy_db_ref(env, param0, param1); + } catch (const std::exception &e) { + duckdb::ErrorData error(e); + ThrowJNI(env, error.Message().c_str()); + + } +} + JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1auto_1commit(JNIEnv * env, jclass param0, jobject param1, jboolean param2) { try { return _duckdb_jdbc_set_auto_commit(env, param0, param1, param2); diff --git a/src/jni/functions.hpp b/src/jni/functions.hpp index 24bf180c8..b089845c1 100644 --- a/src/jni/functions.hpp +++ b/src/jni/functions.hpp @@ -17,6 +17,14 @@ jobject _duckdb_jdbc_connect(JNIEnv * env, jclass param0, jobject param1); JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1connect(JNIEnv * env, jclass param0, jobject param1); +jobject _duckdb_jdbc_create_db_ref(JNIEnv * env, jclass param0, jobject param1); + +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1db_1ref(JNIEnv * env, jclass param0, jobject param1); + +void _duckdb_jdbc_destroy_db_ref(JNIEnv * env, jclass param0, jobject param1); + +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1destroy_1db_1ref(JNIEnv * env, jclass param0, jobject param1); + void _duckdb_jdbc_set_auto_commit(JNIEnv * env, jclass param0, jobject param1, jboolean param2); JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1auto_1commit(JNIEnv * env, jclass param0, jobject param1, jboolean param2); diff --git a/src/jni/holders.hpp b/src/jni/holders.hpp index 48a49cea2..b268b3197 100644 --- a/src/jni/holders.hpp +++ b/src/jni/holders.hpp @@ -4,6 +4,21 @@ #include +/** + * Holds a copy of a shared_ptr to an existing DB instance. + * Is used to keep this DB alive (and accessible from DB cache) + * even after the last connection to this DB is closed. + */ +struct DBHolder { + duckdb::shared_ptr db; + + DBHolder(duckdb::shared_ptr _db) : db(std::move(_db)) {}; + + DBHolder(const DBHolder &) = delete; + + DBHolder &operator=(const DBHolder &) = delete; +}; + /** * Associates a duckdb::Connection with a duckdb::DuckDB. The DB may be shared amongst many ConnectionHolders, but the * Connection is unique to this holder. Every Java DuckDBConnection has exactly 1 of these holders, and they are never @@ -17,6 +32,10 @@ struct ConnectionHolder { ConnectionHolder(duckdb::shared_ptr _db) : db(_db), connection(duckdb::make_uniq(*_db)) { } + + DBHolder *create_db_ref() { + return new DBHolder(db); + } }; struct StatementHolder { @@ -28,17 +47,22 @@ struct ResultHolder { duckdb::unique_ptr chunk; }; -/** - * Throws a SQLException and returns nullptr if a valid Connection can't be retrieved from the buffer. - */ -inline duckdb::Connection *get_connection(JNIEnv *env, jobject conn_ref_buf) { +inline ConnectionHolder *get_connection_ref(JNIEnv *env, jobject conn_ref_buf) { if (!conn_ref_buf) { - throw duckdb::ConnectionException("Invalid connection"); + throw duckdb::ConnectionException("Invalid connection buffer ref"); } - auto conn_holder = (ConnectionHolder *)env->GetDirectBufferAddress(conn_ref_buf); + auto conn_holder = reinterpret_cast(env->GetDirectBufferAddress(conn_ref_buf)); if (!conn_holder) { - throw duckdb::ConnectionException("Invalid connection"); + throw duckdb::ConnectionException("Invalid connection buffer"); } + return conn_holder; +} + +/** + * Throws a SQLException and returns nullptr if a valid Connection can't be retrieved from the buffer. + */ +inline duckdb::Connection *get_connection(JNIEnv *env, jobject conn_ref_buf) { + auto conn_holder = get_connection_ref(env, conn_ref_buf); auto conn_ref = conn_holder->connection.get(); if (!conn_ref || !conn_ref->context) { throw duckdb::ConnectionException("Invalid connection"); diff --git a/src/main/java/org/duckdb/DuckDBConnection.java b/src/main/java/org/duckdb/DuckDBConnection.java index ce4672180..053de7ea1 100644 --- a/src/main/java/org/duckdb/DuckDBConnection.java +++ b/src/main/java/org/duckdb/DuckDBConnection.java @@ -2,8 +2,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.duckdb.DuckDBDriver.JDBC_AUTO_COMMIT; -import static org.duckdb.JdbcUtils.isStringTruish; -import static org.duckdb.JdbcUtils.removeOption; +import static org.duckdb.JdbcUtils.*; import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; @@ -49,19 +48,13 @@ public final class DuckDBConnection implements java.sql.Connection { public static DuckDBConnection newConnection(String url, boolean readOnly, Properties properties) throws SQLException { - if (!url.startsWith("jdbc:duckdb:")) { - throw new SQLException("DuckDB JDBC URL needs to start with 'jdbc:duckdb:'"); - } - String db_dir = url.substring("jdbc:duckdb:".length()).trim(); - if (db_dir.length() == 0) { - db_dir = ":memory:"; - } - if (db_dir.startsWith("memory:")) { - db_dir = ":" + db_dir; + if (null == properties) { + properties = new Properties(); } + String dbName = dbNameFromUrl(url); String autoCommitStr = removeOption(properties, JDBC_AUTO_COMMIT); boolean autoCommit = isStringTruish(autoCommitStr, true); - ByteBuffer nativeReference = DuckDBNative.duckdb_jdbc_startup(db_dir.getBytes(UTF_8), readOnly, properties); + ByteBuffer nativeReference = DuckDBNative.duckdb_jdbc_startup(dbName.getBytes(UTF_8), readOnly, properties); return new DuckDBConnection(nativeReference, url, readOnly, autoCommit); } diff --git a/src/main/java/org/duckdb/DuckDBDriver.java b/src/main/java/org/duckdb/DuckDBDriver.java index c43e9b4b4..9117d00ae 100644 --- a/src/main/java/org/duckdb/DuckDBDriver.java +++ b/src/main/java/org/duckdb/DuckDBDriver.java @@ -1,12 +1,10 @@ package org.duckdb; -import static org.duckdb.JdbcUtils.isStringTruish; -import static org.duckdb.JdbcUtils.removeOption; +import static org.duckdb.JdbcUtils.*; +import java.nio.ByteBuffer; import java.sql.*; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; import java.util.regex.Pattern; @@ -17,8 +15,9 @@ public class DuckDBDriver implements java.sql.Driver { public static final String DUCKDB_USER_AGENT_PROPERTY = "custom_user_agent"; public static final String JDBC_STREAM_RESULTS = "jdbc_stream_results"; public static final String JDBC_AUTO_COMMIT = "jdbc_auto_commit"; + public static final String JDBC_PIN_DB = "jdbc_pin_db"; - private static final String DUCKDB_URL_PREFIX = "jdbc:duckdb:"; + static final String DUCKDB_URL_PREFIX = "jdbc:duckdb:"; private static final String DUCKLAKE_OPTION = "ducklake"; private static final String DUCKLAKE_ALIAS_OPTION = "ducklake_alias"; @@ -26,6 +25,11 @@ public class DuckDBDriver implements java.sql.Driver { private static final String DUCKLAKE_URL_PREFIX = "ducklake:"; private static final ReentrantLock DUCKLAKE_INIT_LOCK = new ReentrantLock(); + private static final LinkedHashMap pinnedDbRefs = new LinkedHashMap<>(); + private static final ReentrantLock pinnedDbRefsLock = new ReentrantLock(); + private static boolean pinnedDbRefsShutdownHookRegistered = false; + private static boolean pinnedDbRefsShutdownHookRun = false; + static { try { DriverManager.registerDriver(new DuckDBDriver()); @@ -60,12 +64,17 @@ public Connection connect(String url, Properties info) throws SQLException { // to be established. info.remove("path"); + String pinDbOptStr = removeOption(info, JDBC_PIN_DB); + boolean pinDBOpt = isStringTruish(pinDbOptStr, false); + String ducklake = removeOption(info, DUCKLAKE_OPTION); String ducklakeAlias = removeOption(info, DUCKLAKE_ALIAS_OPTION); - Connection conn = DuckDBConnection.newConnection(url, readOnly, info); + DuckDBConnection conn = DuckDBConnection.newConnection(url, readOnly, info); - initDucklake(conn, url, ducklake, ducklakeAlias); + pinDB(pinDBOpt, url, conn); + + initDucklake(conn, ducklake, ducklakeAlias); return conn; } @@ -95,8 +104,7 @@ public Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new SQLFeatureNotSupportedException("no logger"); } - private static void initDucklake(Connection conn, String url, String ducklake, String ducklakeAlias) - throws SQLException { + private static void initDucklake(Connection conn, String ducklake, String ducklakeAlias) throws SQLException { if (null == ducklake) { return; } @@ -154,6 +162,55 @@ private static ParsedProps parsePropsFromUrl(String url) throws SQLException { return new ParsedProps(shortUrl, props); } + private static void pinDB(boolean pinnedDbOpt, String url, DuckDBConnection conn) throws SQLException { + if (!pinnedDbOpt) { + return; + } + String dbName = dbNameFromUrl(url); + if (":memory:".equals(dbName)) { + return; + } + + pinnedDbRefsLock.lock(); + try { + // Actual native DB cache uses absolute paths to file DBs, + // but that should not make the difference unless CWD is changed, + // that is not expected for a JVM process, see JDK-4045688. + if (pinnedDbRefsShutdownHookRun || pinnedDbRefs.containsKey(dbName)) { + return; + } + // No need to hold connRef lock here, this connection is not + // yet available to client at this point, so it cannot be closed. + ByteBuffer dbRef = DuckDBNative.duckdb_jdbc_create_db_ref(conn.connRef); + pinnedDbRefs.put(dbName, dbRef); + + if (!pinnedDbRefsShutdownHookRegistered) { + Runtime.getRuntime().addShutdownHook(new Thread(new PinnedDbRefsShutdownHook())); + pinnedDbRefsShutdownHookRegistered = true; + } + } finally { + pinnedDbRefsLock.unlock(); + } + } + + public static boolean releaseDB(String url) throws SQLException { + pinnedDbRefsLock.lock(); + try { + if (pinnedDbRefsShutdownHookRun) { + return false; + } + String dbName = dbNameFromUrl(url); + ByteBuffer dbRef = pinnedDbRefs.remove(dbName); + if (null == dbRef) { + return false; + } + DuckDBNative.duckdb_jdbc_destroy_db_ref(dbRef); + return true; + } finally { + pinnedDbRefsLock.unlock(); + } + } + private static class ParsedProps { final String shortUrl; final LinkedHashMap props; @@ -167,4 +224,23 @@ private ParsedProps(String shortUrl, LinkedHashMap props) { this.props = props; } } + + private static class PinnedDbRefsShutdownHook implements Runnable { + @Override + public void run() { + pinnedDbRefsLock.lock(); + try { + List dbRefsList = new ArrayList<>(pinnedDbRefs.values()); + Collections.reverse(dbRefsList); + for (ByteBuffer dbRef : dbRefsList) { + DuckDBNative.duckdb_jdbc_destroy_db_ref(dbRef); + } + pinnedDbRefsShutdownHookRun = true; + } catch (SQLException e) { + e.printStackTrace(); + } finally { + pinnedDbRefsLock.unlock(); + } + } + } } diff --git a/src/main/java/org/duckdb/DuckDBNative.java b/src/main/java/org/duckdb/DuckDBNative.java index cf8871a78..bfaad6d56 100644 --- a/src/main/java/org/duckdb/DuckDBNative.java +++ b/src/main/java/org/duckdb/DuckDBNative.java @@ -73,7 +73,11 @@ final class DuckDBNative { static native ByteBuffer duckdb_jdbc_startup(byte[] path, boolean read_only, Properties props) throws SQLException; // returns conn_ref connection reference object - static native ByteBuffer duckdb_jdbc_connect(ByteBuffer db_ref) throws SQLException; + static native ByteBuffer duckdb_jdbc_connect(ByteBuffer conn_ref) throws SQLException; + + static native ByteBuffer duckdb_jdbc_create_db_ref(ByteBuffer conn_ref) throws SQLException; + + static native void duckdb_jdbc_destroy_db_ref(ByteBuffer db_ref) throws SQLException; static native void duckdb_jdbc_set_auto_commit(ByteBuffer conn_ref, boolean auto_commit) throws SQLException; diff --git a/src/main/java/org/duckdb/JdbcUtils.java b/src/main/java/org/duckdb/JdbcUtils.java index 6f8f54c4c..8186e1857 100644 --- a/src/main/java/org/duckdb/JdbcUtils.java +++ b/src/main/java/org/duckdb/JdbcUtils.java @@ -1,5 +1,7 @@ package org.duckdb; +import static org.duckdb.DuckDBDriver.DUCKDB_URL_PREFIX; + import java.sql.SQLException; import java.util.Properties; @@ -37,4 +39,28 @@ static boolean isStringTruish(String val, boolean defaultVal) throws SQLExceptio } throw new SQLException("Invalid boolean option value: " + val); } + + static String dbNameFromUrl(String url) throws SQLException { + if (null == url) { + throw new SQLException("Invalid null URL specified"); + } + if (!url.startsWith(DUCKDB_URL_PREFIX)) { + throw new SQLException("DuckDB JDBC URL needs to start with 'jdbc:duckdb:'"); + } + final String shortUrl; + if (url.contains(";")) { + String[] parts = url.split(";"); + shortUrl = parts[0].trim(); + } else { + shortUrl = url; + } + String dbName = shortUrl.substring(DUCKDB_URL_PREFIX.length()).trim(); + if (dbName.length() == 0) { + dbName = ":memory:"; + } + if (dbName.startsWith("memory:")) { + dbName = ":" + dbName; + } + return dbName; + } } diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index bf8ad8e87..e249754fc 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -10,7 +10,6 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.duckdb.DuckDBDriver.DUCKDB_USER_AGENT_PROPERTY; -import static org.duckdb.DuckDBDriver.JDBC_STREAM_RESULTS; import static org.duckdb.DuckDBTimestamp.localDateTimeFromTimestamp; import static org.duckdb.test.Assertions.*; import static org.duckdb.test.Runner.runTests; @@ -3592,6 +3591,59 @@ public static void test_props_from_url() throws Exception { assertThrows(() -> { DriverManager.getConnection("jdbc:duckdb:;foo=bar"); }, SQLException.class); } + public static void test_pinned_db() throws Exception { + Properties config = new Properties(); + config.put(DuckDBDriver.JDBC_PIN_DB, true); + String memUrl = "jdbc:duckdb:memory:test1"; + + try (Connection conn = DriverManager.getConnection(memUrl); Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + try (Connection conn = DriverManager.getConnection(memUrl); Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + try (Connection conn = DriverManager.getConnection(memUrl, config); Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + try (Connection conn = DriverManager.getConnection(memUrl); Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE tab1"); + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + try (Connection conn = DriverManager.getConnection(memUrl); Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE tab1"); + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + assertThrows( + () -> { DriverManager.getConnection(memUrl + ";allow_community_extensions=true;"); }, SQLException.class); + + assertTrue(DuckDBDriver.releaseDB(memUrl)); + assertFalse(DuckDBDriver.releaseDB(memUrl)); + + try (Connection conn = DriverManager.getConnection(memUrl); Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + assertFalse(DuckDBDriver.releaseDB(memUrl)); + + try (Connection conn = DriverManager.getConnection(memUrl + ";allow_community_extensions=true;"); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 int)"); + } + + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 int)"); + assertFalse(DuckDBDriver.releaseDB(JDBC_URL)); + } + + // Leave DB pinned to check shutdown hook run + DriverManager.getConnection(memUrl, config).close(); + } + public static void main(String[] args) throws Exception { String arg1 = args.length > 0 ? args[0] : ""; final int statusCode;