Target client repo: ClickHouse/clickhouse-connect
Severity: sev:1 — Easy client-side workaround. The full type name including precision (e.g. DateTime64(3)) is still exposed as the type_code (element 1) of the description tuple and via the underlying client.query() API's column_types, so a user can parse it out. The PEP 249 precision/scale slots are simply hardcoded to None, which PEP 249 explicitly permits ("set to None" when not available). It is a real ergonomic gap that mirrors the Go bug, but it does not corrupt data, raise errors, or block any default code path.
Description
The source bug for clickhouse-go reports that the Go database/sql driver's ColumnType.DecimalSize() returns 0, 0, false for DateTime64, even though the type carries a precision argument. The analogous surface in clickhouse-connect is the PEP 249 DB-API cursor's description property, where slots 5 (precision) and 6 (scale) of the per-column 7-tuple should reflect type metadata.
In clickhouse-connect, Cursor.description in clickhouse_connect/dbapi/cursor.py unconditionally returns None for both slots:
@property
def description(self):
return [(n, t, None, None, None, None, True) for n, t in zip(self.names, self.types)]
The information is available to the driver. DateTime64 (clickhouse_connect/datatypes/temporal.py, class DateTime64) stores scale and prec = 10**scale on the type instance, and the various Decimal types similarly carry precision/scale. The dbapi cursor discards the type instance and keeps only its name string (self.types = [x.name for x in query_result.column_types] in cursor.execute), so by the time description runs the only path back to the precision is to re-parse the type name.
The behavior is consistent with PEP 249 (the spec allows None), but it makes DB-API consumers (SQLAlchemy reflection, generic DB-API tools, ORMs) unable to discover DateTime64 precision or Decimal precision/scale without parsing the type name themselves.
ClickHouse server version
26.4.2.10
Reproduction
test_dbapi_description_precision.py:
import clickhouse_connect.dbapi as dbapi
conn = dbapi.connect(host="localhost", port=8123)
cur = conn.cursor()
cur.execute("SELECT toDateTime64(1546300800.123, 3) AS dt, toDecimal64(1.5, 4) AS d")
for col in cur.description:
name, type_code, _, _, precision, scale, _ = col
print(f"{name}: type={type_code!r} precision={precision} scale={scale}")
Expected (analogous to the Go report's expectation):
dt: type='DateTime64(3)' precision=3 scale=3
d: type='Decimal(18, 4)' precision=18 scale=4
Actual:
dt: type='DateTime64(3)' precision=None scale=None
d: type='Decimal(18, 4)' precision=None scale=None
Note: precision=None/scale=None is permitted by PEP 249 when the driver does not have the information; here the driver does have it and is just not surfacing it.
(Code analysis only against the cited files; not executed against the running server because the dbapi entry point did not need to be exercised to confirm the hardcoded None values in description.)
Suggested fix
In clickhouse_connect/dbapi/cursor.py:
- Retain the type instances (not just names) in
self.types, or store a parallel list of type instances.
- In
description, inspect the type instance and populate slots 5 (precision) and 6 (scale) when the type exposes them:
DateTime64 -> precision = scale = type.scale (matches Go's convention of using the sub-second digit count for both).
Decimal* -> precision = type.prec, scale = type.scale.
- Leave both
None for types that have no meaningful precision/scale.
Keep type_code (slot 1) as the full type name string so existing consumers that read it are not affected.
Source bug
ClickHouse/clickhouse-go#1462
Target client repo:
ClickHouse/clickhouse-connectSeverity:
sev:1— Easy client-side workaround. The full type name including precision (e.g.DateTime64(3)) is still exposed as thetype_code(element 1) of the description tuple and via the underlyingclient.query()API'scolumn_types, so a user can parse it out. The PEP 249precision/scaleslots are simply hardcoded toNone, which PEP 249 explicitly permits ("set to None" when not available). It is a real ergonomic gap that mirrors the Go bug, but it does not corrupt data, raise errors, or block any default code path.Description
The source bug for clickhouse-go reports that the Go
database/sqldriver'sColumnType.DecimalSize()returns0, 0, falseforDateTime64, even though the type carries a precision argument. The analogous surface in clickhouse-connect is the PEP 249 DB-API cursor'sdescriptionproperty, where slots 5 (precision) and 6 (scale) of the per-column 7-tuple should reflect type metadata.In clickhouse-connect,
Cursor.descriptioninclickhouse_connect/dbapi/cursor.pyunconditionally returnsNonefor both slots:The information is available to the driver.
DateTime64(clickhouse_connect/datatypes/temporal.py, classDateTime64) storesscaleandprec = 10**scaleon the type instance, and the variousDecimaltypes similarly carry precision/scale. The dbapi cursor discards the type instance and keeps only itsnamestring (self.types = [x.name for x in query_result.column_types]incursor.execute), so by the timedescriptionruns the only path back to the precision is to re-parse the type name.The behavior is consistent with PEP 249 (the spec allows
None), but it makes DB-API consumers (SQLAlchemy reflection, generic DB-API tools, ORMs) unable to discoverDateTime64precision orDecimalprecision/scale without parsing the type name themselves.ClickHouse server version
26.4.2.10
Reproduction
test_dbapi_description_precision.py:Expected (analogous to the Go report's expectation):
Actual:
Note:
precision=None/scale=Noneis permitted by PEP 249 when the driver does not have the information; here the driver does have it and is just not surfacing it.(Code analysis only against the cited files; not executed against the running server because the dbapi entry point did not need to be exercised to confirm the hardcoded
Nonevalues indescription.)Suggested fix
In
clickhouse_connect/dbapi/cursor.py:self.types, or store a parallel list of type instances.description, inspect the type instance and populate slots 5 (precision) and 6 (scale) when the type exposes them:DateTime64->precision = scale = type.scale(matches Go's convention of using the sub-second digit count for both).Decimal*->precision = type.prec,scale = type.scale.Nonefor types that have no meaningful precision/scale.Keep
type_code(slot 1) as the full type name string so existing consumers that read it are not affected.Source bug
ClickHouse/clickhouse-go#1462