-
Notifications
You must be signed in to change notification settings - Fork 614
✨ [client-v2] Statement parameters sent via multipart data request. #2693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 To request another review, post a new comment with "/windsurf-review".
| if (writeCallback != null) { | ||
| // Write callback output to buffer, then add as binary body | ||
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | ||
| writeCallback.execute(buffer); | ||
| byte[] queryData = buffer.toByteArray(); | ||
| multipartBuilder.addBinaryBody("query", queryData, CONTENT_TYPE, null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation buffers the entire query data in memory before sending it as part of the multipart request. For large queries, this could lead to OutOfMemoryError. Consider using a streaming approach for the query part of the multipart request to avoid memory issues with large queries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR modifies the HTTP client to send statement parameters as multipart form data instead of query parameters, addressing issue #2324. The change improves how parameterized queries are transmitted to the ClickHouse server.
Key changes:
- Migrated statement parameter transmission from URL query parameters to multipart form data
- Added integration test with WireMock to verify multipart parameter handling
- Removed legacy query parameter logic for statement parameters
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java | Implements multipart form data construction for statement parameters and removes old query parameter logic |
| client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java | Adds integration test to verify statement parameters are correctly sent as multipart data |
| client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java | Minor whitespace addition |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | ||
| writeCallback.execute(buffer); | ||
| byte[] queryData = buffer.toByteArray(); | ||
| multipartBuilder.addBinaryBody("query", queryData, CONTENT_TYPE, null); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ByteArrayOutputStream is not closed after use. While it doesn't hold system resources like file streams, it's good practice to use try-with-resources or explicitly close it to ensure proper resource management and prevent potential memory issues.
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | |
| writeCallback.execute(buffer); | |
| byte[] queryData = buffer.toByteArray(); | |
| multipartBuilder.addBinaryBody("query", queryData, CONTENT_TYPE, null); | |
| try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { | |
| writeCallback.execute(buffer); | |
| byte[] queryData = buffer.toByteArray(); | |
| multipartBuilder.addBinaryBody("query", queryData, CONTENT_TYPE, null); | |
| } |
|
|
||
| requestEntity = multipartBuilder.build(); | ||
| // Update content type header for multipart | ||
| req.setHeader(HttpHeaders.CONTENT_TYPE, requestEntity.getContentType()); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overwriting the Content-Type header may discard the content encoding that was previously set. If compression is enabled and a Content-Encoding header was added earlier, this replacement could cause issues with the multipart boundary. Consider preserving or merging headers appropriately.
| req.setHeader(HttpHeaders.CONTENT_TYPE, requestEntity.getContentType()); | |
| req.setHeader(HttpHeaders.CONTENT_TYPE, requestEntity.getContentType()); | |
| // Restore Content-Encoding header if it was present before | |
| if (contentEncoding != null) { | |
| req.setHeader(HttpHeaders.CONTENT_ENCODING, contentEncoding); | |
| } |
| // Write callback output to buffer, then add as binary body | ||
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | ||
| writeCallback.execute(buffer); | ||
| byte[] queryData = buffer.toByteArray(); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'query' part name in the multipart body should be documented. It's unclear if this is a ClickHouse protocol requirement or an implementation detail. Adding a comment explaining the expected part name would improve maintainability.
| byte[] queryData = buffer.toByteArray(); | |
| byte[] queryData = buffer.toByteArray(); | |
| // The part name "query" is required by the ClickHouse HTTP protocol for the main query body in multipart requests. | |
| // Do not change this unless the protocol changes, as the server expects this exact part name. |
| } | ||
| } | ||
|
|
||
|
|
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary blank line added. This appears to be unintentional whitespace that doesn't add value to the code.
| } | ||
|
|
||
| WireMockServer mockServer = new WireMockServer(WireMockConfiguration | ||
| .options().port(9090).notifier(new ConsoleNotifier(false))); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded port 9090 may cause test failures if the port is already in use. Consider using dynamic port allocation (port 0) and retrieving the actual port via mockServer.port() to avoid port conflicts in CI/CD environments.
| .options().port(9090).notifier(new ConsoleNotifier(false))); | |
| .options().port(0).notifier(new ConsoleNotifier(false))); |
|
|
||
| QueryResponse response = client.query( | ||
| "SELECT {testParam:String} as col1, {anotherParam:Int32} as col2", | ||
| queryParams).get(10, TimeUnit.SECONDS); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The timeout value of 10 seconds is a magic number. Consider extracting this to a named constant (e.g., TEST_TIMEOUT_SECONDS) for better maintainability and consistency across tests.
Client V2 CoverageCoverage Report
Class Coverage
|
JDBC V2 CoverageCoverage Report
Class Coverage
|
JDBC V1 CoverageCoverage Report
Class Coverage
|
Client V1 CoverageCoverage Report
Class Coverage
|
…dy to stream instead of copy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (mimeType != null && mimeType.contains("/")) { | ||
| return mimeType.substring(0, mimeType.indexOf('/')); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getMediaType() method uses contains('/') check before indexOf('/'), but both checks could fail for the same input. If contains('/') returns true, indexOf('/') is guaranteed to return a valid index >= 0, but using indexOf directly would be more efficient. Consider checking indexOf('/') >= 0 instead to avoid two string traversals.
| if (mimeType != null && mimeType.contains("/")) { | |
| return mimeType.substring(0, mimeType.indexOf('/')); | |
| if (mimeType != null) { | |
| int idx = mimeType.indexOf('/'); | |
| if (idx >= 0) { | |
| return mimeType.substring(0, idx); | |
| } |
| if (mimeType != null && mimeType.contains("/")) { | ||
| return mimeType.substring(mimeType.indexOf('/') + 1); |
Copilot
AI
Dec 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getSubType() method uses contains('/') check before indexOf('/'), but both checks could fail for the same input. If contains('/') returns true, indexOf('/') is guaranteed to return a valid index >= 0, but using indexOf directly would be more efficient. Consider checking indexOf('/') >= 0 instead to avoid two string traversals.
| if (mimeType != null && mimeType.contains("/")) { | |
| return mimeType.substring(mimeType.indexOf('/') + 1); | |
| if (mimeType != null) { | |
| int idx = mimeType.indexOf('/'); | |
| if (idx >= 0) { | |
| return mimeType.substring(idx + 1); | |
| } |
|


This pull request updates the way statement parameters are sent in HTTP requests by switching from query parameters to multipart form data, and adds a new integration test to verify this behavior. The changes also include necessary import updates and cleanup of the old parameter handling logic.
HTTP request handling improvements:
KEY_STATEMENT_PARAMS) are now sent as multipart form data instead of as query parameters. The request body is constructed usingMultipartEntityBuilderto include both the query and the statement parameters as separate parts. (client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java)client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java)client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java)Testing enhancements:
testStatementParamsSentAsMultipartto verify that statement parameters are correctly sent as multipart form data and received by the server. This test uses WireMock to assert the presence of parameter parts in the request. (client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java)client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java)Miscellaneous:
client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java)## SummaryCloses #2324
Checklist
Delete items not relevant to your PR: