feat: support arbitrary input/output formats (experimental, HTTP only)#1928
feat: support arbitrary input/output formats (experimental, HTTP only)#1928kavirajk wants to merge 12 commits into
Conversation
Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
Client-side codecs translating between native blocks and named ClickHouse formats, for use over the native TCP protocol where the server always exchanges Native blocks. Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
…ization Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
HTTP: pure passthrough - the server encodes/parses the requested format; the response streams through an exception-scanning reader. Native TCP: client-side codecs from Options.FormatCodecs convert between Native blocks and the requested format, mirroring clickhouse-client. Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
A shared block-to-record converter backs both codecs; any Arrow-adjacent format can be added as a thin codec on top. Parquet decoding buffers the payload (footer-at-end format constraint); ArrowStream is fully streaming. Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
Covers round-trips in all four built-in formats on both protocols, FORMAT-clause replacement, unregistered-format errors, multi-block and malformed inserts, mid-stream HTTP exceptions, cancellation, gzip, and cross-protocol Parquet interop. Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
Also replace the single example with three complete ones: CSV ingest, JSONEachRow streaming decode, and Parquet file export/import. Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
…lease - Both APIs are marked Experimental and return ErrFormatNativeUnsupported over the native protocol, where the server only exchanges Native blocks. - Remove the client-side codec layer (lib/format, Options.FormatCodecs) and the arrow-go dependency tree; server-side formatting over the native protocol is the direction under discussion instead. - Validate format names before interpolating them into the query text. Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
| // parse a mid-stream exception once its marker is seen. | ||
| const exceptionScanLimit = 32 << 10 | ||
|
|
||
| var exceptionMarker = []byte("__exception__") |
There was a problem hiding this comment.
queryFormat wraps every response in exceptionScanReader, which scans the whole (successful) stream for the literal bytes __exception__. A valid result that legitimately contains that sequence is silently cut off at the match and the remainder is parsed as a server exception — so valid data is truncated and a spurious error is returned on a query that actually succeeded.
- Not just binary: a
String/Nullable(String)column holding the text__exception__triggers it on CSV/JSONEachRow too;Parquet/ArrowStreampayloads can carry the bytes anywhere. - The failure is silent (partial data + fabricated
ClickHouse exception), and it hits the headline Parquet path.
The doc comment acknowledges this, but since it corrupts valid success-path data, please decide explicitly: e.g. default these calls to wait_end_of_query=1 (server buffers and reports errors via HTTP status, no in-band marker) unless the caller opts into streaming, or gate the marker scan so it cannot silently truncate. At minimum surface the limitation in the exported QueryFormat doc, not only here.
🤖 Claude reviewAdds experimental My verdict is needs discussion, driven by one design limitation rather than a defect in the implementation. Key concern:
Minor:
Blind spots: could not run the integration tests against a live server; the marker-collision and compression+mid-stream-exception paths are not exercised by tests. Adding these two methods to the public Verdict: 💬 Needs discussion General findings
Inline comments are attached to the relevant lines. This summary updates in place on re-review. |
| // HTTP protocol, where the server encodes the stream and every | ||
| // server-supported format works; over the native protocol it returns | ||
| // clickhouse.ErrFormatNativeUnsupported. | ||
| QueryFormat(ctx context.Context, format string, query string, args ...any) (io.ReadCloser, error) |
There was a problem hiding this comment.
minor: feels like some glue word missing like QueryAsFormat, InsertAsFormat.
There was a problem hiding this comment.
My initial idea was QueryWithFormat and InsertWithFormat. Later decided to keep it simple. Goes well with other apis
Query() and QueryFormat()
Insert() and InsertFormat()
| // their tail is the beginning of an exception marker split across reads. | ||
| func (r *exceptionScanReader) safeLen() int { | ||
| if r.srcDone || r.err != nil { | ||
| return len(r.pending) |
There was a problem hiding this comment.
this is the branch for done or error. Then how would length be used in this case ?Should it return 0 ?
| switch h.compression { | ||
| case CompressionGZIP, CompressionDeflate, CompressionBrotli: | ||
| headers["Accept-Encoding"] = h.compression.String() | ||
| case CompressionZSTD, CompressionLZ4: |
There was a problem hiding this comment.
lz4 and zstd can be used in http compression.
in java we have dedicated flag http_compression to understand what compression should be used.
| return nil, err | ||
| } | ||
|
|
||
| rw := h.compressionPool.Get() |
There was a problem hiding this comment.
how do you detect compression?
would it work with uncompressed response?
Signed-off-by: Kaviraj <kavirajkanagaraj@gmail.com>
Summary
Changes:
Two new methods on
driver.Conn, marked experimental:Today the driver forces everything through the Native format: it overrides default_format on HTTP and rewrites any
FORMATclause inINSERTstatements. That means you can't ask ClickHouse forCSV,JSONEachRow,Parquet, etc., even though the server over HTTP happily produces and parses all ~70 formats by itself.With this PR the driver just gets out of the way: the server does all encoding/decoding, and the client streams raw bytes. Query results come back as an
io.ReadCloseryou read the formatted bytes from.inserts take an
io.Reader(a file, an HTTP body, astrings.Reader) as payload. No new dependencies.The immediate use case is Parquet — exporting query results as Parquet files and ingesting Parquet files directly, without going through Rows/Batch.
Why HTTP only?
Over the native TCP protocol the server always speaks Native blocks, so any other format would have to be encoded/decoded client-side (this is what clickhouse-client does internally).
I prototyped that with pluggable codecs (as separate package) with CSV/JSONEachRow/Parquet implementations — but it means shipping and forever maintaining format encoders in Go, plus heavy dependencies (arrow-go pulls in grpc, thrift, flatbuffers).
We deliberately dropped it from this release; the direction we want to explore instead is server-side formatting over the native protocol.
Over native TCP, both methods return a clear sentinel error (
ErrFormatNativeUnsupported) pointing users at HTTP.Checklist
Delete items not relevant to your PR: