You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When scanning a NULL element from a LowCardinality(Nullable(T)) column into a reused pointer destination (*T), the driver leaves the destination pointing at the previous row's value instead of setting it to nil. Scanning the same column via Nullable(T) (without the LowCardinality wrapper) clears the destination correctly, so the two disagree.
Reproduction
Against a default server (no special settings needed for the String base type):
ctx:=context.Background()
conn, _:=clickhouse.Open(&clickhouse.Options{Addr: []string{"127.0.0.1:9000"}})
conn.Exec(ctx, "CREATE TABLE t (c LowCardinality(Nullable(String))) ENGINE Memory")
conn.Exec(ctx, "INSERT INTO t VALUES ('hi'), (NULL)")
rows, _:=conn.Query(ctx, "SELECT c FROM t ORDER BY c NULLS LAST")
varp*string// reused across rowsforrows.Next() {
_=rows.Scan(&p)
ifp==nil {
fmt.Println("<nil>")
} else {
fmt.Printf("%q\n", *p)
}
}
Observed:
"hi"
"hi" <-- NULL row: p still points at the previous value
Expected:
"hi"
<nil> <-- NULL row: p should be reset to nil
Root cause
LowCardinality.ScanRow returns early on a NULL element (key index 0) without touching dest:
// lib/column/lowcardinality.gofunc (col*LowCardinality) ScanRow(destany, rowint) error {
idx:=col.indexRowNum(row)
ifidx==0&&col.nullable {
returnnil// <-- does not clear dest
}
returncol.index.ScanRow(dest, idx)
}
During parse, LowCardinality disables the inner Nullable (nullable.enable = false) and tracks NULLs itself through key index 0. That means the inner Nullable.ScanRow never runs its NULL branch, so LowCardinality.ScanRow is solely responsible for resetting the destination on NULL — but it doesn't. By contrast, Nullable.ScanRow clears **T pointer destinations and calls sql.Scanner.Scan(nil) for NULLs.
Scope
Affects every base type valid under LowCardinality(Nullable(...)). String reproduces on a default server; numeric / Date / DateTime base types require allow_suspicious_low_cardinality_types to create.
The native driver path (driver.Conn.Query + Rows.Scan) is affected. The database/sql path is not — it reads columns as driver.Value and clears via convertAssign.
Environment
clickhouse-go: main
ClickHouse server: 26.x (also applies to older versions)
Summary
When scanning a
NULLelement from aLowCardinality(Nullable(T))column into a reused pointer destination (*T), the driver leaves the destination pointing at the previous row's value instead of setting it tonil. Scanning the same column viaNullable(T)(without theLowCardinalitywrapper) clears the destination correctly, so the two disagree.Reproduction
Against a default server (no special settings needed for the
Stringbase type):Observed:
Expected:
Root cause
LowCardinality.ScanRowreturns early on aNULLelement (key index0) without touchingdest:During
parse,LowCardinalitydisables the innerNullable(nullable.enable = false) and tracks NULLs itself through key index0. That means the innerNullable.ScanRownever runs its NULL branch, soLowCardinality.ScanRowis solely responsible for resetting the destination on NULL — but it doesn't. By contrast,Nullable.ScanRowclears**Tpointer destinations and callssql.Scanner.Scan(nil)for NULLs.Scope
LowCardinality(Nullable(...)).Stringreproduces on a default server; numeric /Date/DateTimebase types requireallow_suspicious_low_cardinality_typesto create.driver.Conn.Query+Rows.Scan) is affected. Thedatabase/sqlpath is not — it reads columns asdriver.Valueand clears viaconvertAssign.Environment
main