Skip to content

Store the wide decimal on the sixteen byte plane - #785

Merged
tamnd merged 1 commit into
mainfrom
s2-wide-decimal
Aug 25, 2026
Merged

tamnd merged 1 commit into
mainfrom
s2-wide-decimal

Conversation

@tamnd

@tamnd tamnd commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Closes the note left open on the decimal row of S2 (#700).

A decimal was already one declared type: the column holds unscaled units and the scale in the catalog is what makes them a number. This makes it one declared type stored two ways. Up to eighteen digits the units ride a lane word, as they have since #765. Above that they are sixteen little endian bytes at a fixed stride, which is the plane INT128 opened in #777 and the layout BINARY(16) has always used. Nothing new was needed on the storage side: fixed_octets is the single hook that puts a column on that layout, write_columns is generic over it, and answering Some(16) gets padding, the segment write and the reader for free.

The precision in the declared type is what decides which of the two a column is, and writer and reader both ask fixed_octets rather than deciding for themselves, so they cannot disagree about a column they are looking at together.

Where the frontier is now

At the value carrier, which is where the wide integers landed too. Value::Decimal carries an i128 of unscaled units, thirty eight digits is the widest an i128 holds, so thirty eight digits is the widest a value of one can be. DECIMAL(39,2) has no declared form and is refused at the declaration with 42000, the same place and the same condition the wide decimal's own refusal used to land. Below that line every precision a statement can spell is a column a statement can fill.

One consequence worth naming: a bare DECIMAL is DECIMAL(38,0) by the parse cap, so a bare DECIMAL is now a declarable and storable property type. It was not before.

The example that had to move

DECIMAL(38,2) was the canonical spelled and not storable example in six places, and it stores now. All six moved to INT256, which is durable in a way the decimal was not: the decimal was one carrier away from storing and did store, whereas an INT256 column would take a row in and be unable to give it back, and that does not change until the engine has a two hundred and fifty six bit carrier. The six are session.rs, refusals.rs, refusal_shape.rs with its regenerated snapshot, catalog_statements.rs, graph_type.rs and the corpus case a-property-type-no-column-can-hold.

What is checked

check_declared reads sixteen octets back into a Decimal at the declared scale and compares its digit count to the declared precision, so a wide column is held to its declaration the same way the narrow one is, on the same code path the narrow one uses. frontier() in props.rs now pins DECIMAL(38,2) storable and DECIMAL(39,2) not, and the extended type test walks precisions 1, 9, 18, 19, 38, 39 and 76 and asserts which plane each is on. The new end to end test declares DECIMAL(32,4), inserts a twenty eight digit value, a rescale up from DECIMAL(5,1) and a bare integer, reads all three back in order, and pins the two 22003 refusals: thirty three digits, and a fifth decimal place.

The directory version goes to 14.

Local gates green: fmt, clippy with -D warnings, xtask terms, xtask api-map, and 89 test result: ok across the workspace with no failures.

A decimal is one declared type stored two ways. Up to eighteen digits
the column is a lane word of unscaled units; above that it is sixteen
little endian bytes of them at a fixed stride, which is the plane
INT128 opened and the layout BINARY(16) already used. The precision in
the declared type is what tells writer and reader which, and both ask
the same function, fixed_octets, so they cannot disagree.

That moves the frontier for the type to where an i128 ends. Thirty
eight digits is the widest an i128 holds, so it is the widest a value
of one can be, and DECIMAL(39,2) is refused at the declaration rather
than at the table. A bare DECIMAL, which is DECIMAL(38,0), becomes a
declarable and storable property type for the first time.

DECIMAL(38,2) was the canonical spelled and not storable example in six
places. All six now use INT256, which is durable: it will not become
storable without a two hundred and fifty six bit value carrier.

The directory version goes to 14.
@tamnd
tamnd merged commit 547ee92 into main Aug 25, 2026
39 of 40 checks passed
@tamnd
tamnd deleted the s2-wide-decimal branch August 25, 2026 09:57
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