PetToys.BigDecimal.ClickHouse.Dapper 1.0.1

Prefix Reserved
dotnet add package PetToys.BigDecimal.ClickHouse.Dapper --version 1.0.1
                    
NuGet\Install-Package PetToys.BigDecimal.ClickHouse.Dapper -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.Dapper" 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.Dapper" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="PetToys.BigDecimal.ClickHouse.Dapper" />
                    
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.Dapper --version 1.0.1
                    
#r "nuget: PetToys.BigDecimal.ClickHouse.Dapper, 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.Dapper@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.Dapper&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=PetToys.BigDecimal.ClickHouse.Dapper&version=1.0.1
                    
Install as a Cake Tool

PetToys.BigDecimal.ClickHouse.Dapper

NuGet Version NuGet Downloads Unit Test Target frameworks License

Dapper mapping for PetToys.BigDecimal.Core: a ClickHouse Decimal32, Decimal64, Decimal128 or Decimal256 column reads into a BigDecimal and a BigDecimal parameter writes back, at any precision and scale ClickHouse allows and with System.Decimal nowhere in the path.

Without it, Query<BigDecimal> over a decimal column returns a default-constructed value and throws nothing, because Dapper falls through to property-by-name mapping and the struct's own properties are not column names. That silent answer is what this package replaces.

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

Installation

dotnet add package PetToys.BigDecimal.ClickHouse.Dapper

PetToys.BigDecimal.ClickHouse and PetToys.BigDecimal.Core come along as dependencies.

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

There are two calls and neither works alone. One registers the handlers with Dapper, for the process; the other configures a connection.

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

// Once, at start-up: Dapper's registry is static and process-wide.
ClickHouseSqlMapperBigDecimalExtensions.UseBigDecimal();

await using var connection = new ClickHouseConnection(
    new ClickHouseClientSettings(connectionString).UseBigDecimalForDapper());

Reading is Query<T> and nothing else - no call of ours, no wrapper:

public sealed class Invoice
{
    public int Id { get; set; }

    public BigDecimal Total { get; set; }
}

var invoices = await connection.QueryAsync<Invoice>(
    "SELECT id, total FROM invoices WHERE id = 1");

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

Writing needs the parameter's type named in the statement, in ClickHouse's own syntax, and the parameters passed as DynamicParameters:

var parameters = new DynamicParameters();
parameters.Add("total", BigDecimal.Parse("123456789012345678901234567890.123456789"));

await connection.ExecuteAsync(
    "INSERT INTO invoices (id, total) VALUES (1, {total:Decimal256(9)})",
    parameters);

Both halves of that are load bearing and the next section says why.

Why writing looks like that

ClickHouse names a parameter's type in the statement rather than on the parameter, and that name is the only way this package can learn the column's scale and width. With it, the value is rescaled at the column's scale before it is sent and a value the column cannot hold is refused by name; without it, there is nothing to rescale against.

Dapper binds a parameter only when it finds its name in the statement as @total, :total or ?total. ClickHouse's {total:Decimal256(9)} is none of those, so a parameter passed as an anonymous object is dropped and the server answers:

Code: 456. DB::Exception: Substitution `total` is not set.

DynamicParameters with Add is not filtered and is the shape to use. Note that new DynamicParameters(new { total = value }) - the template form - is filtered like the anonymous object it wraps.

A parameter written as @total with no type named anywhere survives Dapper - that syntax is Dapper's own - and is refused by this repository when it reaches the driver:

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.

UseBigDecimalForDapper installs that guard along with the formatter. A parameter type resolver you had already put on the settings is kept and asked about every type this repository does not map. Nothing is narrowed on any of these routes.

Registering it does not change anything else

BigDecimal is for the columns that need it, not a replacement for the driver's own decimal across an application. After the registration, Query<decimal> over a Decimal64(4) column holding 1.5 still answers 1.5000, Query<dynamic> still answers a ClickHouseDecimal, and a reader still reports ClickHouseDecimal as the field type. Code written before this package was referenced reads exactly what it read before, which the test suite asserts rather than this README promising it.

That is the difference between this package and the connection-wide UseBigDecimal in PetToys.BigDecimal.ClickHouse, which maps every decimal column on the connection whether or not anything asked. Both are supported and they compose: a caller who has already used the wide form has this package's formatter as well, and the handlers then receive values it has already converted.

What round-trips, and what does not

Column Coverage
Decimal32(s), Decimal64(s), Decimal128(s), Decimal256(s) Lossless in both directions, at every precision and scale ClickHouse allows.
Nullable(Decimal...) As BigDecimal?. A NULL column read into a non-nullable member is left at its default, which is what Dapper does for System.Decimal too.
Array(Decimal...) As BigDecimal[] when reading. Writing an array is not covered - use InsertBigDecimalAsync in PetToys.BigDecimal.ClickHouse.
NaN, Infinity, -Infinity Refused when writing, with NotSupportedException. No ClickHouse decimal 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 that, so reading never overflows and writing is where the boundaries are.

The scale you read back is the column's

ClickHouse keeps a decimal's scale in the column type and nowhere in the value, so 1.5 written into a Decimal64(4) column reads back as 1.5000.

Where a written value has more fractional digits than the column's scale, this package rescales it half to even before the statement is sent. Both the driver and the server truncate toward zero instead, so a value that reached either of them unrescaled would be a different number: 1.00015 into a Decimal64(4) column is stored as 1.0002 here and would be 1.0001 if the server did it.

A value whose mantissa at the column's scale is outside the column's payload width or its declared precision raises OverflowException naming the parameter and the type, before anything is sent. Where the server computes a value - an aggregate, an expression, a cast - the arithmetic and the rounding are the server's.

Requirements

  • Dapper 2.1.79 up to but not including 3, and ClickHouse.Driver 1.4.0 up to but not including 2.

  • UseBigDecimalForDapper on the connection settings, or the wide UseBigDecimal from PetToys.BigDecimal.ClickHouse. Without either, a read fails naming UseCustomDecimals and a write fails with InvalidCastException from the driver. Neither narrows a value.

  • Trimming and Native AOT: this package works under neither, and both of its dependencies are the reason. Its own marking says only that this assembly's code produces no trim, single-file or AOT diagnostic.

    • Dapper, measured by publishing and running: under PublishTrimmed materialising an object throws InvalidOperationException about a missing constructor and an anonymous-object parameter stops binding; under PublishAot SqlMapper.AddTypeHandler throws NotSupportedException over SqlMapper.TypeHandlerCache<T> before any query runs. Dapper 2.1.79 carries no IsTrimmable metadata and produces IL2104 and IL3053.
    • ClickHouse.Driver, measured the same way: 1.4.0 publishes Native AOT and then throws TypeInitializationException on the first query, and carries no IsTrimmable metadata in either of its assemblies.

    If you publish trimmed, reach decimal columns through PetToys.BigDecimal.ClickHouse directly, which is what its own trimming probe covers.

Two things about Dapper's registry

It is static and process-wide - it takes no scope argument - so UseBigDecimal is a start-up call rather than a per-connection one. It is idempotent.

It also replaces any handler already registered for BigDecimal, silently, and Dapper publishes no way to read the registry back, so a handler of your own has to be registered after this one.

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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 92 9/13/2026
1.0.0 91 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.