ClickHouseDecimal: division throws "Scale cannot be <0" when the quotient has more than 50 integer digits

Closed Beginner friendly
#650 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
88/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
csharp
Domain
databases

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

bug
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
  1. Read toDecimal256('1', 0) and toDecimal256('0.000…001', 60) (1E-60) with the default settings.
  2. Divide the first value by the second.
  3. 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: main at 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 TABLE statements for tables involved: none (literal SELECTs)

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

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ClickHouse/clickhouse-cs

All issues in ClickHouse/clickhouse-cs

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.