Target client repo: ClickHouse/clickhouse-go
Severity: sev:1 — easy client-side workaround. The buggy code path produces a String-typed literal that ClickHouse implicitly coerces back to Int128/UInt128 in column-typed contexts (INSERT, WHERE comparison), so common INSERT/SELECT-by-id flows still return correct data. Users hitting a context that depends on inferred type (e.g. toTypeName, arithmetic on the literal, function dispatch) can wrap with CAST(? AS Int128) or toInt128(?). Per the rubric, "explicit type hint / CAST in the query defeats the bug" → sev:1.
Description
The format() function in bind.go is used to inline parameter values into the SQL string when callers use ?/$N/@name placeholders with Conn.Query/Exec (the SQL-rewrite bind path, not the native batch path). For Int128/UInt128 values, Go users naturally pass *big.Int (the same representation the column drivers use under lib/column/bigint.go).
*big.Int satisfies fmt.Stringer via its String() method, so it matches the case fmt.Stringer: arm in bind.go (around line 316) and is emitted as quote(v.String()) — i.e. wrapped in single quotes. The serialized SQL therefore contains a String literal instead of an integer literal. The reporter's expected behaviour — "Number is interpreted as (U)Int128" — fails: in a context where ClickHouse infers the literal's type (no target column to coerce against), the value is typed as String.
This is the same family of bug as the upstream Rust issue: the human-readable bind serialization does not produce a value that ClickHouse types as Int128/UInt128. The Rust client over-aggressively emits a bare numeric literal that the server then mis-types as Float64 (the upstream ClickHouse/ClickHouse#38480 story); the Go client under-aggressively quotes it as a string. Both miss the goal stated in the source issue.
ClickHouse server version
Code analysis only; not verified against a running server (local http://localhost:8123 was unreachable in this environment). The buggy serialization was verified by running the unit-level bind path directly — see below.
Reproduction
The bind path is a pure SQL-rewrite, so the bug is fully observable without a server. Adding this test to the root package (bind_bigint_test.go) demonstrates it:
package clickhouse
import (
"math/big"
"testing"
"time"
)
func TestBindBigIntAsInt128(t *testing.T) {
v, _ := new(big.Int).SetString("170141183460469231731687303715884105727", 10) // max Int128
cases := []struct {
query string
args []any
want string
}{
{"SELECT toTypeName(?)", []any{v},
"SELECT toTypeName(170141183460469231731687303715884105727)"},
{"SELECT toTypeName($1)", []any{v},
"SELECT toTypeName(170141183460469231731687303715884105727)"},
{"SELECT toTypeName(@x)", []any{Named("x", v)},
"SELECT toTypeName(170141183460469231731687303715884105727)"},
}
for _, c := range cases {
got, err := bind(time.UTC, c.query, c.args...)
if err != nil {
t.Fatalf("bind err for %q: %v", c.query, err)
}
if got != c.want {
t.Errorf("query=%q\n got: %q\n want: %q", c.query, got, c.want)
}
}
}
Actual output (observed by running this against the current main):
query="SELECT toTypeName(?)" got: "SELECT toTypeName('170141183460469231731687303715884105727')"
query="SELECT toTypeName($1)" got: "SELECT toTypeName('170141183460469231731687303715884105727')"
query="SELECT toTypeName(@x)" got: "SELECT toTypeName('170141183460469231731687303715884105727')"
(Compare to int64(123), which the same bind path renders as bare 123 without quotes — the Stringer-quoting path is what's specific to *big.Int.)
When sent to a server, SELECT toTypeName('170141183460469231731687303715884105727') returns 'String' rather than 'Int128', which is the user-visible expression of the source bug ("Number is interpreted as (U)Int128" fails).
Suggested fix
In bind.go format(), add a case *big.Int: (and likely big.Int) arm before the case fmt.Stringer: arm so 128/256-bit integers are emitted as unquoted numeric literals — matching the way int64 is already handled by the fallthrough fmt.Sprint(v) at the end of the function. Sketch:
case *big.Int:
if v == nil {
return "NULL", nil
}
return v.String(), nil
case big.Int:
return v.String(), nil
Ordering matters: case fmt.Stringer: must not come first, or *big.Int will be caught there again.
A regression test under tests/issues/ (per the project's AGENTS.md convention) covering at least Int128, UInt128, Int256, UInt256, including the negative-value and big.Int (non-pointer) cases, would lock the fix.
Source bug
Relayed from ClickHouse/clickhouse-rs#208 (upstream root cause documented at ClickHouse/ClickHouse#38480).
Dedup search performed
gh issue list --repo ClickHouse/clickhouse-go --search "Int128 bind" --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/clickhouse-go --search "big.Int bind" --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/clickhouse-go --search "bigint bind" --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/clickhouse-go --search "Int128 parameter" --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/clickhouse-go --search "big.Int serialization" --state all --limit 20 → 0 results
gh pr list --repo ClickHouse/clickhouse-go --search "Int128 bind" --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/integrations-ai-playground --label backfill --label "target:clickhouse-go" --search "Int128" --state all --limit 20 → 0 results
gh issue list --repo ClickHouse/integrations-ai-playground --label backfill --label "target:clickhouse-go" --search "bind" --state all --limit 20 → 4 unrelated (Tuple, Array(Date)/Array(DateTime), Array(Bool), HTTP URL length)
gh pr list --repo ClickHouse/integrations-ai-playground --search "Int128 bind" --state all --limit 20 → 0 results
Target client repo:
ClickHouse/clickhouse-goSeverity:
sev:1— easy client-side workaround. The buggy code path produces aString-typed literal that ClickHouse implicitly coerces back toInt128/UInt128in column-typed contexts (INSERT, WHERE comparison), so common INSERT/SELECT-by-id flows still return correct data. Users hitting a context that depends on inferred type (e.g.toTypeName, arithmetic on the literal, function dispatch) can wrap withCAST(? AS Int128)ortoInt128(?). Per the rubric, "explicit type hint / CAST in the query defeats the bug" → sev:1.Description
The
format()function inbind.gois used to inline parameter values into the SQL string when callers use?/$N/@nameplaceholders withConn.Query/Exec(the SQL-rewrite bind path, not the native batch path). For Int128/UInt128 values, Go users naturally pass*big.Int(the same representation the column drivers use underlib/column/bigint.go).*big.Intsatisfiesfmt.Stringervia itsString()method, so it matches thecase fmt.Stringer:arm inbind.go(around line 316) and is emitted asquote(v.String())— i.e. wrapped in single quotes. The serialized SQL therefore contains aStringliteral instead of an integer literal. The reporter's expected behaviour — "Number is interpreted as(U)Int128" — fails: in a context where ClickHouse infers the literal's type (no target column to coerce against), the value is typed asString.This is the same family of bug as the upstream Rust issue: the human-readable bind serialization does not produce a value that ClickHouse types as
Int128/UInt128. The Rust client over-aggressively emits a bare numeric literal that the server then mis-types asFloat64(the upstreamClickHouse/ClickHouse#38480story); the Go client under-aggressively quotes it as a string. Both miss the goal stated in the source issue.ClickHouse server version
Code analysis only; not verified against a running server (local
http://localhost:8123was unreachable in this environment). The buggy serialization was verified by running the unit-level bind path directly — see below.Reproduction
The bind path is a pure SQL-rewrite, so the bug is fully observable without a server. Adding this test to the root package (
bind_bigint_test.go) demonstrates it:Actual output (observed by running this against the current
main):(Compare to
int64(123), which the same bind path renders as bare123without quotes — theStringer-quoting path is what's specific to*big.Int.)When sent to a server,
SELECT toTypeName('170141183460469231731687303715884105727')returns'String'rather than'Int128', which is the user-visible expression of the source bug ("Number is interpreted as(U)Int128" fails).Suggested fix
In
bind.goformat(), add acase *big.Int:(and likelybig.Int) arm before thecase fmt.Stringer:arm so 128/256-bit integers are emitted as unquoted numeric literals — matching the wayint64is already handled by the fallthroughfmt.Sprint(v)at the end of the function. Sketch:Ordering matters:
case fmt.Stringer:must not come first, or*big.Intwill be caught there again.A regression test under
tests/issues/(per the project's AGENTS.md convention) covering at leastInt128,UInt128,Int256,UInt256, including the negative-value andbig.Int(non-pointer) cases, would lock the fix.Source bug
Relayed from ClickHouse/clickhouse-rs#208 (upstream root cause documented at ClickHouse/ClickHouse#38480).
Dedup search performed
gh issue list --repo ClickHouse/clickhouse-go --search "Int128 bind" --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/clickhouse-go --search "big.Int bind" --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/clickhouse-go --search "bigint bind" --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/clickhouse-go --search "Int128 parameter" --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/clickhouse-go --search "big.Int serialization" --state all --limit 20→ 0 resultsgh pr list --repo ClickHouse/clickhouse-go --search "Int128 bind" --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/integrations-ai-playground --label backfill --label "target:clickhouse-go" --search "Int128" --state all --limit 20→ 0 resultsgh issue list --repo ClickHouse/integrations-ai-playground --label backfill --label "target:clickhouse-go" --search "bind" --state all --limit 20→ 4 unrelated (Tuple, Array(Date)/Array(DateTime), Array(Bool), HTTP URL length)gh pr list --repo ClickHouse/integrations-ai-playground --search "Int128 bind" --state all --limit 20→ 0 results