From 7dd95c158bfe565dbcceff9007fc0528fa35e8bb Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:35:48 +0100 Subject: [PATCH 1/4] fix(jdbc): report the database product name as SQLite Liquibase selects its SQLite dialect only when the product name equals "SQLite", and jOOQ maps the same value to SQLDialect.SQLITE. Keep the libSQL identity in the driver name and the product version. Update the conformance guard, the integration assertions, and the Liquibase and jOOQ probes to pin the exact-match behavior. Closes #261. --- .../conformance/LibsqlConformanceGuard.java | 2 +- .../LibsqlConformanceGuardTest.java | 4 +-- .../magnusp/libsql/itest/LibsqlJdbcIT.java | 2 +- .../libsql/itest/WebSocketTransportIT.java | 2 +- .../libsql/jdbc/LibsqlDatabaseMetaData.java | 2 +- .../magnusp/libsql/jdbc/jooq/JooqIT.java | 9 +++-- .../libsql/jdbc/liquibase/LiquibaseIT.java | 36 ++++++------------- 7 files changed, 21 insertions(+), 36 deletions(-) diff --git a/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuard.java b/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuard.java index 7628b79..788a62e 100644 --- a/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuard.java +++ b/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuard.java @@ -10,7 +10,7 @@ final class LibsqlConformanceGuard { static final String EXPECTED_DRIVER_NAME = "libSQL JDBC Driver"; - static final String EXPECTED_DATABASE_PRODUCT_NAME = "SQLite (libSQL)"; + static final String EXPECTED_DATABASE_PRODUCT_NAME = "SQLite"; static final String LIBSQL_SOURCE_ID_SUFFIX = "alt1"; private static final String PROBE_URL = "jdbc:sqlite::memory:"; diff --git a/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuardTest.java b/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuardTest.java index bac55e3..b298f27 100644 --- a/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuardTest.java +++ b/libsql-conformance/src/test/java/io/github/magnusp/libsql/conformance/LibsqlConformanceGuardTest.java @@ -18,7 +18,7 @@ void acceptsALibsqlConnection() throws Exception { Connection connection = connection( "libSQL JDBC Driver", - "SQLite (libSQL)", + "SQLite", "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f284" + "11c85dda156f82alt1"); @@ -43,7 +43,7 @@ void rejectsANonLibsqlServerAndNamesTheResolvedSourceId() { Connection connection = connection( "libSQL JDBC Driver", - "SQLite (libSQL)", + "SQLite", "2024-10-21 16:30:22 03a9703e27c44437c39363d0baf82db4ebc94538a0f284" + "11c85dda156f82"); diff --git a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/LibsqlJdbcIT.java b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/LibsqlJdbcIT.java index 2c91127..ebf0ad4 100644 --- a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/LibsqlJdbcIT.java +++ b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/LibsqlJdbcIT.java @@ -113,7 +113,7 @@ void testDatabaseMetaData() throws SQLException { } DatabaseMetaData meta = conn.getMetaData(); - assertThat(meta.getDatabaseProductName()).contains("libSQL"); + assertThat(meta.getDatabaseProductName()).isEqualTo("SQLite"); try (ResultSet rs = meta.getTables(null, null, "meta_table", null)) { assertThat(rs.next()).isTrue(); diff --git a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java index 56340ac..838509c 100644 --- a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java +++ b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java @@ -107,7 +107,7 @@ void failingStatementMapsToSqlExceptionAndLeavesTheConnectionUsable(String schem void metadataAndBatchWorkOverWebSocket(String scheme) throws SQLException { try (Connection connection = connect(scheme)) { DatabaseMetaData metaData = connection.getMetaData(); - assertThat(metaData.getDatabaseProductName()).isEqualTo("SQLite (libSQL)"); + assertThat(metaData.getDatabaseProductName()).isEqualTo("SQLite"); assertThat(metaData.getURL()).isEqualTo(url(scheme)); try (Statement statement = connection.createStatement()) { diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java index 51ebce6..dae8b75 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java @@ -204,7 +204,7 @@ public String getURL() throws SQLException { @Override public boolean nullsAreSortedAtEnd() throws SQLException { return false; } @Override - public String getDatabaseProductName() throws SQLException { return "SQLite (libSQL)"; } + public String getDatabaseProductName() throws SQLException { return "SQLite"; } @Override public String getDatabaseProductVersion() throws SQLException { return resolveDatabaseProductVersion(); } @Override diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java index 5ed536c..7beec79 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java @@ -277,11 +277,10 @@ void autoCommitInsertDoesNotExposeGeneratedIdentity() { } @Test - void jdbcUrlIsDetectedAsDefaultDialect() { + void jdbcUrlIsDetectedAsSqliteDialect() { assertThat(JDBCUtils.dialect(connection)) - .as("DatabaseMetaData reports 'SQLite (libSQL)', which jOOQ does not map to" - + " SQLDialect.SQLITE. Pass SQLDialect.SQLITE explicitly until the" - + " product name is exactly 'SQLite'.") - .isEqualTo(SQLDialect.DEFAULT); + .as("DatabaseMetaData reports exactly 'SQLite', so jOOQ maps the connection" + + " to SQLDialect.SQLITE without an explicit dialect.") + .isEqualTo(SQLDialect.SQLITE); } } diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/liquibase/LiquibaseIT.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/liquibase/LiquibaseIT.java index ea47a85..f50d60b 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/liquibase/LiquibaseIT.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/liquibase/LiquibaseIT.java @@ -6,7 +6,6 @@ import liquibase.database.Database; import liquibase.database.DatabaseFactory; import liquibase.database.core.SQLiteDatabase; -import liquibase.database.core.UnsupportedDatabase; import liquibase.database.jvm.JdbcConnection; import liquibase.resource.ClassLoaderResourceAccessor; import org.junit.jupiter.api.Test; @@ -30,12 +29,9 @@ * snapshot paths against a real sqld container. * *

Liquibase selects its SQLite implementation with - * {@code "SQLite".equalsIgnoreCase(getDatabaseProductName())}. This driver - * reports {@code "SQLite (libSQL)"}, so the tests that need Liquibase's SQLite - * SQL build a {@link SQLiteDatabase} directly. The gap is pinned in - * {@link #databaseFactoryFallsBackToUnsupportedDatabase()}; when the product - * name becomes exactly {@code SQLite}, switch {@link #liquibase} back to - * {@link DatabaseFactory#findCorrectDatabaseImplementation} and delete the pin. + * {@code "SQLite".equalsIgnoreCase(getDatabaseProductName())}. The driver + * reports exactly {@code SQLite}, so {@link DatabaseFactory} resolves + * {@link SQLiteDatabase} through its normal implementation selection. */ class LiquibaseIT extends LiquibaseSqldSupport { @@ -148,30 +144,20 @@ void changeLogSyncMarksChangesAsRunWithoutExecutingThem() throws Exception { } /** - * Pins the driver gap that stops Liquibase from selecting its SQLite - * dialect when an application lets Liquibase inspect the connection. - * - *

Liquibase's {@code SQLiteDatabase.isCorrectDatabaseImplementation} + * Liquibase's {@code SQLiteDatabase.isCorrectDatabaseImplementation} * compares the product name with {@code equalsIgnoreCase("SQLite")}. The - * driver reports {@code "SQLite (libSQL)"}, so Liquibase silently falls - * back to {@link UnsupportedDatabase} and emits generic DDL such as - * {@code INT GENERATED BY DEFAULT AS IDENTITY}, which sqld rejects. - * Returning exactly {@code "SQLite"} and keeping the libSQL identity in - * {@code getDriverName()} would close the gap. - * - *

