Skip to content

Commit 2ebf138

Browse files
committed
Added sending statement parameters in multipart data request
1 parent bbc1af2 commit 2ebf138

File tree

3 files changed

+94
-10
lines changed

3 files changed

+94
-10
lines changed

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
import org.apache.hc.core5.http.impl.io.DefaultHttpResponseParserFactory;
5151
import org.apache.hc.core5.http.io.SocketConfig;
5252
import org.apache.hc.core5.http.io.entity.EntityTemplate;
53+
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
54+
import java.io.ByteArrayOutputStream;
5355
import org.apache.hc.core5.http.protocol.HttpContext;
5456
import org.apache.hc.core5.io.CloseMode;
5557
import org.apache.hc.core5.io.IOCallback;
@@ -432,12 +434,45 @@ public ClassicHttpResponse executeRequest(Endpoint server, Map<String, Object> r
432434
// req.setVersion(new ProtocolVersion("HTTP", 1, 0)); // to disable chunk transfer encoding
433435
addHeaders(req, requestConfig);
434436

435-
436-
// setting entity. wrapping if compression is enabled
437-
String contentEncoding = req.containsHeader(HttpHeaders.CONTENT_ENCODING) ? req.getHeader(HttpHeaders.CONTENT_ENCODING).getValue() : null;
438-
req.setEntity(wrapRequestEntity(new EntityTemplate(-1, CONTENT_TYPE, contentEncoding , writeCallback),
439-
lz4Factory,
440-
requestConfig));
437+
// Check if statement params exist - if so, send as multipart
438+
boolean hasStatementParams = requestConfig.containsKey(KEY_STATEMENT_PARAMS);
439+
Map<?, ?> statementParams = hasStatementParams ?
440+
(Map<?, ?>) requestConfig.get(KEY_STATEMENT_PARAMS) : null;
441+
442+
HttpEntity requestEntity;
443+
String contentEncoding = req.containsHeader(HttpHeaders.CONTENT_ENCODING) ?
444+
req.getHeader(HttpHeaders.CONTENT_ENCODING).getValue() : null;
445+
446+
if (hasStatementParams && statementParams != null && !statementParams.isEmpty()) {
447+
// Create multipart entity with query body and statement params
448+
MultipartEntityBuilder multipartBuilder = MultipartEntityBuilder.create();
449+
450+
// Add query/body data as binary body
451+
if (writeCallback != null) {
452+
// Write callback output to buffer, then add as binary body
453+
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
454+
writeCallback.execute(buffer);
455+
byte[] queryData = buffer.toByteArray();
456+
multipartBuilder.addBinaryBody("query", queryData, CONTENT_TYPE, null);
457+
}
458+
459+
// Add statement params as multipart parts
460+
for (Map.Entry<?, ?> entry : statementParams.entrySet()) {
461+
String paramName = "param_" + entry.getKey().toString();
462+
String paramValue = String.valueOf(entry.getValue());
463+
multipartBuilder.addTextBody(paramName, paramValue);
464+
}
465+
466+
requestEntity = multipartBuilder.build();
467+
// Update content type header for multipart
468+
req.setHeader(HttpHeaders.CONTENT_TYPE, requestEntity.getContentType());
469+
} else {
470+
// No statement params - use regular entity template
471+
requestEntity = new EntityTemplate(-1, CONTENT_TYPE, contentEncoding, writeCallback);
472+
}
473+
474+
// Wrap entity with compression if enabled
475+
req.setEntity(wrapRequestEntity(requestEntity, lz4Factory, requestConfig));
441476

442477
HttpClientContext context = HttpClientContext.create();
443478
Number responseTimeout = ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getOrDefault(requestConfig);
@@ -595,10 +630,7 @@ private void addQueryParams(URIBuilder req, Map<String, Object> requestConfig) {
595630
if (requestConfig.containsKey(ClientConfigProperties.QUERY_ID.getKey())) {
596631
req.addParameter(ClickHouseHttpProto.QPARAM_QUERY_ID, requestConfig.get(ClientConfigProperties.QUERY_ID.getKey()).toString());
597632
}
598-
if (requestConfig.containsKey(KEY_STATEMENT_PARAMS)) {
599-
Map<?, ?> params = (Map<?, ?>) requestConfig.get(KEY_STATEMENT_PARAMS);
600-
params.forEach((k, v) -> req.addParameter("param_" + k, String.valueOf(v)));
601-
}
633+
// Statement params are now sent as multipart, not query params
602634

603635
boolean clientCompression = ClientConfigProperties.COMPRESS_CLIENT_REQUEST.getOrDefault(requestConfig);
604636
boolean serverCompression = ClientConfigProperties.COMPRESS_SERVER_RESPONSE.getOrDefault(requestConfig);

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
import java.util.Arrays;
4444
import java.util.Base64;
4545
import java.util.EnumSet;
46+
import java.util.HashMap;
4647
import java.util.List;
48+
import java.util.Map;
4749
import java.util.Random;
4850
import java.util.UUID;
4951
import java.util.concurrent.CompletableFuture;
@@ -1102,6 +1104,55 @@ public void testTimeoutsWithRetry() {
11021104
}
11031105
}
11041106

1107+
@Test(groups = {"integration"})
1108+
public void testStatementParamsSentAsMultipart() throws Exception {
1109+
if (isCloud()) {
1110+
return; // mocked server
1111+
}
1112+
1113+
WireMockServer mockServer = new WireMockServer(WireMockConfiguration
1114+
.options().port(9090).notifier(new ConsoleNotifier(false)));
1115+
mockServer.start();
1116+
1117+
try {
1118+
// Configure WireMock to capture multipart requests
1119+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
1120+
.withHeader(HttpHeaders.CONTENT_TYPE, WireMock.matching("multipart/form-data.*"))
1121+
.withRequestBody(WireMock.containing("param_testParam"))
1122+
.withRequestBody(WireMock.containing("testValue"))
1123+
.withRequestBody(WireMock.containing("param_anotherParam"))
1124+
.withRequestBody(WireMock.containing("123"))
1125+
.willReturn(WireMock.aResponse()
1126+
.withStatus(HttpStatus.SC_OK)
1127+
.withHeader("X-ClickHouse-Summary",
1128+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")
1129+
.withBody("1\n")).build());
1130+
1131+
try (Client client = new Client.Builder()
1132+
.addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false)
1133+
.setUsername("default")
1134+
.setPassword(ClickHouseServerForTest.getPassword())
1135+
.compressClientRequest(false)
1136+
.build()) {
1137+
1138+
// Create query with statement parameters
1139+
Map<String, Object> queryParams = new HashMap<>();
1140+
queryParams.put("testParam", "testValue");
1141+
queryParams.put("anotherParam", 123);
1142+
1143+
QueryResponse response = client.query(
1144+
"SELECT {testParam:String} as col1, {anotherParam:Int32} as col2",
1145+
queryParams).get(10, TimeUnit.SECONDS);
1146+
1147+
// Verify the request was made (WireMock will throw if expectations not met)
1148+
Assert.assertNotNull(response);
1149+
response.close();
1150+
}
1151+
} finally {
1152+
mockServer.stop();
1153+
}
1154+
}
1155+
11051156
@Test(groups = {"integration"})
11061157
public void testSNIWithCloud() throws Exception {
11071158
if (!isCloud()) {

client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ private static void delayForProfiler(long millis) {
138138
}
139139
}
140140

141+
141142
@Test(groups = {"integration"})
142143
public void testSimpleQueryWithTSV() {
143144
prepareSimpleDataSet();

0 commit comments

Comments
 (0)