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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -93,27 +95,34 @@ public Connection getConnection(ConnectInfo connectInfo) {
}

private String setDatabaseInJdbcUrl(ConnectInfo connectInfo) {
String schemaName = connectInfo.getSchemaName();
String url = connectInfo.getUrl();
if (StringUtils.isBlank(schemaName)) {
return replaceDatabaseInJdbcUrl(connectInfo.getUrl(), connectInfo.getSchemaName());
}

static String replaceDatabaseInJdbcUrl(String url, String schemaName) {
if (StringUtils.isBlank(url) || StringUtils.isBlank(schemaName)) {
return url;
}
String connectAddress = connectInfo.getHost() + ":" + connectInfo.getPort();
String[] addressSplit = url.split(connectAddress);
if(addressSplit == null){

int queryStart = url.indexOf('?');
int fragmentStart = url.indexOf('#');
int suffixStart = queryStart >= 0 ? queryStart : url.length();
if (fragmentStart >= 0) {
suffixStart = Math.min(suffixStart, fragmentStart);
}
int authorityStart = url.indexOf("://");
if (authorityStart < 0 || authorityStart + 3 >= suffixStart) {
return url;
}
StringBuilder newUrl = new StringBuilder();
newUrl.append(addressSplit[0]).append(connectAddress).append("/").append(schemaName);
if (addressSplit.length == 2) {
if (StringUtils.isNotBlank(addressSplit[1])) {
String[] param = addressSplit[1].split("\\?");
if (param.length == 2) {
newUrl.append("?").append(param[1]);
}
}

// The path starts at the first '/' after the authority. A '/' inside userinfo is only
// expressible URL-encoded (%2F, RFC 3986); a raw one there is unparseable and must not
// be second-guessed, otherwise legal paths containing '@' would be misread as userinfo.
int pathStart = url.indexOf('/', authorityStart + 3);
if (pathStart < 0 || pathStart >= suffixStart) {
pathStart = suffixStart;
}
return newUrl.toString();
String encodedSchemaName = URLEncoder.encode(schemaName, StandardCharsets.UTF_8).replace("+", "%20");
return url.substring(0, pathStart) + "/" + encodedSchemaName + url.substring(suffixStart);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package ai.chat2db.plugin.clickhouse;

import ai.chat2db.spi.model.datasource.ConnectInfo;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;

import static org.junit.jupiter.api.Assertions.assertEquals;

class ClickHouseDBManagerConnectionUrlEncodingTest {

@Test
void encodesReservedUrlDelimitersInDatabaseName() throws Exception {
assertEquals(
"jdbc:clickhouse://localhost:8123/analytics%2Feast%3Fmode%23blue?ssl=true",
rewriteDatabase("analytics/east?mode#blue")
);
}

@Test
void encodesSpacesAndUnicodeInDatabaseName() throws Exception {
assertEquals(
"jdbc:clickhouse://localhost:8123/%E5%88%86%E6%9E%90%20%E5%BA%93?ssl=true",
rewriteDatabase("分析 库")
);
}

@Test
void encodesLiteralPercentAndPlusInDatabaseName() throws Exception {
assertEquals(
"jdbc:clickhouse://localhost:8123/already%252F%2Bname?ssl=true",
rewriteDatabase("already%2F+name")
);
}

@Test
void encodesDatabaseSegmentWhilePreservingIpv6AuthorityQueryAndFragment() {
assertEquals(
"jdbc:clickhouse://[2001:db8::1]:8123/analytics%2Feast%3Fmode%23blue?ssl=true#replica/tag",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://[2001:db8::1]:8123/default?ssl=true#replica/tag",
"analytics/east?mode#blue")
);
}

@Test
void rewritesPathBehindEncodedUserInfo() {
assertEquals(
"jdbc:clickhouse://user%2Fp:pass@host:8123/newdb",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://user%2Fp:pass@host:8123/olddb", "newdb")
);
}

@Test
void rewritesPathSegmentContainingAtSign() {
assertEquals(
"jdbc:clickhouse://host:8123/newdb",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://host:8123/db@x", "newdb")
);
}

@Test
void keepsStarLiteralAndEncodesTildeInDatabaseName() {
assertEquals(
"jdbc:clickhouse://host:8123/a*b%7Ec",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://host:8123/olddb", "a*b~c")
);
}

private static String rewriteDatabase(String databaseName) throws Exception {
ConnectInfo connectInfo = new ConnectInfo();
connectInfo.setUrl("jdbc:clickhouse://localhost:8123/default?ssl=true");
connectInfo.setHost("localhost");
connectInfo.setPort(8123);
connectInfo.setSchemaName(databaseName);

Method method = ClickHouseDBManager.class.getDeclaredMethod("setDatabaseInJdbcUrl", ConnectInfo.class);
method.setAccessible(true);
return (String) method.invoke(new ClickHouseDBManager(), connectInfo);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ai.chat2db.plugin.clickhouse;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class ClickHouseDBManagerConnectionUrlTest {

@Test
void replacesDatabasePathForBracketedIpv6AndPreservesQueryAndFragment() {
assertEquals(
"jdbc:clickhouse://[2001:db8::1]:8123/analytics?ssl=true#replica/tag",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://[2001:db8::1]:8123/default?ssl=true#replica/tag",
"analytics")
);
}

@Test
void repeatedReplacementIsIdempotent() {
String expected = "jdbc:clickhouse://[2001:db8::1]:8123/analytics?ssl=true";
String first = ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://[2001:db8::1]:8123/default?ssl=true",
"analytics");

assertEquals(expected, first);
assertEquals(expected, ClickHouseDBManager.replaceDatabaseInJdbcUrl(first, "analytics"));
}

@Test
void appendsDatabasePathWhenHostnameUrlHasNoPath() {
assertEquals(
"jdbc:clickhouse://clickhouse.example:8123/analytics",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://clickhouse.example:8123",
"analytics")
);
}

@Test
void appendsDatabasePathBeforeFragmentWhenUrlHasNoPath() {
assertEquals(
"jdbc:clickhouse://clickhouse.example:8123/analytics#replica/tag",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://clickhouse.example:8123#replica/tag",
"analytics")
);
}

@Test
void replacesExistingDatabasePathAndPreservesFragment() {
assertEquals(
"jdbc:clickhouse://clickhouse.example:8123/analytics#replica/tag",
ClickHouseDBManager.replaceDatabaseInJdbcUrl(
"jdbc:clickhouse://clickhouse.example:8123/default#replica/tag",
"analytics")
);
}
}
Loading