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
<PackageReference Include="PetToys.BigDecimal.ClickHouse.Dapper" Version="1.0.1" />
<PackageVersion Include="PetToys.BigDecimal.ClickHouse.Dapper" Version="1.0.1" />
<PackageReference Include="PetToys.BigDecimal.ClickHouse.Dapper" />
paket add PetToys.BigDecimal.ClickHouse.Dapper --version 1.0.1
#r "nuget: PetToys.BigDecimal.ClickHouse.Dapper, 1.0.1"
#:package PetToys.BigDecimal.ClickHouse.Dapper@1.0.1
#addin nuget:?package=PetToys.BigDecimal.ClickHouse.Dapper&version=1.0.1
#tool nuget:?package=PetToys.BigDecimal.ClickHouse.Dapper&version=1.0.1
PetToys.BigDecimal.ClickHouse.Dapper
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 withread:packageseven 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.Driver1.4.0 up to but not including 2.UseBigDecimalForDapperon the connection settings, or the wideUseBigDecimalfromPetToys.BigDecimal.ClickHouse. Without either, a read fails namingUseCustomDecimalsand a write fails withInvalidCastExceptionfrom 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
PublishTrimmedmaterialising an object throwsInvalidOperationExceptionabout a missing constructor and an anonymous-object parameter stops binding; underPublishAotSqlMapper.AddTypeHandlerthrowsNotSupportedExceptionoverSqlMapper.TypeHandlerCache<T>before any query runs.Dapper2.1.79 carries noIsTrimmablemetadata and producesIL2104andIL3053. ClickHouse.Driver, measured the same way: 1.4.0 publishes Native AOT and then throwsTypeInitializationExceptionon the first query, and carries noIsTrimmablemetadata in either of its assemblies.
If you publish trimmed, reach decimal columns through
PetToys.BigDecimal.ClickHousedirectly, which is what its own trimming probe covers.- Dapper, measured by publishing and running: under
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.
Links
License
Provided under the Apache License, Version 2.0.
| Product | Versions 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. |
-
net10.0
- Dapper (>= 2.1.79 && < 3.0.0)
- PetToys.BigDecimal.ClickHouse (>= 1.0.1)
-
net8.0
- Dapper (>= 2.1.79 && < 3.0.0)
- PetToys.BigDecimal.ClickHouse (>= 1.0.1)
-
net9.0
- Dapper (>= 2.1.79 && < 3.0.0)
- PetToys.BigDecimal.ClickHouse (>= 1.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
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.