Skip to content

fix: bind Int128/UInt128/Int256/UInt256 (big.Int) params as numeric values - #1922

Open
polyglotAI-bot wants to merge 4 commits into
mainfrom
polyglot/fix-bigint-bind-params
Open

fix: bind Int128/UInt128/Int256/UInt256 (big.Int) params as numeric values#1922
polyglotAI-bot wants to merge 4 commits into
mainfrom
polyglot/fix-bigint-bind-params

Conversation

@polyglotAI-bot

Copy link
Copy Markdown
Collaborator

Description

Fixes #1917.

*big.Int / big.Int are the Go types behind ClickHouse Int128, UInt128, Int256 and UInt256. In the client-side SQL bind rewrite (?, $1, @name), formatValue() matched *big.Int on the case fmt.Stringer: arm and emitted it as a quoted string literal ('170141183460469231731687303715884105727'). The server then saw a StringtoTypeName(?) returned String instead of a number — so type-inference contexts (arithmetic, function dispatch, toTypeName) broke. A big.Int passed by value was worse still: it fell through to fmt.Sprint and produced garbage struct text.

Emitting a bare decimal is not enough: ClickHouse infers an integer literal wider than 64 bits as Float64, losing precision (a WHERE on an Int128 column then matches nothing). The fix instead wraps the exact decimal in the narrowest wide-integer conversion that holds ittoInt128('..'), toUInt128('..'), toInt256('..') or toUInt256('..') — mirroring how the driver already binds times as toDateTime(...) and floats as cast(..., 'Float64'). This keeps both the exact value and an integer type. Server-side {name:Type} query parameters already declare the type, so there the bare decimal is sent unchanged.

Changes

  • bind.go: handle big.Int and *big.Int in formatValue() before the fmt.Stringer arm. New helpers formatBigInt / bigIntConvFunc pick the narrowest of Int128/UInt128/Int256/UInt256 that holds the value, or return an actionable error if it fits none. nil *big.IntNULL.
  • bind_test.go: TestFormatBigInt (magnitudes, spill boundaries, out-of-range, both format modes, value + pointer), TestBindBigInt (all three placeholder styles, arrays, tuples, maps, nil, and contrast cases that must stay quoted: string, another fmt.Stringer, decimal.Decimal), TestBindBigIntQueryParameter (server-side {name:Type} path).
  • tests/issues/1917_test.go: integration test over Native + HTTP — parameter binds as a wide integer, round-trips exactly, matches an Int128 column, and works via server-side query parameters.
  • tests/std/1917_test.go: the same round-trip through the database/sql surface on both protocols.

Test

Verified against ClickHouse 26.5 that the new tests fail on main (*big.Int'…'String; big.Int value → fmt.Sprint garbage) and pass with the fix. toInt128('…') etc. confirmed to yield the exact value and the expected wide-integer type on the server. gofmt/go vet clean on changed files; full root-package unit suite green.

Pre-PR validation gate

  • Deterministic repro confirmed (tests fail on unpatched bind.go, pass with fix)
  • Root cause documented above
  • Fix targets the root cause
  • Test fails without fix, passes with fix
  • No existing tests weakened or broken
  • Convention compliance (AGENTS.md / CONTRIBUTING.md — CHANGELOG.md is auto-generated, not hand-edited; regression test under tests/issues/; t.Cleanup + version-gated server-side subtest)

…alues

A *big.Int (the Go type behind Int128/UInt128/Int256/UInt256) implements
fmt.Stringer, so client-side binding matched the `case fmt.Stringer:` arm in
formatValue and emitted the value as a quoted string literal. The server then
saw a String -- `toTypeName(?)` returned `String`, and server-side
`{name:Int128}` parameters failed to parse the quoted value.

Emitting the bare decimal instead is not enough: ClickHouse reads a bare decimal
literal wider than 64 bits as Float64, losing precision (a WHERE on an Int128
column would match nothing). So in SQL mode the value is wrapped in the narrowest
wide-integer conversion that holds it exactly -- toInt128/toUInt128/toInt256/
toUInt256('<decimal>') -- mirroring how times bind as toDateTime(...) and floats
as cast(..., 'Float64'). In query-parameter text mode the {name:Type} placeholder
already declares the type, so the bare decimal is sent as-is.

Also fixes a nil *big.Int binding as '<nil>' instead of NULL.

Fixes: #1917
Add a tests/std regression test that drives a big.Int bind parameter
through the database/sql surface (rebind -> bind) on both Native and
HTTP, and add tuple and map nesting cases to the unit test, so every
entry point and container type that reaches the big.Int bind path in
issue #1917 is asserted.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes client-side SQL placeholder binding for big.Int/*big.Int (ClickHouse Int128/UInt128/Int256/UInt256) so values are emitted as correctly-typed wide integers instead of quoted strings (or fmt.Sprint struct output), preserving both type inference and precision.

