Fix ClickHouseDecimal: IConvertible narrowing and ChangeType stack overflow - #653
Open
polyglotAI-bot wants to merge 2 commits into
Open
polyglotAI-bot wants to merge 2 commits into
polyglotAI-bot wants to merge 2 commits into
Conversation
ToInt32 cut the value to 16 bits, so InsertBinaryAsync stored a wrong value in an Int32 column. ToChar/ToSByte/ToByte/ToInt16/ToUInt16 wrapped an out-of-range value instead of throwing OverflowException. ToType fell back to Convert.ChangeType, which calls ToType again for every type it does not convert itself, so an unsupported target type overflowed the stack. ToType now maps the IConvertible types to the matching members and throws InvalidCastException for the others, as decimal does. Fixes: #646 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
polyglotAI-bot
requested review from
alex-clickhouse and
mzitnik
as code owners
October 1, 2026 15:50
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The mandatory HTTP documentation update is missing, and the changelog entry needs condensation.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Fixes ClickHouseDecimal conversions in the HTTP client to prevent value wrapping and recursive stack overflow.
Changes:
- Adds checked narrowing conversions and corrects
ToInt32. - Makes
ToTypesafely dispatch supported conversions. - Adds unit, integration, and changelog coverage.
| File | Description |
|---|---|
ClickHouse.Driver/Numerics/ClickHouseDecimal.cs |
Corrects IConvertible behavior. |
ClickHouse.Driver.Tests/Numerics/ClickHouseDecimalTests.cs |
Covers conversion boundaries and unsupported types. |
ClickHouse.Driver.Tests/Numerics/ClickHouseDecimalIntegerColumnTests.cs |
Verifies integer-column writes against ClickHouse. |
changelog.d/646-clickhousedecimal-iconvertible.fixes.md |
Records the conversion fixes. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…gment Add a note to the integer write section of docs/http.mdx: writing a ClickHouseDecimal drops the fractional part, an out-of-range integer part throws OverflowException, and Convert.ChangeType to an unsupported type throws InvalidCastException. Condense the #646 fragment to the user-visible outcomes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Fixes #646.
The
IConvertiblemembers ofClickHouseDecimalhad three defects:ToInt32was(short)(int)this. The extra cast kept only 16 bits, soConvert.ToInt32(new ClickHouseDecimal(100000m))returned-31072.Int32Type.WritecallsConvert.ToInt32(object), soInsertBinaryAsyncstored that wrong value in anInt32column.ToChar,ToSByte,ToByte,ToInt16andToUInt16narrowed the result ofexplicit operator int/uintwith an unchecked cast, so an out-of-range value wrapped (40000→Int16gave-25536).Convert.ToInt16(decimal)and the server (DECIMAL_OVERFLOW) reject it.ToTypefell back toConvert.ChangeType(this, conversionType, provider).Convert.ChangeTypeconverts the primitive types itself and callsIConvertible.ToTypefor all other types. Thus a target such asdecimal?,int?,Guid,DateTimeOffset,TimeSpan, an enum orClickHouseDecimalitself recursed until aStackOverflowException, which stops the process.Changes
ClickHouse.Driver/Numerics/ClickHouseDecimal.cs:ToInt32returns(int)this.checked(...), so they throwOverflowExceptionwhen the integer part is out of range. The fractional part is still truncated toward zero first, as the server does (toUInt8(toDecimal32(255.9, 1))is255, andtoUInt8(toDecimal32(256.5, 1))overflows).ToTypekeeps theBigIntegerbranch. It returns the value fortypeof(ClickHouseDecimal)andtypeof(object), and sends eachIConvertibletype to itsToXxxmember. For all other types it throwsInvalidCastException, asdecimaldoes (Convert.DefaultToType). Enums are excluded explicitly, because they report theTypeCodeof their underlying type. Anulltype still throwsArgumentNullException.changelog.d/646-clickhousedecimal-iconvertible.fixes.md: changelog fragment.Not changed: in-range results,
ToUInt32/ToInt64/ToUInt64/ToSingle/ToDouble/ToDecimal,ToDateTime(stillNotSupportedException), andChangeTypetostringandBigInteger. The TCP client is not affected (ClickHouseTcpDecimaldoes not implementIConvertible).Other driver paths that reach these members get the same fix with no further change:
Int8Type/UInt8Type/Int16Type/UInt16Typewrites,Enum8Type/Enum16Typewrites (now throw instead of writing a wrapped index), and theTimeparameter path inHttpParameterFormatter(Convert.ToInt32).PR #647 (for #642) edits other lines of the same two files. The two PRs do not overlap.
ToType(typeof(BigInteger))for values below 1 depends on theNumberOfDigitsfix in #647, so this PR does not change it.Test
ClickHouseDecimalIntegerColumnTests(real server):InsertBinaryAsync_ClickHouseDecimalIntoInt32Column_StoresExactValue: inserts100000.00,-40000,int.MinValueandint.MaxValueinto anInt32column and reads them back. Onmainthe stored values are-31072,25536,0and-1.InsertBinaryAsync_ClickHouseDecimalOutOfColumnRange_ThrowsOverflowException: forInt8,UInt8,Int16andUInt16. Onmainthe insert succeeds with a wrapped value.ClickHouseDecimalTests:ChangeType_NarrowIntegralTypeInRange_ReturnsValueandChangeType_NarrowIntegralTypeOutOfRange_ThrowsOverflowException: the boundaries of each narrowed type, fractional values, and a 70-digit mantissa. The expected values for fractional input come from the server.ChangeType_TypeWithoutConversion_ThrowsInvalidCastException:decimal?,int?,Guid,DateTimeOffset,TimeSpan, an enum andIntPtr. Onmainthe test host crashes with a stack overflow.ToType_OwnTypeOrObject_ReturnsSameValue,ToType_ConvertibleType_MatchesChangeTypeandToType_NullType_ThrowsArgumentNullException: direct calls toToType.ChangeTypenever callsToTypefor the primitive types, so these tests are the only coverage of the new mapping. They also check that each type is boxed as itself.The full
ClickHouse.Driver.Testssuite passes on net10.0 against ClickHouse 26.9 (11125 passed, 0 failed). No existing test was changed.Checklist
mainand passes with the fixInt32Type.Write→Convert.ToInt32→ToInt32)🤖 Generated with Claude Code