PetToys.BigDecimal.Npgsql.Dapper
1.0.1
Prefix Reserved
dotnet add package PetToys.BigDecimal.Npgsql.Dapper --version 1.0.1
NuGet\Install-Package PetToys.BigDecimal.Npgsql.Dapper -Version 1.0.1
<PackageReference Include="PetToys.BigDecimal.Npgsql.Dapper" Version="1.0.1" />
<PackageVersion Include="PetToys.BigDecimal.Npgsql.Dapper" Version="1.0.1" />
<PackageReference Include="PetToys.BigDecimal.Npgsql.Dapper" />
paket add PetToys.BigDecimal.Npgsql.Dapper --version 1.0.1
#r "nuget: PetToys.BigDecimal.Npgsql.Dapper, 1.0.1"
#:package PetToys.BigDecimal.Npgsql.Dapper@1.0.1
#addin nuget:?package=PetToys.BigDecimal.Npgsql.Dapper&version=1.0.1
#tool nuget:?package=PetToys.BigDecimal.Npgsql.Dapper&version=1.0.1
PetToys.BigDecimal.Npgsql.Dapper
Dapper mapping for PetToys.BigDecimal.Core: a PostgreSQL numeric
column reads into a BigDecimal and a BigDecimal parameter writes back, at
any width the type holds and with System.Decimal nowhere in the path.
Without it, Query<BigDecimal> over a numeric 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.Npgsql.Dapper
PetToys.BigDecimal.Npgsql 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 registrations and neither works alone. The adapter installs the codec on the data source; this package registers the type handler with Dapper.
using Dapper;
using Npgsql;
using PetToys.BigDecimal.Numerics;
// Once, at start-up: Dapper's registry is static and process-wide.
SqlMapperBigDecimalExtensions.UseBigDecimal();
await using var source = new NpgsqlDataSourceBuilder(connectionString)
.UseBigDecimal()
.Build();
Writing needs nothing else - a parameter carries the value and no database type is named at the call site:
await using var connection = await source.OpenConnectionAsync();
await connection.ExecuteAsync(
"insert into invoices (id, total) values (@id, @total)",
new { id = 1, total = BigDecimal.Parse("123456789012345678901234567890.123456789") });
Reading exactly is a different call from Query<T>, and the reason is in
the next section:
public sealed record Invoice(int Id, BigDecimal Total);
var invoices = await connection.QueryBigDecimalAsync<Invoice>(
"select id, total from invoices where id = @id",
new { id = 1 });
Every numeric column of that query reads as BigDecimal - including
numeric(p, s), an aggregate over one, a column of a domain over one, and
numeric[] into BigDecimal[]. A member of the same result still typed
System.Decimal keeps working: it converts where the value fits and throws
OverflowException where it does not, so nothing narrows quietly.
For anything QueryBigDecimal does not cover - an unbuffered read,
QueryMultiple, multi-mapping with splitOn - wrap the reader and hand it to
Dapper's own Parse<T>:
await using var reader = connection
.ExecuteReader("select id, total from invoices", new { })
.AsBigDecimalReader();
foreach (var invoice in reader.Parse<Invoice>())
{
}
Why reading is a different call
Dapper's extension point for a custom type is SqlMapper.TypeHandler<T>, whose
Parse is handed whatever GetValue already produced. Over a numeric column
that is a System.Decimal, and a value wider than one throws inside the driver
before the handler is reached. There is no way to ask Dapper for the exact read
in an ordinary Query<T>.
The three ways around that all cost something. Making BigDecimal the driver's
default for numeric would change what every untyped read in your application
already produces. Casting the column to text in the SQL makes every query this
package's business. Mapping only the write side leaves reading where it was.
So the exact read is its own call, over a reader that answers numeric columns
with the driver's own typed accessor. Nothing global changes, and the widening
is scoped to the query you chose it for.
Registering it does not change anything else
BigDecimal is for the columns that need it, not a replacement for
System.Decimal across an application. After both registrations, Query<decimal>
over 1.50 still answers 1.50, Query<dynamic> still answers a
System.Decimal, and a reader still reports System.Decimal as the field type
of a numeric column. Code written before this package was referenced reads
exactly what it read before, which the test suite asserts rather than the README
promising it.
What an ordinary Query<T> does after the registration
It gets better, but it is still bounded by System.Decimal, because that is
what the driver produced before the handler saw anything:
| Value in the column | Query<T> |
QueryBigDecimal<T> |
|---|---|---|
Inside System.Decimal |
Exact, scale included | Exact |
Wider than System.Decimal |
OverflowException from the driver |
Exact |
NaN, Infinity, -Infinity |
InvalidCastException or OverflowException from the driver |
Exact |
Neither column of that table narrows a value. Where an ordinary read cannot answer, it fails.
The handler also reads a column you cast yourself - select total::text - which
is exact at any width, if you would rather change the SQL than the call.
What round-trips, and what does not
| Column | Coverage |
|---|---|
numeric(p, s), p up to 77 |
Lossless. This covers numeric(38, 18), the common money and blockchain precision, with room to spare. |
numeric unconstrained, integer part within the magnitude |
Accepted; fractional digits beyond what the magnitude leaves are rounded half to even. PostgreSQL allows 16383 of them, so a value read from such a column can lose digits silently. |
numeric unconstrained, integer part beyond the magnitude |
OverflowException. PostgreSQL allows 131072 integer digits. |
NaN, Infinity, -Infinity |
Lossless through QueryBigDecimal, as BigDecimal.NaN, BigDecimal.PositiveInfinity and BigDecimal.NegativeInfinity. PostgreSQL sorts NaN above every other numeric value where this type sorts it below every other value; both make NaN equal to itself. |
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.
The scale you read back is the column's, and the server rounds the other way
A value goes out at its own scale and PostgreSQL applies the column's. 1.5
written into a numeric(12,4) is stored and read back as 1.5000; written into
an unconstrained numeric it stays 1.5.
Where the column's scale is shorter than the value's, the server rounds half
away from zero, and this type rounds half to even: 1.00025 written into a
numeric(12,4) is stored as 1.0003, where the type's own rescaling would give
1.0002. This package does not pre-empt that - the handler is handed a
parameter and never learns which column it is bound for, so it has no schema to
rescale against. If you need this type's rule, rescale before writing.
A value beyond the column's declared precision is refused by PostgreSQL as
22003 numeric field overflow, raised as a PostgresException. The server does
not name the column and neither can this package.
Requirements
Dapper 2.1.79 up to but not including 3.
PetToys.BigDecimal.Npgsqlon the data source. Without it the read fails withReading as 'BigDecimal' is not supported for fields having DataTypeName 'numeric'and a write is refused naming the type. Neither narrows a value.PostgreSQL 14 or later for the infinities, which is where
numericgained the sign codes that carry them.Trimming and Native AOT: Dapper does not work under either, and this package's own marking does not change that. Measured by publishing and running rather than read off a warning:
PublishTrimmed: the registration and a single-column read still work, and materialising an object throwsInvalidOperationExceptionabout a missing constructor, because the trimmer removed the members Dapper reflects for. An anonymous-object parameter stops binding for the same reason.PublishAot:SqlMapper.AddTypeHandleritself throwsNotSupportedExceptionoverSqlMapper.TypeHandlerCache<T>, before any query runs, because Dapper instantiates that generic reflectively.
IsAotCompatibleis set on this project and this assembly produces no trim, single-file or AOT diagnostic;Dapper2.1.79 carries noIsTrimmablemetadata and producesIL2104andIL3053. If you publish trimmed or Native AOT, reachnumericthroughPetToys.BigDecimal.Npgsqldirectly, which is marked and whose driver is too.
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.Npgsql (>= 1.0.1)
-
net8.0
- Dapper (>= 2.1.79 && < 3.0.0)
- PetToys.BigDecimal.Npgsql (>= 1.0.1)
-
net9.0
- Dapper (>= 2.1.79 && < 3.0.0)
- PetToys.BigDecimal.Npgsql (>= 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.