ClickHouseDecimal: division throws "Scale cannot be <0" when the quotient has more than 50 integer digits
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 88/100
Research direction
Start in ClickHouse.Driver/Numerics/ClickHouseDecimal.cs at operator / around lines 239-253, then read the existing ClickHouseDecimalTests.ShouldDivide coverage. Reproduce the large-quotient cases and add regression coverage for divisions with a higher divisor scale, including the existing 1 / 3 and 1E60 / 3 behaviors; done means valid quotients no longer reach the negative-scale constructor path.
Written by the indexing model from the issue text.
Description
Describe the bug
ClickHouseDecimal operator / throws ArgumentException: Scale cannot be <0 (Parameter 'scale') for some valid divisions. It occurs when both conditions are true:
- the divisor has more fractional digits than the dividend (
divisor.Scale > dividend.Scale), and - the quotient has more integer digits than
MaxDivisionPrecision(default 50) allows.
Examples: 1 / 1E-60, 1E60 / 0.3, 1E45 / 0.000001. If an application sets MaxDivisionPrecision lower, small values also fail: with MaxDivisionPrecision = 5, 123456789 / 0.5 throws.
The exception comes from the constructor's invariant check, not from a designed error path. The same operator returns large integer quotients when both scales are 0 (1E60 / 3 returns the 60-digit result at scale 0), and it returns 1 / 0.3 correctly.
UseCustomDecimals is true by default, so Decimal(P, S) values read by the HTTP client are ClickHouseDecimal with Scale = S. Values read from Decimal128/Decimal256 columns reach this path directly.
Steps to reproduce
- Read
toDecimal256('1', 0)andtoDecimal256('0.000…001', 60)(1E-60) with the default settings. - Divide the first value by the second.
- The division throws
ArgumentException.
Expected behaviour
The operator returns the quotient, truncated to the division precision, as it does for smaller quotients. The server computes these quotients with divideDecimal:
$ curl -s --data-binary "SELECT divideDecimal(toDecimal256('1', 0), toDecimal256('0.000000000000000000000000000000000000000000000000000000000001', 60), 0)" http://clickhouse:8123/
1000000000000000000000000000000000000000000000000000000000000
$ curl -s --data-binary "SELECT divideDecimal(toDecimal256('1000000000000000000000000000000000000000000000000000000000000', 0), toDecimal32('0.3', 1), 0)" http://clickhouse:8123/
3333333333333333333333333333333333333333333333333333333333333
$ curl -s --data-binary "SELECT divideDecimal(toDecimal256('1000000000000000000000000000000000000000000000', 0), toDecimal64('0.000001', 6), 0)" http://clickhouse:8123/
1000000000000000000000000000000000000000000000000000
Note: the server's / operator is not a usable reference here. It rejects every decimal division where the divisor scale is larger than the dividend scale, including toDecimal64(1, 0) / toDecimal64('0.3', 1) (Decimal result's scale is less than argument's one, ARGUMENT_OUT_OF_BOUND). ClickHouseDecimal already supports that case.
Code example
using var conn = new ClickHouseConnection(connectionString); // UseCustomDecimals = true (default)
await conn.OpenAsync();
var one = (ClickHouseDecimal)await conn.ExecuteScalarAsync("SELECT toDecimal256('1', 0)");
var small = (ClickHouseDecimal)await conn.ExecuteScalarAsync(
"SELECT toDecimal256('0.000000000000000000000000000000000000000000000000000000000001', 60)");
var q = one / small; // ArgumentException: Scale cannot be <0 (Parameter 'scale')
// No server needed:
var huge = ClickHouseDecimal.Parse("1" + new string('0', 60), CultureInfo.InvariantCulture);
var r = huge / ClickHouseDecimal.Parse("0.3", CultureInfo.InvariantCulture); // same ArgumentException
Observed results:
| Division | Result | Expected |
|---|---|---|
1 / 1E-60 (values read from the server, see above) |
ArgumentException |
1E60 |
1E60 / 0.3 |
ArgumentException |
3333…3 (61 digits) |
-1E60 / 0.3, 1E60 / -0.3 |
ArgumentException |
∓3333…3 (61 digits) |
1E45 / 0.000001 |
ArgumentException |
1E51 |
5 / 1E-56 |
ArgumentException |
5E56 |
1.5 / 1E-60 |
ArgumentException |
1.5E60 |
MaxDivisionPrecision = 5: 123456789 / 0.5 |
ArgumentException |
246913578 |
MaxDivisionPrecision = 0: 10 / 0.5 |
ArgumentException |
20 |
1E60 / 3 (both scales 0) |
333…3 (60 digits), scale 0 |
correct |
1E50 / 0.3, 1 / 3, 0 / 1E-60 |
correct | correct |
Root cause
ClickHouse.Driver/Numerics/ClickHouseDecimal.cs, operator / (lines 239-253):
var bias = MaxDivisionPrecision - (NumberOfDigits(dividend_mantissa) - NumberOfDigits(divisor_mantissa));
bias = Math.Max(0, bias);
...
var result_scale = dividend.Scale - divisor.Scale + bias;
Normalize(ref result_mantissa, ref result_scale);
return new ClickHouseDecimal(result_mantissa, result_scale);
result_scale is negative when divisor.Scale > dividend.Scale + bias. bias gets small (or is clamped to 0) when the quotient is larger than about 10^MaxDivisionPrecision. Normalize only changes the scale while scale > 0, so the negative scale reaches the ClickHouseDecimal(BigInteger, int) constructor, which throws at line 57.
This is separate from #642. With the change in #647 applied, the same divisions still throw, and 1E50 / 0.3 also starts to throw (its dividend digit count goes from 50 to 51, so bias is clamped to 0).
Suggested fix
Make sure that bias is at least divisor.Scale - dividend.Scale, so that result_scale is never negative:
bias = Math.Max(bias, divisor.Scale - dividend.Scale);
The quotient is then the integer part, truncated toward zero, at scale 0. This agrees with the current 1E60 / 3 behavior, where the integer part is kept in full even when it has more than MaxDivisionPrecision digits. Cases that must keep their current results: 1 / 3, 1E60 / 3, and the System.Decimal-range divisions in ClickHouseDecimalTests.ShouldDivide. Inside the System.Decimal range this path does not occur, which is why the existing tests do not find it.
Error log
System.ArgumentException: Scale cannot be <0 (Parameter 'scale')
Configuration
Environment
- Client version:
mainat 3e2f799 (1.5.0) - Language version: C# (.NET SDK 10.0.203)
- .NET version: net10.0
- OS: Linux (Debian, Docker)
ClickHouse server
- ClickHouse Server version: 26.9.1.1629
- ClickHouse Server non-default settings, if any: none
CREATE TABLEstatements for tables involved: none (literalSELECTs)
Found by automated analysis of the client (Polyglot AI) while working on #642 / #647. Verified with a running test against a live server, not by inspection only.
- Dominant language
- C#
- Stars
- 100
- Forks
- 26
- Avg merge
- 20h 24m
- Merged PRs (30d)
- 7
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ClickHouse/clickhouse-cs
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
ClickHouse/clickhouse-cs#714 ·
Maintainers usually reply within 1 day
-
HTTP client: DateTime64(9) silently stores a wrapped (wrong) value for dates outside 1677-2262Possibly taken @polyglotAI-bot claimed this 1 day ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
ClickHouse/clickhouse-cs#711 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
ClickHouse/clickhouse-cs#689 ·
Maintainers usually reply within 1 day
-
Every query fails with Code 164 (READONLY) for a readonly = 1 user: the driver always sends enable_http_compression and input_format_binary_read_json_as_string as settingsPossibly taken @polyglotAI-bot claimed this 31 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
ClickHouse/clickhouse-cs#659 · 1 comment ·
Maintainers usually reply within 1 day
-
ClickHouseDecimal: Floor() rounds negative non-integers toward zero (-1.5 → -1, server floor gives -2)Possibly taken @polyglotAI-bot claimed this 30 days ago. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/clickhouse-cs#645 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in ClickHouse/clickhouse-cs
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 83/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
RicoSuter/Namotion.Interceptor#918 ·
Maintainers usually reply within 1 day
-
area-integrations
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
Sample solution restore fails with NU1109 (Microsoft.Extensions.Logging.Abstractions downgrade)Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
ardalis/CleanArchitecture#1150 ·
Maintainers usually reply within 1 day