Skip to content

An exact decimal is a decimal.Decimal - #42

Merged
tamnd merged 5 commits into
mainfrom
engine-decimal
Aug 25, 2026
Merged

An exact decimal is a decimal.Decimal#42
tamnd merged 5 commits into
mainfrom
engine-decimal

Conversation

@tamnd

@tamnd tamnd commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The engine grew a decimal at ABI 0.15: an i128 of unscaled units and a scale, reachable today through CAST('1.20' AS DECIMAL(5, 2)). Python already has the type for it, so this hands one over as a decimal.Decimal and takes one back as a parameter.

Not a float, which is the whole reason the engine has the type. A tenth is not a binary fraction, and a price read into a float is not the price.

Built from the engine's own spelling rather than from the digits and the scale, because Decimal("1.20") is the one constructor that is exact for both. Read back through format(d, "f") rather than str(d), because Python prints some decimals with an exponent and Decimal("1E+2") is a hundred at no places rather than a one at two of them.

A parameter that is not an exact number is refused at the call and says which: a NaN or an infinity is not a number the engine holds, thirty eight digits is the widest DECIMAL(p, s) takes, and a scale past that is a point further right than a column could declare. Failing there is the point, since a decimal that quietly became a float would be a query comparing a price against something that is not it.

The type check runs before the integer and the float arms, since a Decimal is neither and would otherwise go through extract::<f64>, which is the loss the caller picked the type to avoid.

NUMBER in the DB-API module now covers it, because PEP 249 puts every numeric column under NUMBER and a decimal is one. The conformance runner learned to print and compare decimals with the scale as well as the number, since two decimals of one number at two scales are equal to Python and print differently. DECIMAL stays a reserved name a case may not write, matching the reference runner.

The engine pin moves to cfdc70f291719309b96a8300dc8425154d0fd5d5.

tamnd added 5 commits August 25, 2026 13:39
The engine grew a decimal at ABI 0.15: an i128 of unscaled units and a
scale, reachable today through CAST('1.20' AS DECIMAL(5, 2)). Python
already has the type for it, so this hands one over as a
decimal.Decimal and takes one back as a parameter.

Not a float, which is the whole reason the engine has the type. A tenth
is not a binary fraction, and a price read into a float is not the
price. Built from the engine's own spelling rather than from the digits
and the scale, because Decimal("1.20") is the one constructor exact for
both, and read back through format(d, "f") rather than str(d), because
Python prints some decimals with an exponent and Decimal("1E+2") is a
hundred at no places rather than a one at two of them.

A parameter that is not an exact number is refused at the call and says
which: a NaN or an infinity is not a number the engine holds, thirty
eight digits is the widest DECIMAL(p, s) takes, and a scale past that
is a point further right than a column could declare. Failing there is
the point, since a decimal that quietly became a float would be a query
comparing a price against something that is not it.

The type check runs before the integer and the float arms, since a
Decimal is neither and would otherwise go through extract::<f64>, which
is the loss the caller picked the type to avoid.

NUMBER in the DB-API module now covers it, because PEP 249 puts every
numeric column under NUMBER and a decimal is one. The conformance
runner learned to print and compare decimals with the scale as well as
the number, since two decimals of one number at two scales are equal to
Python and print differently. DECIMAL stays a reserved name a case may
not write, matching the reference runner.
Three git dependencies point at the engine and the lock records the
revision each of them resolved to, so a pin that moves without it is a
build that fetches one commit and records another.
pyo3 0.29 renamed the downcast family to cast, which is what the rest
of this crate already calls. The decimal type object was the one place
still spelling it the old way, and it did not compile.
The api gate compares the public surface against main and lets a
change through once the version has moved, which is the same rule the
engine gets from cargo semver-checks and the TypeScript client gets
from its committed report. Two names moved here: Value now admits a
decimal.Decimal and NUMBER now counts one as a number. Both are
additions rather than removals, and both are still the surface
changing, so the version moves with them.

The wheel test that wanted two versions out of one build now wants
0.0.2 and 0.0.3, since 0.0.2 is no longer a version this project is
not.
A Windows runner came in at 20.3 ms against a ceiling of 20, and the
module that put it there is decimal, which costs about four
milliseconds and is now imported at package scope because Value names
decimal.Decimal.

The alternative was to import it only for a type checker and leave the
union as a string, which is cheaper and worse: Value stops being a real
object, and every annotation that mentions it stops resolving under
get_type_hints. The engine grew an exact decimal, so the package grew a
module, and the ceiling says so.
@tamnd
tamnd merged commit a0ac0dd into main Aug 25, 2026
35 checks passed
@tamnd
tamnd deleted the engine-decimal branch August 25, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant