diff --git a/README.md b/README.md index f24120a..25fb5dc 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,29 @@ Token based authentication supported in version 1.0. Pass token value as passwor Driver class name: `com.dbeaver.jdbc.driver.libsql.LibSqlDriver` +### Database namespaces (multitenancy) + +For a standalone `sqld` server with namespaces enabled, select an existing database using the optional +`db_name` connection property or URL parameter: + +```java +Properties properties = new Properties(); +properties.setProperty("db_name", "testdb"); +properties.setProperty("password", token); // Omit for servers without authentication +Connection connection = DriverManager.getConnection("jdbc:dbeaver:libsql:http://localhost:8080", properties); +``` + +Equivalent URL: `jdbc:dbeaver:libsql:http://localhost:8080?db_name=testdb`. +The Turso URL form also accepts this parameter: `libsql://server.example?db_name=testdb`. +URL-encode database names when passing them as URL parameters. An explicit connection property takes precedence +over the URL parameter. The driver sends the UTF-8 database name as unpadded standard Base64 in the +`x-namespace-bin` HTTP header (supported by stable and newer sqld releases), not as SQL schema selection. +If `db_name` is omitted or empty, existing server-default/hostname-based routing is preserved. + +In DBeaver, enter the server URL and the optional **Database** name in the LibSQL connection settings. +Leave Database empty when the database is already selected by the hostname (for example, a Turso Cloud URL). +Namespaces must already exist; the driver does not create them. + ## Example ```java diff --git a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnection.java b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnection.java index 1f692e0..ce67297 100644 --- a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnection.java +++ b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnection.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,8 @@ public LibSqlConnection( try { String token = CommonUtils.toString(driverProperties.get("password"), null); - this.client = new LibSqlClient(new URL(url), token); + String databaseName = CommonUtils.toString(driverProperties.get(LibSqlConstants.PROP_DATABASE_NAME), null); + this.client = new LibSqlClient(new URL(url), token, databaseName); } catch (IOException e) { throw new SQLException(e); } diff --git a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConstants.java b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConstants.java index e82f5ca..8d8dcfd 100644 --- a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConstants.java +++ b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,9 @@ public class LibSqlConstants { public static final String CONNECTION_URL_EXAMPLES = "jdbc:dbeaver:libsql:, libsql://"; public static final Pattern CONNECTION_URL_PATTERN = Pattern.compile("(jdbc:dbeaver:libsql:|libsql://)(.+)"); + public static final String PROP_DATABASE_NAME = "db_name"; + public static final String HEADER_NAMESPACE = "x-namespace-bin"; + public static final int DRIVER_VERSION_MAJOR = 1; public static final int DRIVER_VERSION_MINOR = 0; public static final int DRIVER_VERSION_MICRO = 2; diff --git a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlDriver.java b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlDriver.java index 84c9f43..3ad8cbb 100644 --- a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlDriver.java +++ b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlDriver.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ */ package com.dbeaver.jdbc.driver.libsql; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import java.sql.*; import java.util.Enumeration; @@ -40,14 +42,14 @@ public class LibSqlDriver implements Driver { private static final java.util.logging.Logger parentLogger; @Override - public Connection connect(String url, Properties info) throws SQLException { - String targetUrl = LibSqlUtils.validateAndFormatUrl(url); - + @NotNull + public Connection connect(@NotNull String url, @NotNull Properties info) throws SQLException { Map props = new LinkedHashMap<>(); for (Enumeration pne = info.propertyNames(); pne.hasMoreElements(); ) { String propName = (String) pne.nextElement(); - props.put(propName, info.get(propName)); + props.put(propName, info.getOrDefault(propName, info.getProperty(propName))); } + String targetUrl = LibSqlUtils.parseUrlProperties(url, props); return new LibSqlConnection(this, targetUrl, props); } @@ -57,10 +59,20 @@ public boolean acceptsURL(String url) { } @Override - public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) { - return new DriverPropertyInfo[] { - - }; + @NotNull + public DriverPropertyInfo[] getPropertyInfo(@Nullable String url, @NotNull Properties info) throws SQLException { + Map props = new LinkedHashMap<>(); + String databaseName = info.getProperty(LibSqlConstants.PROP_DATABASE_NAME); + if (databaseName != null) { + props.put(LibSqlConstants.PROP_DATABASE_NAME, databaseName); + } + if (url != null && !url.isEmpty()) { + LibSqlUtils.parseUrlProperties(url, props); + } + DriverPropertyInfo database = new DriverPropertyInfo( + LibSqlConstants.PROP_DATABASE_NAME, (String) props.get(LibSqlConstants.PROP_DATABASE_NAME)); + database.description = "Database namespace (optional; leave empty to use the server default)"; + return new DriverPropertyInfo[] { database }; } @Override diff --git a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlUtils.java b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlUtils.java index 4bcc5e3..7266617 100644 --- a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlUtils.java +++ b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/LibSqlUtils.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2025 DBeaver Corp and others + * Copyright (C) 2010-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,10 +18,16 @@ import org.jkiss.code.NotNull; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.Map; +import java.util.StringJoiner; import java.util.regex.Matcher; public class LibSqlUtils { @@ -89,4 +95,40 @@ public static String validateAndFormatUrl(@NotNull String url) throws LibSqlExce } return formattedUrl; } + + /** + * Extract JDBC-only URL properties without forwarding them to the HTTP server. + * Explicit connection properties take precedence over URL values. + */ + @NotNull + static String parseUrlProperties(@NotNull String url, @NotNull Map properties) throws LibSqlException { + String targetUrl = validateAndFormatUrl(url); + try { + URI uri = new URI(targetUrl); + String query = uri.getRawQuery(); + if (query == null) { + return targetUrl; + } + StringJoiner remaining = new StringJoiner("&"); + boolean found = false; + for (String parameter : query.split("&", -1)) { + String[] parts = parameter.split("=", 2); + if (LibSqlConstants.PROP_DATABASE_NAME.equals(URLDecoder.decode(parts[0], StandardCharsets.UTF_8))) { + String value = parts.length == 2 ? URLDecoder.decode(parts[1], StandardCharsets.UTF_8) : ""; + properties.putIfAbsent(LibSqlConstants.PROP_DATABASE_NAME, value); + found = true; + } else { + remaining.add(parameter); + } + } + if (!found) { + return targetUrl; + } + return targetUrl.substring(0, targetUrl.indexOf('?')) + + (remaining.length() == 0 ? "" : "?" + remaining) + + (uri.getRawFragment() == null ? "" : "#" + uri.getRawFragment()); + } catch (URISyntaxException | IllegalArgumentException e) { + throw new LibSqlException("Invalid connection URL", e); + } + } } diff --git a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/client/LibSqlClient.java b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/client/LibSqlClient.java index 27efdc0..5f6bafb 100644 --- a/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/client/LibSqlClient.java +++ b/com.dbeaver.jdbc.driver.libsql/src/main/java/com/dbeaver/jdbc/driver/libsql/client/LibSqlClient.java @@ -36,6 +36,7 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.util.Base64; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.ExecutorService; @@ -54,6 +55,8 @@ public class LibSqlClient { private final URL url; private final String authToken; + @Nullable + private final String namespaceHeader; private final ExecutorService clientExecutor; private String userAgent = LibSqlConstants.DRIVER_INFO + " " + LibSqlConstants.DRIVER_VERSION_MAJOR + @@ -61,9 +64,16 @@ public class LibSqlClient { "." + LibSqlConstants.DRIVER_VERSION_MICRO; private final HttpClient client; - public LibSqlClient(URL url, String authToken) { + public LibSqlClient(@NotNull URL url, @Nullable String authToken) { + this(url, authToken, null); + } + + public LibSqlClient(@NotNull URL url, @Nullable String authToken, @Nullable String databaseName) { this.url = url; this.authToken = authToken; + // sqld expects UTF-8 namespace bytes in unpadded standard Base64 metadata. + this.namespaceHeader = CommonUtils.isEmpty(databaseName) ? null + : Base64.getEncoder().withoutPadding().encodeToString(databaseName.getBytes(StandardCharsets.UTF_8)); this.clientExecutor = Executors.newSingleThreadExecutor(); HttpClient.Builder builder = HttpClient.newBuilder() @@ -88,6 +98,7 @@ public LibSqlExecutionResult execute(String stmt, Map parameters /** * Execute a batch of SQL statements. */ + @NotNull public LibSqlExecutionResult[] executeBatch( @NotNull String[] stmts, @Nullable Map[] parameters) throws SQLException { @@ -104,6 +115,9 @@ public LibSqlExecutionResult[] executeBatch( if (!CommonUtils.isEmpty(authToken)) { builder.header(HttpConstants.HEADER_AUTHORIZATION, "Bearer " + authToken); } + if (namespaceHeader != null) { + builder.header(LibSqlConstants.HEADER_NAMESPACE, namespaceHeader); + } HttpResponse.BodyHandler readerBodyHandler = info -> HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8); @@ -156,7 +170,8 @@ public LibSqlExecutionResult[] executeBatch( } - public HttpURLConnection openSimpleConnection(String endpoint) throws IOException { + @NotNull + public HttpURLConnection openSimpleConnection(@NotNull String endpoint) throws IOException { String baseURL = url.toString(); if (!baseURL.endsWith("/")) { baseURL += "/"; @@ -167,6 +182,9 @@ public HttpURLConnection openSimpleConnection(String endpoint) throws IOExceptio if (!CommonUtils.isEmpty(authToken)) { connection.setRequestProperty(HttpConstants.HEADER_AUTHORIZATION, "Bearer " + authToken); } + if (namespaceHeader != null) { + connection.setRequestProperty(LibSqlConstants.HEADER_NAMESPACE, namespaceHeader); + } return connection; } diff --git a/com.dbeaver.jdbc.driver.libsql/src/test/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnectionPropertiesTest.java b/com.dbeaver.jdbc.driver.libsql/src/test/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnectionPropertiesTest.java new file mode 100644 index 0000000..a6489b6 --- /dev/null +++ b/com.dbeaver.jdbc.driver.libsql/src/test/java/com/dbeaver/jdbc/driver/libsql/LibSqlConnectionPropertiesTest.java @@ -0,0 +1,98 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * 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 com.dbeaver.jdbc.driver.libsql; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.sql.DriverPropertyInfo; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +public class LibSqlConnectionPropertiesTest { + @Test + public void extractsNamespaceWithoutChangingOtherUrlComponents() throws Exception { + Map properties = new HashMap<>(); + Assertions.assertEquals( + "https://server.example:8443/proxy/?option=a%2Bb&empty=#fragment", + LibSqlUtils.parseUrlProperties( + "libsql://server.example:8443/proxy/?option=a%2Bb&db_name=test%2Bdb&empty=#fragment", properties)); + Assertions.assertEquals("test+db", properties.get("db_name")); + } + + @Test + public void removesJdbcOnlyQueryWhenItIsTheOnlyParameter() throws Exception { + Map properties = new HashMap<>(); + Assertions.assertEquals("http://localhost:8080", LibSqlUtils.parseUrlProperties( + "jdbc:dbeaver:libsql:http://localhost:8080?db_name=testdb", properties)); + Assertions.assertEquals("testdb", properties.get("db_name")); + } + + @Test + public void preservesUrlsWithoutNamespace() throws Exception { + for (String suffix : new String[] {"", "/", "?option=a%2Bb&empty=", "#db_name=testdb"}) { + Map properties = new HashMap<>(); + Assertions.assertEquals("https://server.example" + suffix, LibSqlUtils.parseUrlProperties( + "libsql://server.example" + suffix, properties)); + Assertions.assertTrue(properties.isEmpty()); + } + } + + @Test + public void explicitPropertiesOverrideAllUrlValuesIncludingWhenEmpty() throws Exception { + for (String database : new String[] {"from-properties", ""}) { + Map properties = new HashMap<>(Map.of("db_name", database)); + Assertions.assertEquals("https://server.example", LibSqlUtils.parseUrlProperties( + "libsql://server.example?db_name=first&db_name=second", properties)); + Assertions.assertEquals(database, properties.get("db_name")); + } + } + + @Test + public void acceptsEmptyAndEncodedNamespaceParameters() throws Exception { + for (String parameter : new String[] {"db_name", "db_name=", "db%5Fname="}) { + Map properties = new HashMap<>(); + Assertions.assertEquals("https://server.example", LibSqlUtils.parseUrlProperties( + "libsql://server.example?" + parameter, properties)); + Assertions.assertEquals("", properties.get("db_name")); + } + } + + @Test + public void rejectsMalformedEncoding() { + Assertions.assertThrows(LibSqlException.class, () -> LibSqlUtils.parseUrlProperties( + "libsql://server.example?db_name=invalid%XX", new HashMap<>())); + } + + @Test + public void advertisesOptionalDatabasePropertyWithEffectiveValue() throws Exception { + LibSqlDriver driver = new LibSqlDriver(); + Properties properties = new Properties(); + DriverPropertyInfo database = driver.getPropertyInfo("libsql://server.example?db_name=from-url", properties)[0]; + Assertions.assertEquals("db_name", database.name); + Assertions.assertEquals("from-url", database.value); + Assertions.assertFalse(database.required); + Assertions.assertNotNull(database.description); + Assertions.assertTrue(properties.isEmpty()); + + properties.setProperty("db_name", "from-properties"); + Assertions.assertEquals("from-properties", + driver.getPropertyInfo("libsql://server.example?db_name=from-url", properties)[0].value); + Assertions.assertNull(driver.getPropertyInfo(null, new Properties())[0].value); + } +} diff --git a/com.dbeaver.jdbc.driver.libsql/src/test/java/com/dbeaver/jdbc/driver/libsql/LibSqlNamespaceTest.java b/com.dbeaver.jdbc.driver.libsql/src/test/java/com/dbeaver/jdbc/driver/libsql/LibSqlNamespaceTest.java new file mode 100644 index 0000000..c0d1e82 --- /dev/null +++ b/com.dbeaver.jdbc.driver.libsql/src/test/java/com/dbeaver/jdbc/driver/libsql/LibSqlNamespaceTest.java @@ -0,0 +1,180 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2026 DBeaver Corp and others + * + * 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 com.dbeaver.jdbc.driver.libsql; + +import com.dbeaver.jdbc.driver.libsql.client.LibSqlClient; +import com.sun.net.httpserver.HttpServer; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.Statement; +import java.util.Base64; +import java.util.Properties; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class LibSqlNamespaceTest { + private final BlockingQueue requests = new LinkedBlockingQueue<>(); + private HttpServer server; + + @BeforeEach + public void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + requests.add(new Request( + exchange.getRequestHeaders().getFirst("x-namespace-bin"), + exchange.getRequestHeaders().getFirst("Authorization"), + exchange.getRequestURI().toString(), + new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8))); + byte[] response = (exchange.getRequestMethod().equals("POST") + ? "[{\"results\":{\"columns\":[\"value\"],\"rows\":[[1]]}}]" + : "libsql 0.24.0 test").getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + try (var output = exchange.getResponseBody()) { + output.write(response); + } finally { + exchange.close(); + } + }); + server.start(); + } + + @AfterEach + public void stopServer() { + if (server != null) { + server.stop(0); + } + } + + @Test + public void namespaceAndTokenReachValidationStatementsBatchesAndMetadata() throws Exception { + Properties properties = new Properties(); + properties.setProperty("db_name", "tenant-one"); + properties.setProperty("password", "test-token"); + try (Connection connection = new LibSqlDriver().connect(jdbcUrl(), properties)) { + assertRequest("tenant-one", "Bearer test-token", "/"); + try (Statement statement = connection.createStatement()) { + statement.executeQuery("SELECT 1"); + assertRequest("tenant-one", "Bearer test-token", "/"); + } + try (PreparedStatement statement = connection.prepareStatement("SELECT ?")) { + statement.setInt(1, 42); + statement.executeQuery(); + Request request = assertRequest("tenant-one", "Bearer test-token", "/"); + Assertions.assertTrue(request.body().contains("\"params\":[42]")); + } + ((LibSqlConnection) connection).getClient().executeBatch(new String[] {"SELECT 1", "SELECT 2"}, null); + Request batch = assertRequest("tenant-one", "Bearer test-token", "/"); + Assertions.assertTrue(batch.body().contains("\"statements\":[\"SELECT 1\",\"SELECT 2\"]")); + Assertions.assertEquals("0.24.0", connection.getMetaData().getDatabaseProductVersion()); + assertRequest("tenant-one", "Bearer test-token", "/version"); + } + } + + @Test + public void urlNamespaceIsDecodedAndNotForwardedInQuery() throws Exception { + try (Connection connection = new LibSqlDriver().connect(jdbcUrl() + "?db_name=tenant%2Bone", new Properties())) { + Assertions.assertFalse(connection.isClosed()); + assertRequest("tenant+one", null, "/"); + } + } + + @Test + public void inheritedConnectionPropertyOverridesUrlWithoutMutatingCaller() throws Exception { + Properties defaults = new Properties(); + defaults.setProperty("db_name", "tenant-two"); + Properties properties = new Properties(defaults); + try (Connection connection = new LibSqlDriver().connect(jdbcUrl() + "?db_name=tenant-one", properties)) { + Assertions.assertFalse(connection.isClosed()); + assertRequest("tenant-two", null, "/"); + Assertions.assertTrue(properties.isEmpty()); + } + } + + @Test + public void namespaceIsIsolatedPerConnection() throws Exception { + LibSqlDriver driver = new LibSqlDriver(); + try (Connection first = driver.connect(jdbcUrl() + "?db_name=tenant-one", new Properties()); + Connection second = driver.connect(jdbcUrl() + "?db_name=tenant-two", new Properties())) { + assertRequest("tenant-one", null, "/"); + assertRequest("tenant-two", null, "/"); + try (Statement statement = first.createStatement()) { + statement.executeQuery("SELECT 1"); + assertRequest("tenant-one", null, "/"); + } + try (Statement statement = second.createStatement()) { + statement.executeQuery("SELECT 1"); + assertRequest("tenant-two", null, "/"); + } + } + } + + @Test + public void unspecifiedAndEmptyNamespacesPreserveDefaultRouting() throws Exception { + for (String suffix : new String[] {"", "?db_name="}) { + try (Connection connection = new LibSqlDriver().connect(jdbcUrl() + suffix, new Properties())) { + Assertions.assertFalse(connection.isClosed()); + assertRequest(null, null, "/"); + } + } + } + + @Test + public void legacyClientConstructorPreservesDefaultRouting() throws Exception { + LibSqlClient client = new LibSqlClient(URI.create(jdbcUrl().substring("jdbc:dbeaver:libsql:".length())).toURL(), null); + try { + client.execute("SELECT 1", null); + assertRequest(null, null, "/"); + } finally { + client.close(); + } + } + + @NotNull + private String jdbcUrl() { + return "jdbc:dbeaver:libsql:http://127.0.0.1:" + server.getAddress().getPort(); + } + + @NotNull + private Request assertRequest(@Nullable String namespace, @Nullable String authorization, @NotNull String path) + throws InterruptedException { + Request request = requests.poll(5, TimeUnit.SECONDS); + Assertions.assertNotNull(request); + Assertions.assertEquals(namespace, request.namespace() == null ? null + : new String(Base64.getDecoder().decode(request.namespace()), StandardCharsets.UTF_8)); + if (request.namespace() != null) { + Assertions.assertFalse(request.namespace().contains("=")); + } + Assertions.assertEquals(authorization, request.authorization()); + Assertions.assertEquals(path, request.path()); + return request; + } + + private record Request(@Nullable String namespace, @Nullable String authorization, @NotNull String path, @NotNull String body) { + } +}