Everything here throws SQLFeatureNotSupportedException (SQLSTATE 0A000) rather than
returning null, 0, false or a fake success.
That is deliberate. A driver that answers "yes, transactions are supported" and then ignores
commit() gives you an application that appears to have a unit of work and does not. An
exception at the call site is worth more than wrong results later, and a framework probing for a
capability gets a truthful answer it can branch on.
DatabaseMetaData agrees with the behaviour throughout: whatever
supportsTransactions() and friends report is what the methods actually do.
Two sections at the end are about frameworks rather than the API: what
DatabaseMetaData does when a GUI sweeps it, and
what each framework hits.
Every SQLSTATE the driver throws carries the SQLException subclass JDBC 4 defines for its
class, so catch on the type and a check of getSQLState() agree:
| SQLSTATE | thrown as | when |
|---|---|---|
08001, 08003, 08004 |
SQLNonTransientConnectionException |
bad URL or unusable platform; a statement started after the shutdown hook claimed the connection; a second storage path in one JVM |
0A000 |
SQLFeatureNotSupportedException |
everything in this document, plus ? parameters on the statements listed under Other |
22xxx |
SQLDataException |
a value that does not fit the requested type, an unparseable date, a negative setQueryTimeout |
42xxx |
SQLSyntaxErrorException |
the engine rejected the SQL; a ? the driver's lexer cannot place; no such column in the result set |
53200 |
SQLTransientException |
max_memory_usage exceeded — the one failure where retrying is meaningful |
57014 |
SQLTimeoutException for a timeout, SQLException for a cancel |
setQueryTimeout expired, or cancel() was called |
25000, 70100, HY010, HY000, 07xxx, 24000 |
SQLException, or SQLNonTransientException for HY010 |
JDBC defines no subclass for these classes, so the driver claims nothing more |
getErrorCode() carries ClickHouse's own error number whenever the engine supplied one, which
is what to branch on for a specific engine condition; it is stable across engine versions in a
way message text is not. An engine error the driver does not recognise deliberately arrives with
no SQLSTATE rather than a guessed one.
| Method | |
|---|---|
setAutoCommit(false) |
throws — setAutoCommit(true) is accepted, being already the case |
commit(), rollback() |
throw |
setSavepoint(), releaseSavepoint(), rollback(Savepoint) |
throw |
setTransactionIsolation(level) |
throws for anything but TRANSACTION_NONE |
setHoldability(h) |
throws for anything but CLOSE_CURSORS_AT_COMMIT |
chDB is an embedded analytical engine with no transaction manager. getAutoCommit() returns
true, getTransactionIsolation() returns TRANSACTION_NONE, and supportsTransactions()
returns false.
Instead: each statement takes effect as it executes. For all-or-nothing loading, stage into
a temporary table and swap it in with EXCHANGE TABLES or RENAME TABLE.
previous(), first(), last(), absolute(), relative(), beforeFirst(), afterLast() |
throw |
isLast() |
throws |
every updateXxx(), insertRow(), updateRow(), deleteRow(), refreshRow() |
throw |
getCursorName(), setCursorName() |
throw |
getUnicodeStream() |
throws (deprecated since JDBC 2.0) |
Result sets are TYPE_FORWARD_ONLY and CONCUR_READ_ONLY, and asking createStatement for
anything else is refused at creation rather than silently downgraded.
Forward-only is what makes streaming work: one chunk is live at a time, so a 100 GB result
reads in the footprint of a 100 MB one. Supporting previous() would mean buffering the whole
result, which defeats that.
isLast() throws rather than guessing, because answering needs a one-row lookahead — which for
a stream means fetching a chunk the caller may never read, and would be wrong at every chunk
boundary.
Instead: collect the rows you need into a Java collection, or re-run the query with
ORDER BY and LIMIT/OFFSET. For updates, use INSERT, ALTER TABLE ... UPDATE or
ALTER TABLE ... DELETE.
addBatch(), clearBatch(), executeBatch(), executeLargeBatch() all throw, on both
Statement and PreparedStatement.
Instead: ClickHouse wants few large inserts, not many small ones, so a multi-row VALUES
or an INSERT ... SELECT from a file or table function is both simpler and much faster than a
batch:
// One statement, many rows.
statement.executeUpdate("INSERT INTO t VALUES (1,'a'), (2,'b'), (3,'c')");
// Or let the engine read the data itself.
statement.executeUpdate("INSERT INTO t SELECT * FROM file('data.parquet', 'Parquet')");A high-throughput streaming insert path is on the post-V1 list; the engine's C ABI has
chdb_stream_insert, which V1 does not expose.
prepareCall() and every CallableStatement method throw. ClickHouse has no stored
procedures.
Instead: a CREATE FUNCTION UDF called from SELECT, or a parameterized view.
getGeneratedKeys() throws, and prepareStatement(sql, RETURN_GENERATED_KEYS) and the
column-list overloads throw. ClickHouse has no auto-increment or sequences.
Instead: generate the key yourself — generateUUIDv4() in SQL, or a UUID from Java.
getBlob(), setBlob(), createBlob() |
throw — use getBytes/setBytes |
getClob(), getNClob(), setClob() |
throw — use getString/setString |
setArray(), createArrayOf() |
throw — there is no INSERT binding for an array. getArray() works. |
getRef(), setRef() |
throw |
getRowId(), setRowId() |
throw — chDB tables have no row identity |
getSQLXML(), setSQLXML(), createSQLXML() |
throw |
createStruct() |
throws |
getURL() |
throws — use getString |
setTypeMap() with a non-empty map |
throws |
AggregateFunction, and nothing else.
Its serialized state is the aggregate function's own, with no documented shape a reader outside
the engine can rely on. SimpleAggregateFunction is not in this list: it stores the plain nested
type and reads as that.
Array, Map, Tuple, Nested, Variant, Dynamic, JSON, the geometry types, IPv4 and
IPv6 used to be here and are readable now — see type mapping. A JSON
column reads as a Map of the row's paths.
ResultSetMetaData reports the real type name with DATA_TYPE OTHER, so a framework can see
the column. Reading it throws SQLSTATE 0A000 naming the column and its type, with the
workaround: toString(col), hex(col), or a -Merge to finalize it.
? parameters on SHOW ... LIKE ? |
throws (SQLSTATE 0A000) |
setMaxFieldSize(n) for n != 0 |
throws; the driver does not truncate values |
getMoreResults(KEEP_CURRENT_RESULT) |
throws; a statement has one result |
Driver.getParentLogger() |
throws; the driver does not use java.util.logging |
Connection.unwrap(x) for an unrelated x |
throws |
A SHOW/DESCRIBE/EXPLAIN/EXISTS/CHECK statement goes through the engine's
materializing entry point, because the streaming one accepts only a SELECT pipeline. That entry
point does take parameters — chdb_query_with_params_n — so DESCRIBE, EXPLAIN and EXISTS
carry server-side bindings like a SELECT does. SHOW ... LIKE ? does not: the engine
substitutes {name:Type} while parsing and does not do it inside a SHOW's pattern, so the
placeholder reaches the parser as text and the statement fails to parse. The driver reports
that as unsupported rather than interpolating the value into the SQL, which is the injection
server-side binding exists to avoid.
This used to be all five of them, because the Arrow entry point the driver read results through had no parameter-binding form at all.
Instead: ask the system tables. They answer every one of these questions, they are
SELECTs, so they take parameters, and DatabaseMetaData already works this way:
| instead of | use |
|---|---|
SHOW TABLES [FROM d] LIKE ? |
SELECT name FROM system.tables WHERE database = ? AND name LIKE ? |
SHOW DATABASES LIKE ? |
SELECT name FROM system.databases WHERE name LIKE ? |
DESCRIBE TABLE t |
SELECT name, type FROM system.columns WHERE database = ? AND table = ? ORDER BY position |
SHOW COLUMNS FROM t LIKE ? |
the same, plus AND name LIKE ? |
SHOW CREATE TABLE t |
SELECT create_table_query FROM system.tables WHERE database = ? AND name = ? |
EXISTS TABLE t |
SELECT count() > 0 FROM system.tables WHERE database = ? AND name = ? |
SHOW SETTINGS LIKE ? |
SELECT name, value FROM system.settings WHERE name LIKE ? |
Do not build the statement by pasting the value into the SQL of a plain Statement. This
document used to suggest that, and it was wrong: a LIKE pattern that closes the literal and
comments out the rest injects whole clauses into a statement the caller did not write. Measured
on v26.7.2-rc.2, with the pattern interpolated into SHOW TABLES FROM d LIKE '<pattern>':
| pattern | result |
|---|---|
%' LIMIT 1 -- |
accepted; the result silently drops from 3 rows to 1 |
%' FORMAT JSON -- |
accepted |
%' INTO OUTFILE '/tmp/x.tsv' TRUNCATE -- |
accepted through execute(), and it writes the file |
That last one is an arbitrary file write from a metadata query, which is the whole reason
server-side binding exists. Escaping it correctly by hand is possible and is not advice this
document is going to give, because the system table above is both safer and shorter.
EXPLAIN is the one form with no system equivalent, since there is no table of query plans.
Write the literal into the SQL there — but note what that means: an EXPLAIN is something a
developer runs against a query they are debugging, with a value they chose. It is not a place to
put input that came from somewhere else. If the value is not yours, there is nothing to explain
that a parameterized SELECT cannot answer.
Not refused, because throwing would break frameworks that set them as a matter of course, and honouring them is not possible:
setFetchSize(n) |
recorded and reported by getFetchSize(). Batch size is the engine's block size, which the driver does not control. |
setReadOnly(true) |
recorded and reported by isReadOnly(). JDBC describes it as an optimizer hint; chDB has no session read-only mode, and rejecting writes in the driver would need it to classify every statement and would still miss what a table function can do. |
setNetworkTimeout(e, ms) |
recorded and reported. There is no network — the engine is in this process. Statement.setQueryTimeout is the timeout that does something. |
setEscapeProcessing(b) |
there is no escape processing; SQL is passed through verbatim. |
setClientInfo(...) |
stored and returned; not sent anywhere. |
setCatalog(name) |
stored and returned. ClickHouse has one namespace level and it is mapped to JDBC's schema, so use setSchema. |
lowCardinalityAsDictionary, unsupportedAsBinary and stringAsString configured the Arrow
export the driver used to read results through. It reads RowBinaryWithNamesAndTypes now, where
the engine names every type as it declared it, so there is nothing for them to configure.
They are still accepted, and still swallowed rather than forwarded to the engine. A property the
driver stopped recognising would be passed on as --<key>=<value> and fail the connection on an
unknown setting, which is a worse answer than "accepted and ignored" for a URL somebody already
has.
These are supported, with a rule you have to know:
One storage path per JVM. Many connections may share it; a second path is refused with a diagnostic. See the README.
setQueryTimeout is a deadline, and for some statements only a deadline. It normally stops
the engine: a timeout during a result set's fetches cancels the query. But no cancel in the chDB
C ABI applies before a query has returned a handle, so the call that opens a result set cannot
be interrupted — which for a full aggregate, a GROUP BY, an ORDER BY without a LIMIT, or
anything on the materialized route (SHOW, DESCRIBE, EXPLAIN, EXISTS, CHECK) is the
whole statement. Those still raise SQLTimeoutException when the deadline passes, but the work
has already been done by then and is discarded. The alternative — returning a result set after
the caller's deadline, which is what happened before — is worse. Statement.cancel() during
that same window behaves the same way. See engine findings.
One statement at a time per connection, including a result set's fetches. A second
statement waits for the first result set to close; a second one on the same thread raises
SQLException (SQLSTATE 25000) rather than deadlocking. See the
README.
Closing a connection from another thread waits for a statement that is starting on it.
Connection.close() — and abort(), which JDBC defines as a close from another thread while
the connection is in use, and which HikariCP calls on every connection still in use when the
pool is closed — closes the statements under the connection first, and a statement whose result set
is still being created has to finish being created before it can be closed. So the close blocks
until the engine call that starts the statement returns. That is not a new wait:
chdb_close_conn() on a connection whose statement is running blocks for the same query
anyway, measured at 0.85 s of a 1.5 s aggregate when the close arrived 0.7 s in. What changed
is that the wait now happens before the connection handle is retired rather than after, because
the other order left the statement's stream pointing at a connection that no longer existed —
and the failure that produced named handle numbers rather than the close: stream handle 18801 does not belong to connection handle 18576, once in 11 860 soak iterations. A read whose
connection is closed underneath it now says so, with SQLSTATE 08003; a read whose result set
was closed underneath it says that, with HY010.
A column this driver cannot decode fails the row, not just the column. AggregateFunction
states are the case that remains: an opaque per-function blob with no documented layout. The
error arrives from next() rather than from the accessor, and it has to — a row-wise format
gives no way to skip a value whose length cannot be worked out, so the rest of the row is
unreachable too. Cast it in the query; toString(col) always works.
The Calendar overloads of getDate, getTime and getTimestamp do not shift the value.
Those overloads exist to interpret a value whose zone is unknown. A ClickHouse DateTime either
declares its zone or takes the engine's session zone, so the wall clock a caller is given is
already the column's own; shifting it by the caller's calendar would move a moment that was
never ambiguous. The calendar is accepted and ignored. getObject(column, Instant.class) gives
the instant when that is what is wanted.
Stop your query threads before the JVM exits. The driver installs a shutdown hook that
closes connections the application forgot, which covers a leaked result set: without it, a JVM
exiting with a streaming ResultSet open aborts inside the engine (SIGABRT, exit 134) rather
than exiting. Having closed them it calls chdb_shutdown(), which joins the engine's threads
so the host's own native teardown does not race them. Disable both with
-Dchdb.shutdownHook=false if the host manages its own teardown.
One consequence worth knowing: chdb_connect() fails once the hook has run, because
chdb_shutdown() closes the engine for the rest of the process. The JVM runs shutdown hooks
concurrently and in no defined order, so a host that queries chDB from a shutdown hook of its
own is racing this one. Do that work before shutdown, or turn the hook off.
What the hook cannot cover is a thread still executing a query when the process halts. It
used to try, and trying made things worse: chdb_close_conn() on a connection whose query is
running blocks until the query finishes, and closing one is enough to provoke the same engine
abort the hook exists to prevent. Two threads on a two-billion-row aggregate, macOS arm64,
engine 26.7.2-rc.2: 21 aborts in 60 runs with the old hook, 0 in 60 with the hook off, 0 in 80
with the hook as it now is; and the same again on the route that materializes its result set
(SHOW, DESCRIBE, EXPLAIN, EXISTS, CHECK), 6 aborts in 40 against 0 in 80, because the
abort is about closing the connection rather than about how the statement was started. The
surviving runs held the JVM open for the length of the query — 34 s for four threads on five
billion rows — against 0.3 s now. The hook leaves those
connections alone, which is what the process would have done with no hook at all.
The cost of leaving them is that chdb_shutdown() declines while any connection is open, so a
process exiting mid-query does not get the engine's threads joined. Measured, that changes no
exit code, but it is a guarantee you do not have. So shut your executor down before returning
from main if you want it. The driver also has no way to interrupt such a query on your
behalf: the C ABI's cancel applies to a stream, and a query that has not produced one yet has
nothing to cancel.
The mirror of that rule is visible to your code. Once the hook has taken a connection, a
statement started on it is refused with SQLException, SQLSTATE 08003, whose message names
the shutdown. The hook has to refuse rather than let the statement through — starting one on a
connection it is closing is the abort above — so a thread that queries during shutdown should
expect this, and it means the hook declined for a reason rather than that anything is broken.
The Statement is left usable and unclosed, and no stream or result set is left half-open.
There is also a rarer abort in this shape that no hook reaches — the engine's C++ exit-time
destructors racing a thread that is still inside it, mutex lock failed: Invalid argument, at
the same rate with the hook on and off. Turning the hook off does not avoid it; only not
exiting mid-query does. See upstream findings §9, which asks upstream
for a shutdown that does not require the caller to have closed everything first.
A misspelled setting name in the URL is silent. Properties after ? that the driver does
not recognize are handed to the engine as --key=value, and the engine accepts a name it has
never heard of without complaint: jdbc:chdb:/data?max_thread=4 connects, and nothing applies
max_threads. The driver cannot help — it has no list of setting names to check against, and
inventing one would refuse settings a newer engine added.
An invalid value for a name the engine does know is caught, since engine 26.7.2-rc.2:
?max_threads=not-a-number, ?max_threads=-5 and ?max_memory_usage=abc raise SQLException
rather than connecting. Engine 26.7.0 accepted those too, so this is stricter than it was. The
exception names the arguments that were passed, because chdb_connect() itself reports nothing.
If a setting matters, assert it took effect:
SELECT value FROM system.settings WHERE name = 'max_threads'. See
upstream findings §5.
A non-ASCII storage path works whatever the JVM's locale is, including a container started
with no LANG — the default for most base images, which gives the JVM an ASCII
sun.jnu.encoding and makes java.nio.file refuse the name outright. The driver only needs
the path in order to decide whether two URLs name the same directory, so where Paths.get
cannot encode it the driver absolutises and normalises the path as text instead. normalize()
is a text operation anyway: it collapses . and .. without touching the filesystem. The
engine is handed the path as UTF-8 bytes and creates the directory itself.
What still needs a UTF-8 locale is your own java.nio.file code: on such a JVM the
application cannot open, list or delete that directory through File/Path, even though the
driver and the engine are using it. If you inspect the storage directory from Java, set
LANG=C.UTF-8 or LC_ALL=C.UTF-8. Note that -Dfile.encoding=UTF-8 will not do it — JEP 400
changed file.encoding and deliberately left sun.jnu.encoding following the OS locale.
Spaces, and names up to the filesystem's own length limit, work regardless. A path containing a
NUL is refused (SQLSTATE 08001): it would truncate the string handed to the engine's C ABI.
PreparedStatement.getMetaData() before execution throws. chDB's C ABI cannot describe a
statement without running it, and running the caller's query as a side effect of asking about it
is not something a metadata call may do. Call it on the ResultSet after executeQuery().
ParameterMetaData reports the parameter count exactly, and reports every parameter as
VARCHAR/String — which is what the driver actually binds, not a placeholder answer, though
it says nothing about the column a value is compared against.
A bound parameter is a String, and ClickHouse will not convert one everywhere. This is the
consequence of the line above, and it is the one that shows up under a query builder. Parameters
go to the engine through chdb_query_with_params_n, whose values are text, so every ? arrives
typed String whichever setter bound it. ClickHouse converts a string literal to the other
side's type in a comparison, and does not convert it where a numeric constant is required.
Measured on engine 26.7.0:
| Position | |
|---|---|
WHERE n = ?, < ?, > ?, IN (?) |
works — the literal is converted to the column's type |
SELECT ?, length(?), any string context |
works |
LIMIT ?, OFFSET ? |
fails — Code 440, "LIMIT expression must be constant with numeric type. Actual: '3'" |
n + ?, ? + 1, arithmetic |
fails — Code 43, "Illegal types UInt64 and String of arguments of function plus" |
numbers(?) and other numeric table-function arguments |
fails — Code 43, "Illegal type String expression, must be numeric type" |
Instead: cast the parameter in the SQL — numbers(toUInt64(?)), LIMIT toUInt64(?) — or
render the value into the statement and let the framework escape it. For jOOQ that means
StatementType.STATIC_STATEMENT; see under a framework.
Splicing the value into the SQL string yourself is the one thing not to do: the escaping is what
bind() exists for, and PreparedStatementIT is the evidence it holds.
getColumns() reports real answers: TYPE_NAME is the ClickHouse type name and
DATA_TYPE, COLUMN_SIZE, DECIMAL_DIGITS and NULLABLE come from the same
JdbcTypeMapping that ResultSetMetaData uses, so the two agree. They were OTHER and zeroes
while the result-set mapping lived in the Arrow reader and a second implementation here would
have drifted from it. SELECT * FROM t LIMIT 0 and its ResultSetMetaData is still the way to
get a type without a metadata call.
jdbcCompliant() returns false, and will keep doing so. Compliance requires full SQL-92
entry level and the whole API surface; chDB has no transactions, no scrollable cursors and
ClickHouse SQL. Claiming it would be a false statement about the driver rather than a
formality.
A JDBC GUI — DBeaver, DataGrip, SQuirreL — sweeps most of DatabaseMetaData while a connection
is being opened, before the user has typed anything, and it does so whether or not the answer is
useful to it. One method throwing SQLFeatureNotSupportedException inside that sweep is reported
as "could not connect", so the driver looks broken on first contact rather than merely limited.
DatabaseMetaDataSurfaceIT reproduces the sweep: every method the running JDK reports on
DatabaseMetaData, invoked reflectively with the arguments a tool would pass. On engine 26.7.0,
under JDK 11, 21 and 26 alike:
| Returned a value, or a result set with rows | 160 |
| Returned a result set with no rows | 19 |
Threw SQLFeatureNotSupportedException |
0 |
| Threw anything else | 0 |
| Total | 179 |
How many methods that is belongs to the JDK rather than to this driver. DatabaseMetaData
declares 177 — of which getSchemas and supportsConvert are overload pairs — and inherits
unwrap and isWrapperFor from Wrapper, for 179 from getMethods(). Measured identical on
JDK 11, 21 and 26; Java 11 is this driver's floor and gets the same sweep as the newest JDK, so
the coverage above is not a claim that only holds on a recent runtime.
The test therefore asserts coverage rather than that number: one classified result per method the running JDK reports, exactly once. A future JDBC revision may move the total without touching this driver, whereas a method quietly falling out of the sweep is a real regression and fails.
DatabaseMetaData is the one interface in this driver with no refusal anywhere in it, and that
is on purpose: it is the interface whose whole job is to be asked questions by software that
cannot handle being told no. The test asserts the zeros rather than reporting them, so the
property cannot be lost by accident.
The 19 with no rows are the ones ClickHouse has nothing to say about — no foreign keys, no indexes in the JDBC sense, no procedures, no UDTs, no privileges table, no catalog level:
getAttributes · getBestRowIdentifier · getCatalogs · getClientInfoProperties ·
getColumnPrivileges · getCrossReference · getExportedKeys · getFunctionColumns ·
getImportedKeys · getIndexInfo · getPrimaryKeys · getProcedureColumns · getProcedures ·
getPseudoColumns · getSuperTables · getSuperTypes · getTablePrivileges · getUDTs ·
getVersionColumns
An empty result set is not a cheap substitute for an exception here — it is a different code
path, and the one jOOQ and MyBatis actually read. Both address the columns of these sets by
label, so a zero-row set spelling PK_NAME differently, or reporting no columns at all, is as
broken as a throw and far harder to notice. DatabaseMetaDataSurfaceIT therefore also checks
every result-set-returning method against the column names JDBC 4.3 specifies — transcribed from
the JDBC javadoc, not from this driver, so a rename here is a failure — and calls every
ResultSetMetaData accessor on every column of every one of them. All present, all answerable,
rows or not.
What each framework does that the driver refuses, and what to do instead. HikariCP and Spring
JdbcTemplate are covered elsewhere and are not repeated here; these are the four from issue
#11.
Versions are pinned to the last release of each that still runs on Java 11, which is this driver's floor. jOOQ 3.17 and HikariCP 6 require Java 17, and testing those would quietly drop Java 11 out of the supported matrix.
MyBatis's default JdbcTransactionFactory calls setAutoCommit(false) on the connection the
moment a session opens, because openSession() with no argument means "not auto-commit". The
driver refuses that, so the session fails on its first statement.
Instead: openSession(true), or configure ManagedTransactionFactory — which makes
commit() and rollback() no-ops rather than calls the driver has to refuse, and is the honest
description of what a session against chDB is.
Everything else works. Automatic result mapping, which is MyBatis's heaviest use of
ResultSetMetaData, picks the right TypeHandler per column from the driver's reported JDBC
types; an empty result set gives an empty list; insert() returns the real affected-row count.
#{} parameters bind, subject to the
String-typed parameter rule above — numbers(#{n}) needs
numbers(toUInt64(#{n})).
.limit() fails by default. jOOQ renders a limit as a bind value, so LIMIT ? reaches the
engine as LIMIT '3' and is refused. This is the most-used clause jOOQ parameterises, so it is
the first thing a jOOQ user hits.
Instead: new Settings().withStatementType(StatementType.STATIC_STATEMENT), which has jOOQ
render its values inline. jOOQ escapes them itself, so this is not a return to concatenated SQL.
transaction() is refused out of the box, for the same reason as everything else in
Transactions: jOOQ's DefaultTransactionProvider calls setAutoCommit(false)
before running the block, so the block never runs. The connection survives the refusal and stays
usable.
TransactionProvider is a jOOQ SPI with three methods, so the block can be made to run — but
read the next paragraph before doing it, and note that jOOQ's own NoTransactionProvider is not
the way:
// Honoured. Not NoTransactionProvider: DefaultConfiguration.transactionProvider() treats that
// class as a sentinel for "unset" and substitutes DefaultTransactionProvider, so configuring
// it changes nothing. Subclassing it does not help either — the check is an instanceof.
final class NoOpTransactionProvider implements TransactionProvider {
public void begin(TransactionContext ctx) {}
public void commit(TransactionContext ctx) {}
public void rollback(TransactionContext ctx) {}
}
DSLContext ctx = DSL.using(new DefaultConfiguration()
.set(connection)
.set(SQLDialect.DEFAULT)
.set(new NoOpTransactionProvider()));What that costs is the whole transaction. The statements inside the block run one at a time
exactly as they would outside it, and a failure part-way through leaves the earlier ones
applied — JooqIT asserts precisely that, by throwing out of a block after an INSERT and
finding the row still there. So this buys nothing except the ability to keep transaction() in
the source; it is not a weak transaction, it is no transaction, and code that reads as though it
had a unit of work would not have one.
That is not a gap in the driver. chDB has no transaction manager, so a framework's transaction
abstraction can only degrade to a no-op on it — the choice is between a refusal at the call site
and a block that silently is not atomic. The driver refuses; if you install the no-op provider
you are choosing the second, and the honest thing is then to stop writing transaction() at all
and let each statement stand on its own, as Transactions suggests.
One thing that is not a driver limitation but will look like one: jOOQ types every chDB column
as Object. jOOQ resolves a field's Java type from the type name against its dialect
registry, and no jOOQ dialect this driver is routed to knows Int32 or DateTime64. The driver
itself reports the column correctly — ResultSetMetaData gives type INTEGER, type name
Int32, class name java.lang.Integer, on a populated and on a zero-row result alike, which
JooqIT asserts. So declare the type at the call site: DSL.field("n", Integer.class), or
record.get("n", Integer.class).
jOOQ's meta() walk — getCatalogs, getSchemas, getTables, getColumns, getPrimaryKeys,
getIndexInfo, the heaviest DatabaseMetaData use of anything tested here — completes, and
lands on an empty key list and an empty index list rather than an error.
jOOQ resolves this driver to SQLDialect.DEFAULT, because getDatabaseProductName() returns
chDB and no jOOQ version recognises it. Everything above was measured under DEFAULT.
Not installed and driven, which is not practical in CI. Covered instead by the full metadata sweep, which is the part of a GUI's connect sequence that can fail: 179 methods on JDK 11 through 26, nothing throws, every result set well-formed.
What that does not cover is a GUI's own SQL — the statements it runs to populate its navigator
tree beyond DatabaseMetaData, its data editor's assumption that a result set can be updated,
and its use of Connection.setAutoCommit(false) when the user turns off auto-commit in the
toolbar. The first is engine SQL rather than driver surface; the other two are refused, and
Result sets and Transactions say so.
Not a driver limitation: nothing the driver can change makes this work, and the fix belongs upstream.
ShardingSphere already knows about chDB: its ClickHouseDatabaseType lists jdbc:chdb among
its JDBC URL prefixes alongside jdbc:clickhouse: and jdbc:ch:, so a chDB URL is routed to
its ClickHouse dialect without being asked. But every DatabaseType then parses the URL with
StandardJdbcUrlParser, which requires the //authority component of a client/server URL to be
present and refuses the URL outright when it is not. An embedded engine has no host and no port,
so no chDB URL gets through — jdbc:chdb:, jdbc:chdb::memory: and a filesystem storage path
all fail identically, with UnrecognizedDatabaseURLException.
Both entry points fail in the same place: the jdbc:shardingsphere: driver with a YAML
configuration, and ShardingSphereDataSourceFactory handed an already-built HikariCP pool and
no rules at all. So there is no configuration route around it — no YAML key and no factory
argument avoids the URL parse.
There is a code route, and it is a large one: DatabaseType and ConnectionPropertiesParser
are both ServiceLoader SPIs, so a chDB-specific DatabaseType registered ahead of
ClickHouseDatabaseType, with a parser that tolerates an authority-less URL, would get past
this. That means shipping and versioning a ShardingSphere plugin against internal-ish SPIs
whose signatures move between minor releases, and it would be testing that plugin rather than
this driver, so it is not done here and is not recommended over waiting for the upstream fix.
Inventing a //localhost/ to satisfy the parser is not a workaround either: the driver reads
the text after the prefix as the storage path, so it would open a database in a directory named
after a host that is not there.
ShardingSphereIT pins this rather than skipping it, so that the test starts failing when
ShardingSphere learns to parse an embedded URL.
What is consequently untested is ShardingSphere's own connection management against the
one-storage-path-per-JVM rule. The nearest thing that is tested is HikariPoolIT —
ShardingSphere's pools are HikariCP, and pool churn against the storage-path registry is exactly
what that covers.