Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,9 @@ public class LibSqlConstants {
public static final String CONNECTION_URL_EXAMPLES = "jdbc:dbeaver:libsql:<server-url>, 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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<String, Object> 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);
}

Expand All @@ -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<String, Object> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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<String, Object> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -54,16 +55,25 @@ 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 +
"." + LibSqlConstants.DRIVER_VERSION_MAJOR +
"." + 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()
Expand All @@ -88,6 +98,7 @@ public LibSqlExecutionResult execute(String stmt, Map<Object, Object> parameters
/**
* Execute a batch of SQL statements.
*/
@NotNull
public LibSqlExecutionResult[] executeBatch(
@NotNull String[] stmts,
@Nullable Map<Object, Object>[] parameters) throws SQLException {
Expand All @@ -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<String> readerBodyHandler =
info -> HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
Expand Down Expand Up @@ -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 += "/";
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
}
}
Loading
Loading