Skip to content

Commit 99dfb3f

Browse files
committed
jdbc preserves path of endpoint
1 parent f0a88f2 commit 99dfb3f

File tree

3 files changed

+134
-12
lines changed

3 files changed

+134
-12
lines changed

jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,17 +188,53 @@ private Map<String, String> parseUrl(String url) throws SQLException {
188188
if (uri.getAuthority().contains(",")) {
189189
throw new SQLException("Multiple endpoints not supported");
190190
}
191-
properties.put(PARSE_URL_CONN_URL_PROP, uri.getScheme() + "://"
192-
+ uri.getRawAuthority()); // will be parsed again later
193191

194-
if (uri.getPath() != null
195-
&& !uri.getPath().trim().isEmpty()
196-
&& !"/".equals(uri.getPath()))
197-
{
198-
properties.put(
199-
ClientConfigProperties.DATABASE.getKey(),
200-
uri.getPath().substring(1));
192+
// Parse path: last segment is database name, everything before is HTTP path
193+
// Example: /proxy/path/mydb -> httpPath=/proxy/path, database=mydb
194+
// Example: /mydb -> httpPath=empty, database=mydb
195+
// Example: /sales/db -> httpPath=/sales, database=db
196+
// Use raw path for splitting to avoid issues with URL-encoded slashes (e.g., %2F)
197+
String rawPath = uri.getRawPath();
198+
String httpPath = "";
199+
String database = null;
200+
201+
if (rawPath != null && !rawPath.trim().isEmpty() && !"/".equals(rawPath)) {
202+
// Remove leading slash for processing
203+
String pathWithoutLeadingSlash = rawPath.startsWith("/") ? rawPath.substring(1) : rawPath;
204+
int lastSlashIndex = pathWithoutLeadingSlash.lastIndexOf('/');
205+
206+
if (lastSlashIndex > 0) {
207+
// Path has multiple segments: everything before last slash is HTTP path
208+
httpPath = "/" + pathWithoutLeadingSlash.substring(0, lastSlashIndex);
209+
// Decode the database name
210+
try {
211+
database = URLDecoder.decode(pathWithoutLeadingSlash.substring(lastSlashIndex + 1), StandardCharsets.UTF_8.name());
212+
} catch (UnsupportedEncodingException e) {
213+
throw new SQLException("Failed to decode database name", e);
214+
}
215+
} else {
216+
// Single segment: it's the database name, no HTTP path
217+
// Decode the database name
218+
try {
219+
database = URLDecoder.decode(pathWithoutLeadingSlash, StandardCharsets.UTF_8.name());
220+
} catch (UnsupportedEncodingException e) {
221+
throw new SQLException("Failed to decode database name", e);
222+
}
223+
}
224+
}
225+
226+
// Build connection URL with HTTP path preserved
227+
StringBuilder connectionUrl = new StringBuilder();
228+
connectionUrl.append(uri.getScheme()).append("://").append(uri.getRawAuthority());
229+
if (!httpPath.isEmpty()) {
230+
connectionUrl.append(httpPath);
201231
}
232+
properties.put(PARSE_URL_CONN_URL_PROP, connectionUrl.toString());
233+
234+
if (database != null && !database.trim().isEmpty()) {
235+
properties.put(ClientConfigProperties.DATABASE.getKey(), database);
236+
}
237+
202238
if (uri.getQuery() != null && !uri.getQuery().trim().isEmpty()) {
203239
for (String pair : uri.getRawQuery().split("&")) {
204240
try {

jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,4 +967,90 @@ public void testUseUserTimeZone() throws Exception {
967967
}
968968

969969
}
970+
971+
@Test(groups = {"integration"})
972+
public void testEndpointUrlPathIsPreserved() throws Exception {
973+
if (isCloud()) {
974+
return; // mocked server
975+
}
976+
977+
WireMockServer mockServer = new WireMockServer(WireMockConfiguration
978+
.options().port(9090).notifier(new ConsoleNotifier(false)));
979+
mockServer.start();
980+
981+
try {
982+
// From wireshark dump as C Array - response for SELECT currentUser() AS user, timezone() AS timezone, version() AS version LIMIT 1
983+
char selectServerInfo[] = {
984+
0x03, 0x04, 0x75, 0x73, 0x65, 0x72, 0x08, 0x74,
985+
0x69, 0x6d, 0x65, 0x7a, 0x6f, 0x6e, 0x65, 0x07,
986+
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x06,
987+
0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x06, 0x53,
988+
0x74, 0x72, 0x69, 0x6e, 0x67, 0x06, 0x53, 0x74,
989+
0x72, 0x69, 0x6e, 0x67, 0x07, 0x64, 0x65, 0x66,
990+
0x61, 0x75, 0x6c, 0x74, 0x03, 0x55, 0x54, 0x43,
991+
0x0b, 0x32, 0x34, 0x2e, 0x33, 0x2e, 0x31, 0x2e,
992+
0x32, 0x36, 0x37, 0x32};
993+
994+
char select1Res[] = {
995+
0x01, 0x01, 0x31, 0x05, 0x55, 0x49, 0x6e, 0x74,
996+
0x38, 0x01};
997+
998+
// URL format: jdbc:clickhouse://host:port/http_path/database
999+
// For /sales/db: http_path=/sales, database=db
1000+
// For /billing/db: http_path=/billing, database=db
1001+
1002+
// Setup stubs for sales virtual instance (path: /sales)
1003+
mockServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/sales"))
1004+
.withRequestBody(WireMock.matching(".*SELECT 1.*"))
1005+
.willReturn(WireMock.ok(new String(select1Res))
1006+
.withHeader("X-ClickHouse-Summary",
1007+
"{ \"read_bytes\": \"100\", \"read_rows\": \"10\"}")).build());
1008+
1009+
mockServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/sales"))
1010+
.withRequestBody(WireMock.equalTo("SELECT currentUser() AS user, timezone() AS timezone, version() AS version LIMIT 1"))
1011+
.willReturn(WireMock.ok(new String(selectServerInfo))
1012+
.withHeader("X-ClickHouse-Summary",
1013+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build());
1014+
1015+
// Setup stubs for billing virtual instance (path: /billing)
1016+
mockServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/billing"))
1017+
.withRequestBody(WireMock.matching(".*SELECT 2.*"))
1018+
.willReturn(WireMock.ok(new String(select1Res))
1019+
.withHeader("X-ClickHouse-Summary",
1020+
"{ \"read_bytes\": \"200\", \"read_rows\": \"20\"}")).build());
1021+
1022+
mockServer.addStubMapping(WireMock.post(WireMock.urlPathEqualTo("/billing"))
1023+
.withRequestBody(WireMock.equalTo("SELECT currentUser() AS user, timezone() AS timezone, version() AS version LIMIT 1"))
1024+
.willReturn(WireMock.ok(new String(selectServerInfo))
1025+
.withHeader("X-ClickHouse-Summary",
1026+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build());
1027+
1028+
Properties properties = new Properties();
1029+
properties.put("compress", "false");
1030+
1031+
// Test sales virtual instance: /sales/db means http_path=/sales, database=db
1032+
String salesJdbcUrl = "jdbc:clickhouse://localhost:" + mockServer.port() + "/sales/db";
1033+
try (Connection conn = new ConnectionImpl(salesJdbcUrl, properties);
1034+
Statement stmt = conn.createStatement();
1035+
ResultSet rs = stmt.executeQuery("SELECT 1")) {
1036+
Assert.assertTrue(rs.next());
1037+
Assert.assertEquals(rs.getInt(1), 1);
1038+
}
1039+
1040+
// Test billing virtual instance: /billing/db means http_path=/billing, database=db
1041+
String billingJdbcUrl = "jdbc:clickhouse://localhost:" + mockServer.port() + "/billing/db";
1042+
try (Connection conn = new ConnectionImpl(billingJdbcUrl, properties);
1043+
Statement stmt = conn.createStatement();
1044+
ResultSet rs = stmt.executeQuery("SELECT 2")) {
1045+
Assert.assertTrue(rs.next());
1046+
}
1047+
1048+
// Verify requests were made to the correct HTTP paths (/sales and /billing, not /sales/db)
1049+
mockServer.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/sales")));
1050+
mockServer.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/billing")));
1051+
1052+
} finally {
1053+
mockServer.stop();
1054+
}
1055+
}
9701056
}

jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ public void testParseURLValid(String jdbcURL, Properties properties,
123123
throws Exception
124124
{
125125
JdbcConfiguration configuration = new JdbcConfiguration(jdbcURL, properties);
126-
assertEquals(configuration.getConnectionUrl(), connectionURL);
127-
assertEquals(configuration.clientProperties, expectedClientProps);
126+
assertEquals(configuration.getConnectionUrl(), connectionURL, "URL: " + jdbcURL);
127+
assertEquals(configuration.clientProperties, expectedClientProps, "URL: " + jdbcURL);
128128
Client.Builder bob = new Client.Builder();
129129
configuration.applyClientProperties(bob);
130130
Client client = bob.build();
@@ -144,7 +144,7 @@ public void testParseURLInvalid(String jdbcURL) {
144144

145145
@Test(dataProvider = "validURLs")
146146
public void testAcceptsURLValid(String url) throws Exception {
147-
Assert.assertTrue(JdbcConfiguration.acceptsURL(url));
147+
Assert.assertTrue(JdbcConfiguration.acceptsURL(url), "URL: " + url);
148148
}
149149

150150
@Test

0 commit comments

Comments
 (0)