Changes:

  • Extend formatValue() to detect big.Int/*big.Int before the fmt.Stringer path and format them via the narrowest to(Int|UInt)(128|256)('...') conversion in SQL mode.
  • Add unit tests covering formatting/binding behavior across placeholder styles and query-parameter mode.
  • Add integration tests for native + HTTP and database/sql surfaces to validate server-side typing and round-trips.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
bind.go Adds big.Int formatting/conversion selection logic for SQL-rewrite binding and query-parameter text mode.
bind_test.go Adds unit tests for wide-int formatting/binding behavior, including contrast cases for other fmt.Stringer types.
tests/issues/1917_test.go New integration test validating server-side inferred type, exact round-trip, and column match behavior.
tests/std/1917_test.go New database/sql integration test validating server-side inferred type and exact round-trip.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/issues/1917_test.go
Comment thread tests/std/1917_test.go
Comment thread bind.go
Comment on lines +779 to +782
}
}
return "", fmt.Errorf("big.Int value %s is out of range for Int128, UInt128, Int256 and UInt256", v.String())
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I considered truncating but kept the full value. This branch only fires for values outside [Int256 min, UInt256 max] (>78 digits); it's a single one-shot allocation of a string the caller already holds as a big.Int (no loop, no repeated growth), and AGENTS.md asks that errors be "as actionable as possible" — the exact offending value is the most actionable thing to print. A caller who builds a pathologically huge big.Int has already allocated it, so truncating the echo wouldn't meaningfully bound memory. I'd rather not introduce a magic truncation threshold for that edge, but happy to reconsider if you feel strongly.

Comment thread bind.go
Comment thread bind_test.go
Comment thread tests/std/1917_test.go
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Claude review

Fixes #1917 by giving big.Int/*big.Int a dedicated arm in formatValue() ahead of fmt.Stringer, so a wide integer binds as toInt128('…')/toUInt128/toInt256/toUInt256 in SQL mode and as a bare decimal in server-side {name:Type} mode. The root cause is correctly identified and the fix is well placed: it also repairs the by-value big.Int case (previously fmt.Sprint struct garbage) and nil *big.Int (previously '<nil>', since big.Int has a pointer-receiver String, so the existing nil-Stringer guard never fired).

The follow-up push addressing the earlier array review is sound. commonBigIntConvFunc picks one conversion for the whole literal, keeps the array signed when any element is negative, and falls back to per-element formatting when no single wide type fits — so no new error class is introduced, and the out-of-range error still surfaces through the fallback.

Coverage looks right: unit tables over both format modes and both pointer/value forms, tests/issues/1917_test.go over Native + HTTP via TestProtocols, and tests/std/1917_test.go for the database/sql surface. t.Cleanup on every connection, and both files version-gate wide integers (21.12+) with the server-side-parameter subtest gated at 22.8+, per the project rules.

One remaining issue, inline: the single-conversion unification is one level deep, so a mixed-magnitude big.Int in a map or a nested array still emits sibling toInt128/toUInt256 and now errors where the old quoted-string form worked. That is a narrow input and the author deliberately scoped it out, but it should at least be documented in the code.

Blind spots:

  • Could not fetch or apply the PR branch in this run (permissions), so gofmt/go vet/the unit suite on the branch and the integration tests against a live server are taken from the author's report; all reasoning above is from reading the diff against the checked-out main.
  • ClickHouse's exact getLeastSupertype behavior for map() and nested arrays across the 25.8 → 26.x range is inferred from the author's reproduced NO_COMMON_TYPE, not independently reproduced.

Verdict: ✅ Approve

Inline comments are attached to the relevant lines. This summary updates in place on re-review.

A []*big.Int / big.Int slice (and clickhouse.ArraySet) was formatted with a
per-element wide-integer conversion, so a mixed-width array such as
[]*big.Int{1, uint256Max} became [toInt128('1'), toUInt256('...')]. Those
element types have no common ClickHouse type unless use_variant_as_common_type
is on (off by default before 26.x), so WHERE ... IN ? failed with NO_COMMON_TYPE
on 25.8 (the CI floor) where the old quoted-string form used to coerce cleanly.

Render such arrays with one conversion — the narrowest wide type that holds
every element — via a shared bigIntArray helper, falling back to per-element
formatting when no single type fits (unchanged behavior). Tuples and scalars
are untouched.

Also version-gate the wide-integer integration tests at 21.12 (native and
database/sql), matching tests/bigint_test.go, and merge a split stdlib import
group, per review feedback.
Comment thread bind.go
…eview)

commonBigIntConvFunc unifies only a flat []*big.Int / ArraySet. Mixed-magnitude
big.Int values nested in a map, a nested array, or a tuple used as an IN set
still get per-element toInt128/toUInt256 conversions and can hit NO_COMMON_TYPE.
Document that boundary so a user hitting it from a map/nested bind can find the
reason; the deeper container unification is tracked as a separate follow-up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[backfill: ClickHouse/clickhouse-go] Int128/UInt128 bind parameters serialized as quoted strings, not integer literals

3 participants