This assertion describes current behavior, not desired behavior. If - * it starts failing because the product name changed, remove this test and - * the explicit {@link SQLiteDatabase} workaround in {@link #liquibase}. + * driver reports exactly that, so the database factory selects the SQLite + * dialect and no longer emits generic DDL that sqld rejects. */ @Test - void databaseFactoryFallsBackToUnsupportedDatabase() throws Exception { + void databaseFactoryResolvesTheSqliteImplementation() throws Exception { try (Connection conn = DriverManager.getConnection(jdbcUrl())) { - assertThat(conn.getMetaData().getDatabaseProductName()).isEqualTo("SQLite (libSQL)"); + assertThat(conn.getMetaData().getDatabaseProductName()).isEqualTo("SQLite"); Database database = DatabaseFactory.getInstance() .findCorrectDatabaseImplementation(new JdbcConnection(conn)); - assertThat(database).isInstanceOf(UnsupportedDatabase.class); + assertThat(database).isInstanceOf(SQLiteDatabase.class); } } @@ -195,8 +181,8 @@ void databaseFactoryCannotResolveALibsqlUrlToADriver() { } private static Liquibase liquibase(Connection conn, String changelog) throws Exception { - Database database = new SQLiteDatabase(); - database.setConnection(new JdbcConnection(conn)); + Database database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation(new JdbcConnection(conn)); return new Liquibase(changelog, new ClassLoaderResourceAccessor(), database); } From 4ba311d18c7c2103c710804b0d64933a8201a2da Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sat, 31 Oct 2026 01:50:38 +0100 Subject: [PATCH 2/4] test(jooq): pin jOOQ's URL-based dialect detection jOOQ 3.21 resolves JDBCUtils.dialect(Connection) from DatabaseMetaData.getURL(), not from the database product name. The driver reports its transport endpoint, so jOOQ still returns SQLDialect.DEFAULT for a jdbc:libsql: connection and callers must pass SQLDialect.SQLITE explicitly. Correct the previous pin, which assumed the product name drove dialect detection. --- .../io/github/magnusp/libsql/jdbc/jooq/JooqIT.java | 10 ++++++---- .../magnusp/libsql/jdbc/jooq/JooqSqldSupport.java | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java index 7beec79..d01f4e1 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java @@ -277,10 +277,12 @@ void autoCommitInsertDoesNotExposeGeneratedIdentity() { } @Test - void jdbcUrlIsDetectedAsSqliteDialect() { + void dialectDetectionUsesTheUrlNotTheProductName() { assertThat(JDBCUtils.dialect(connection)) - .as("DatabaseMetaData reports exactly 'SQLite', so jOOQ maps the connection" - + " to SQLDialect.SQLITE without an explicit dialect.") - .isEqualTo(SQLDialect.SQLITE); + .as("jOOQ 3.21 derives the dialect from DatabaseMetaData.getURL(), not from" + + " the product name. The driver reports its transport endpoint there," + + " so jOOQ returns SQLDialect.DEFAULT; pass SQLDialect.SQLITE" + + " explicitly.") + .isEqualTo(SQLDialect.DEFAULT); } } diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqSqldSupport.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqSqldSupport.java index 958dbad..77a28b0 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqSqldSupport.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqSqldSupport.java @@ -52,11 +52,15 @@ abstract class JooqSqldSupport { .withStartupTimeout(Duration.ofSeconds(60))); static String jdbcUrl() { - return "jdbc:libsql:http://" + LIBSQL.getHost() + ":" + LIBSQL.getMappedPort(PORT); + return "jdbc:libsql:" + httpUrl(); + } + + static String httpUrl() { + return "http://" + LIBSQL.getHost() + ":" + LIBSQL.getMappedPort(PORT); } static void createSchema() { - String baseUrl = "http://" + LIBSQL.getHost() + ":" + LIBSQL.getMappedPort(PORT); + String baseUrl = httpUrl(); String requests = SCHEMA.stream() .map(sql -> "{\"type\":\"execute\",\"stmt\":{\"sql\":\"" + escape(sql) + "\"}}") .collect(Collectors.joining(",", "", ",{\"type\":\"close\"}")); From 116a0b24784ccc2da39399261d28fca04fb8ec93 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sat, 31 Oct 2026 03:01:38 +0100 Subject: [PATCH 3/4] fix(jdbc): report the JDBC URL from DatabaseMetaData Preserve the URL passed to the driver and return it from getURL() instead of the transport endpoint. URL-based consumers can now see the jdbc:... prefix: jOOQ detects SQLDialect.SQLITE for a jdbc:sqlite: connection, while jdbc:libsql: still needs an explicit dialect because jOOQ has no libsql prefix. Closes #274. --- .../libsql/itest/WebSocketTransportIT.java | 2 +- .../magnusp/libsql/jdbc/LibsqlConnection.java | 13 +++++++++++++ .../libsql/jdbc/LibsqlDatabaseMetaData.java | 2 +- .../magnusp/libsql/jdbc/LibsqlDriver.java | 2 +- .../libsql/jdbc/LibsqlConnectionTest.java | 11 +++++++++++ .../github/magnusp/libsql/jdbc/jooq/JooqIT.java | 17 ++++++++++++----- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java index 838509c..68d260a 100644 --- a/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java +++ b/libsql-itests/src/test/java/io/github/magnusp/libsql/itest/WebSocketTransportIT.java @@ -108,7 +108,7 @@ void metadataAndBatchWorkOverWebSocket(String scheme) throws SQLException { try (Connection connection = connect(scheme)) { DatabaseMetaData metaData = connection.getMetaData(); assertThat(metaData.getDatabaseProductName()).isEqualTo("SQLite"); - assertThat(metaData.getURL()).isEqualTo(url(scheme)); + assertThat(metaData.getURL()).isEqualTo("jdbc:libsql:" + url(scheme)); try (Statement statement = connection.createStatement()) { statement.execute( diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlConnection.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlConnection.java index def5980..b7599ec 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlConnection.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlConnection.java @@ -58,6 +58,9 @@ public class LibsqlConnection implements Connection { private final LibsqlClientConfig config; private final LibsqlClient client; + /** The JDBC URL the caller connected with, kept for {@code DatabaseMetaData.getURL()}. */ + private final String jdbcUrl; + /** Guards every field below and every transport round trip. See the class javadoc. */ private final Object stateLock = new Object(); @@ -102,14 +105,24 @@ public static TransactionMode from(String mode) { // Interactive stream state lives in the client; see LibsqlClient.hasOpenStream(). public LibsqlConnection(LibsqlClientConfig config, LibsqlClient client) { + this(config, client, null); + } + + public LibsqlConnection(LibsqlClientConfig config, LibsqlClient client, String jdbcUrl) { this.config = config; this.client = client; + this.jdbcUrl = jdbcUrl; } public LibsqlClientConfig getConfig() { return config; } + /** The JDBC URL used to open this connection, or the transport endpoint for test doubles. */ + String getJdbcUrl() { + return jdbcUrl != null ? jdbcUrl : (config != null ? config.url() : null); + } + public void checkClosed() throws SQLException { synchronized (stateLock) { if (closed) { diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java index dae8b75..5cf7474 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDatabaseMetaData.java @@ -189,7 +189,7 @@ private String resolveDatabaseProductVersion() throws SQLException { public boolean allTablesAreSelectable() throws SQLException { return true; } @Override public String getURL() throws SQLException { - return connection.getConfig() != null ? connection.getConfig().url() : null; + return connection.getJdbcUrl(); } @Override public String getUserName() throws SQLException { return null; } diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java index d62bd80..f557c9a 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java @@ -45,7 +45,7 @@ public Connection connect(String url, Properties info) throws SQLException { } else { client = new LibsqlHttpClient(config); } - return new LibsqlConnection(config, client); + return new LibsqlConnection(config, client, url); } private static boolean isWebSocketUrl(String url) { diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlConnectionTest.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlConnectionTest.java index 33a3bef..93f1b90 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlConnectionTest.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlConnectionTest.java @@ -54,6 +54,17 @@ void getMetaDataIsCachedPerConnection() throws SQLException { assertThat(other.getMetaData()).isNotSameAs(connection.getMetaData()); } + @Test + void metadataReportsTheJdbcUrlUsedToConnect() throws SQLException { + LibsqlConnection connection = new LibsqlConnection( + CONFIG, + new ScriptedClient((request, index) -> successResponse(request)), + "jdbc:sqlite:http://localhost:1"); + + assertThat(connection.getMetaData().getURL()) + .isEqualTo("jdbc:sqlite:http://localhost:1"); + } + @Test void disablingAutoCommitDefersTheBeginUntilTheNextStatement() throws SQLException { ScriptedClient client = new ScriptedClient((request, index) -> successResponse(request)); diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java index d01f4e1..305947a 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/jooq/JooqIT.java @@ -277,12 +277,19 @@ void autoCommitInsertDoesNotExposeGeneratedIdentity() { } @Test - void dialectDetectionUsesTheUrlNotTheProductName() { + void libsqlUrlIsNotDetectedByJooq() { assertThat(JDBCUtils.dialect(connection)) - .as("jOOQ 3.21 derives the dialect from DatabaseMetaData.getURL(), not from" - + " the product name. The driver reports its transport endpoint there," - + " so jOOQ returns SQLDialect.DEFAULT; pass SQLDialect.SQLITE" - + " explicitly.") + .as("jOOQ 3.21 derives the dialect from DatabaseMetaData.getURL(), and it" + + " has no jdbc:libsql: prefix. Pass SQLDialect.SQLITE explicitly.") .isEqualTo(SQLDialect.DEFAULT); } + + @Test + void sqliteAliasUrlIsDetectedAsSqliteDialect() throws Exception { + try (Connection alias = DriverManager.getConnection("jdbc:sqlite:" + httpUrl())) { + assertThat(JDBCUtils.dialect(alias)) + .as("getURL() preserves the jdbc:sqlite: prefix, which jOOQ recognizes") + .isEqualTo(SQLDialect.SQLITE); + } + } } From eebe0e9911f422bee7b2176c97ff538f6ec34f5d Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Sat, 31 Oct 2026 03:08:01 +0100 Subject: [PATCH 4/4] fix(jdbc): redact credentials from the metadata URL DatabaseMetaData.getURL() must not expose auth tokens or user info from the JDBC URL. Strip credential query parameters and user info before storing the URL that getURL() reports. --- .../magnusp/libsql/jdbc/LibsqlDriver.java | 2 +- .../magnusp/libsql/jdbc/LibsqlUrlParser.java | 40 +++++++++++++++++++ .../libsql/jdbc/LibsqlUrlParserTest.java | 23 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java index f557c9a..4fab91e 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlDriver.java @@ -45,7 +45,7 @@ public Connection connect(String url, Properties info) throws SQLException { } else { client = new LibsqlHttpClient(config); } - return new LibsqlConnection(config, client, url); + return new LibsqlConnection(config, client, LibsqlUrlParser.metadataUrl(url)); } private static boolean isWebSocketUrl(String url) { diff --git a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParser.java b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParser.java index e0ebbbe..d0231e1 100644 --- a/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParser.java +++ b/libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParser.java @@ -5,16 +5,24 @@ import java.net.URI; import java.sql.SQLException; import java.time.Duration; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.regex.Pattern; public class LibsqlUrlParser { public static final String PREFIX = "jdbc:libsql:"; public static final String SQLITE_PREFIX = "jdbc:sqlite:"; + private static final Set CREDENTIAL_QUERY_PARAMS = + Set.of("authToken", "jwt", "password", "token"); + private static final Pattern USER_INFO = Pattern.compile("^(jdbc:[a-zA-Z]+:[a-zA-Z]+://)[^/@]*@"); + public static boolean acceptsUrl(String url) { if (url == null) return false; if (url.startsWith(PREFIX)) return true; @@ -41,6 +49,38 @@ public static LibsqlClientConfig parse(String url, Properties info) throws SQLEx buildEndpoint(uri, scheme), authToken, connectTimeout, busyTimeout, encoding); } + /** + * Returns the URL for {@code DatabaseMetaData.getURL()} without credentials. The driver + * accepts an auth token in the query string, so returning the raw URL would leak it to any + * consumer that inspects metadata. + */ + static String metadataUrl(String url) { + if (url == null) { + return null; + } + String redacted = USER_INFO.matcher(url).replaceFirst("$1"); + int queryStart = redacted.indexOf('?'); + if (queryStart < 0) { + return redacted; + } + String query = redacted.substring(queryStart + 1); + String[] pairs = query.split("&"); + List kept = new ArrayList<>(pairs.length); + for (String pair : pairs) { + int equals = pair.indexOf('='); + String key = equals >= 0 ? pair.substring(0, equals) : pair; + if (!CREDENTIAL_QUERY_PARAMS.contains(key)) { + kept.add(pair); + } + } + if (kept.size() == pairs.length) { + return redacted; + } + return kept.isEmpty() + ? redacted.substring(0, queryStart) + : redacted.substring(0, queryStart) + "?" + String.join("&", kept); + } + private static URI normalizeUri(String url) throws SQLException { String rawUriStr = url.startsWith(PREFIX) ? url.substring(PREFIX.length()) diff --git a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParserTest.java b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParserTest.java index da68640..04bd891 100644 --- a/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParserTest.java +++ b/libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LibsqlUrlParserTest.java @@ -134,4 +134,27 @@ void testAcceptsWebSocketUrlForms() { assertThat(LibsqlUrlParser.acceptsUrl("jdbc:sqlite:wss://localhost:8080")).isTrue(); assertThat(LibsqlUrlParser.acceptsUrl("jdbc:other:ws://localhost:8080")).isFalse(); } + + @Test + void testMetadataUrlRedactsAuthenticationQueryParameters() { + assertThat(LibsqlUrlParser.metadataUrl( + "jdbc:libsql:http://localhost:8080/dev/tenant" + + "?authToken=secretToken&connectTimeout=5000")) + .isEqualTo("jdbc:libsql:http://localhost:8080/dev/tenant?connectTimeout=5000"); + assertThat(LibsqlUrlParser.metadataUrl( + "jdbc:libsql:http://localhost:8080?jwt=secretToken&password=secret")) + .isEqualTo("jdbc:libsql:http://localhost:8080"); + } + + @Test + void testMetadataUrlRedactsUserInfo() { + assertThat(LibsqlUrlParser.metadataUrl("jdbc:libsql:http://user:pass@localhost:8080/dev")) + .isEqualTo("jdbc:libsql:http://localhost:8080/dev"); + } + + @Test + void testMetadataUrlLeavesANonCredentialUrlUnchanged() { + String url = "jdbc:sqlite:http://localhost:8080/dev/tenant?connectTimeout=5000"; + assertThat(LibsqlUrlParser.metadataUrl(url)).isEqualTo(url); + } }