Skip to content

[backfill: ClickHouse/clickhouse-connect] DB-API Cursor.description missing precision/scale for DateTime64 (and Decimal) #881

Description

@alex-clickhouse

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

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions