PetToys.BigDecimal.ClickHouse 1.0.1

Prefix Reserved
dotnet add package PetToys.BigDecimal.ClickHouse --version 1.0.1
                    
NuGet\Install-Package PetToys.BigDecimal.ClickHouse -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PetToys.BigDecimal.ClickHouse" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PetToys.BigDecimal.ClickHouse" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="PetToys.BigDecimal.ClickHouse" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PetToys.BigDecimal.ClickHouse --version 1.0.1
                    
#r "nuget: PetToys.BigDecimal.ClickHouse, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PetToys.BigDecimal.ClickHouse@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PetToys.BigDecimal.ClickHouse&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=PetToys.BigDecimal.ClickHouse&version=1.0.1
                    
Install as a Cake Tool

PetToys.BigDecimal.ClickHouse

NuGet Version NuGet Downloads Unit Test Target frameworks License

ClickHouse helpers for PetToys.BigDecimal.Core: they map the Decimal32, Decimal64, Decimal128, and Decimal256 column types - whose range and scale go well past decimal - onto BigDecimal when reading and writing through ClickHouse.Driver.

Bring your own configured connection; this package only deals with the value mapping.

Installation

dotnet add package PetToys.BigDecimal.ClickHouse

The core type's own package, PetToys.BigDecimal.Core, comes along as a dependency.

Releases go to nuget.org, so the command above is all that is needed. Prereleases are published to GitHub Packages instead: that feed has to be added to your nuget.config, and it requires a personal access token with read:packages even for a public package.

Usage

Ask for the mapping on the query that needs it:

using ClickHouse.Driver;
using PetToys.BigDecimal.Numerics;

using var client = new ClickHouseClient(connectionString);

await using var reader = await client.ExecuteReaderAsync(
    "SELECT total FROM invoices WHERE id = 1",
    null,
    ClickHouseBigDecimal.CreateQueryOptions(),
    cancellationToken);

await reader.ReadAsync(cancellationToken);

BigDecimal total = reader.GetBigDecimal("total");

Nullable(Decimal...) and Array(Decimal...) come with it, as BigDecimal? and BigDecimal[]. Every other column of the same row reads exactly as it would without this package.

To write in bulk:

await client.InsertBigDecimalAsync(
    "invoices",
    ["id", "total"],
    rows,
    options: null,
    cancellationToken);

InsertBigDecimalAsync reads the destination's column types from the server once per call, because a width that disagrees with the column does not fail, it stores a different number. A loader that inserts many batches should declare them instead, through InsertOptions.ColumnTypes, which skips the lookup.

To write one value as a query parameter, annotate its type in the statement, as ClickHouse requires, and build the connection from settings carrying the mapping:

using ClickHouse.Driver;
using ClickHouse.Driver.ADO;

var settings = new ClickHouseClientSettings(connectionString).UseBigDecimal();

await using var connection = new ClickHouseConnection(settings);
await connection.OpenAsync(cancellationToken);

await using var command = connection.CreateCommand(
    "INSERT INTO invoices (id, total) VALUES (1, {total:Decimal256(6)})");
command.AddParameter("total", value);
await command.ExecuteNonQueryAsync(cancellationToken);

The two scopes, and what the wide one changes

CreateQueryOptions maps one query. UseBigDecimal on ClickHouseClientSettings maps every query on the connection, and the two are not equivalent:

  • Every decimal column read on that connection becomes a BigDecimal, including in code written before this package was referenced.
  • GetFieldType keeps reporting the driver's own decimal type while GetValue returns a BigDecimal. The reader disagrees with itself, and nothing outside the driver can change it, so anything that builds a schema from the reader and then fills it - a DataTable above all - sees a column typed for one type receiving another.
  • The read hook is consulted once per value, not once per column, so a wide result set pays for every column of every row.

It exists because ClickHouseCommand has no per-query hook: an ADO caller, or anything layered on one, has no narrower option. Where ClickHouseClient is in reach, prefer the per-query form.

GetFieldValue<BigDecimal> does not work in either scope and cannot be made to: the driver casts its own value to the requested type before consulting the hook that would have changed it. Use the accessors above.

Everything this package adds lives in the ClickHouse.Driver namespace, the one a caller already imports, including the extensions on types that live below it: ClickHouse.Driver.ADO is needed only where the snippet names ClickHouseClientSettings or ClickHouseConnection itself.

Requirements

  • UseCustomDecimals=true on the connection. The mapping needs the driver's own arbitrary-precision decimals. With the option off, a value wider than decimal raises inside the driver before this package is reached, which nothing here can rescue. UseBigDecimal on the settings switches it on; the per-query form cannot, and says so by name if it meets a column the driver decoded through decimal.
  • ClickHouse.Driver 1.4.0, up to but not including 2. That is the version this package's use of IReadValueConverter and IParameterFormatter was measured against, and the range is closed at the major because the driver passes the formatter its arguments in an order the interface does not declare. A driver major that reshapes that therefore fails at restore rather than at the first write; the ceiling moves once the new major has been tested against.

Trimming yes, Native AOT no, and the reason is the driver

This assembly is marked IsAotCompatible, and its own code is gated by the trim, single-file and AOT analyzers. That marking is per assembly and does not reach ClickHouse.Driver, which is what decides the answer here.

Trimmed publishing works. It is verified by publishing a probe over this package and running it against a real server, on every supported framework. The publish does warn: ClickHouse.Driver 1.4.0 carries no IsTrimmable in either ClickHouse.Driver.dll or ClickHouse.Driver.Common.dll, and neither does its Microsoft.IO.RecyclableMemoryStream dependency, so you get IL2104 for both - "assembly produced trim warnings". That is expected, and it is the reason to test your own application rather than to trust this paragraph.

Native AOT does not work, and it fails at runtime rather than at publish. The driver compiles: the failure arrives on the first query, as a TypeInitializationException out of ClickHouse.Driver.Types.TypeConverter, whose static constructor calls TupleType.BuildTupleFactory to look up a constructor of System.Tuple<double, double> by reflection, which ILC has removed. Nothing in this package is on that path, and nothing in it can rescue the call. If you need Native AOT against ClickHouse today, the way through is the driver's own issue tracker, not a workaround here.

What round-trips, and what does not

Column Coverage
Decimal32(s), Decimal64(s), Decimal128(s), Decimal256(s) Read losslessly, at every precision and scale ClickHouse allows.
The same, when writing Lossless while the value fits the column's precision and width. Fractional digits beyond the column's scale are rounded half to even.
A value whose integer part exceeds the column OverflowException, naming the column, its declared type, and which of the two bounds it crossed.
NaN, Infinity, -Infinity Refused with NotSupportedException before anything is sent, naming the value and the width. No ClickHouse decimal type represents them.

The type's own bounds are 77 significant digits, a largest magnitude of 2^256-1, and a range of 1e-255 to approximately 1.157e77. Every ClickHouse decimal fits inside them, which is why reading never overflows and never rounds, and writing is the direction with a boundary. That is the opposite of the PostgreSQL adapter.

Two things ClickHouse does that this package does not hide

A round trip returns the column's scale, not the value's. Writing 1.5 into a Decimal64(4) reads back 1.5000. ClickHouse keeps the scale in the column type and nowhere in the value, so there is nothing to restore it from; the values remain numerically equal.

Rounding is this package's, not the server's. ClickHouse truncates toward zero when it parses a decimal literal into a narrower column, and so does the driver when it lowers a scale: both turn 0.135 into 0.13 at scale 2. This type rounds half to even, giving 0.14, and every write path here rescales the value itself so that neither of the other two is ever asked to. One rule, whichever path a value takes.

A parameter has to name its type, and forgetting it is refused by name

ClickHouse takes a parameter's type from the statement, {total:Decimal256(6)}, and that annotation is the only place a column's type reaches this package. Write @total instead and there is no scale to rescale against, so the write is refused before anything is sent:

The parameter 'total' carries a BigDecimal and the statement does not name its
ClickHouse type, so the column's scale is unknown. Annotate it in the statement,
as in {total:Decimal256(6)}. Through Dapper, pass it with DynamicParameters.Add
as well - an anonymous object is stripped before the parameter reaches this
package.

The refusal is this package's, and it arrives instead of the driver's own Unknown type, which names the CLR type and nothing else. Nothing is guessed: a type derived from the value rather than from the column would let the server truncate at the column's scale, which is the one thing this package exists to prevent.

UseBigDecimal installs that guard, and it is the only hook here that composes rather than replaces. A parameter type resolver already on the settings is kept and asked about every type this package does not map; the read hook and the parameter formatter are replaced, because neither of those interfaces lets an implementation say a value is not its own.

License

Provided under the Apache License, Version 2.0.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on PetToys.BigDecimal.ClickHouse:

Package Downloads
PetToys.BigDecimal.ClickHouse.Dapper

Dapper mapping that reads and writes ClickHouse Decimal32, Decimal64, Decimal128, and Decimal256 columns as PetToys.BigDecimal.Core values, over the formatter PetToys.BigDecimal.ClickHouse installs on the connection.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 100 9/13/2026
1.0.0 107 9/12/2026

A patch release over 1.0.0. One tag versions every package in the repository, so
all six are published together; what each of them carries is below.

PetToys.BigDecimal.Core

Fixed:
- Parsing a literal whose exponent is far below the scale floor no longer costs
 one division per digit it drops. A scale above 255 is reduced to it, so
 "1e-99999" asked the pack to drop 99744 decimal positions and it divided for
 every one of them, thousands of times over after the magnitude had already
 become zero. The parser caps an exponent at 100000, which is what bounds the
 worst case rather than what creates it. That input took 48 microseconds on
 .NET 10 before the change; after it, the benchmark reads the same literal at
 1.2x a parse that drops nothing, such as "1e-200". Every parsed value, scale
 and sign is unchanged - those literals are zero at scale 255 before the
 change and after it - and rounding, rescaling and the database wire paths
 reach the same helper and stop the same way.

Notes:
- What GetHashCode costs is documented now, in the README and in the method's
 own remarks. A hash has to agree with numeric equality, so it goes through
 the value's shortest form: one carrying no trailing zeros costs 12.7x to 16.1x
 decimal's hash, which strips them for the same reason, and one widened to a
 database column's scale about 2x as much, because that one pays a division
 pass over the magnitude. Nothing in the algorithm changed - the table of costs
 covered arithmetic, parsing and formatting and simply did not reach hashing.
- What Pow guarantees for a power it cannot represent exactly is stated now, in
 the method's own remarks and in the README, and the suite carries the deep
 chains that back it. The power is raised in a working width of 154 digits
 against the 77 a result keeps, so the rounding reads the digit the exact power
 reads unless the exact power sits nearer the midpoint than 1e-66 of a unit in
 the last place, which takes a 5 and then sixty-five zeros past the 77th
 digit, or a 4 and then sixty-five nines. Nothing in the algorithm changed and
 no value moved; what changed is that the documents no longer read as though
 the exact power were carried.