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
tamnd added a commit that referenced this pull request Oct 1, 2026
#785 made zu1::props::fixed_octets public without regenerating docs/api/model.json, which is why api-model is red on main.
tamnd added a commit that referenced this pull request Oct 1, 2026
* Publish to crates.io as zudb, from a tag

The crate name zu is taken on crates.io, so every crate that goes up is
zudb or zudb-*, which is what the overview already promised. The
packages are renamed and the libraries are not: the workspace table
names each one under its old key with `package = "zudb-..."`, and the
inner crates keep `[lib] name = "zu_..."`, so no source outside the
engine crate changes. The engine crate's library is zudb because that
is what `use zudb::Database` in the README needs, so its own tests,
benches and doctests say zudb.

Fourteen crates are published: zudb, the twelve under it that it or the
CLI reaches, and zudb-cli, which installs the zu binary. The internal
pins are exact, so a version bump that misses one fails to resolve
rather than publishing against the previous release.

The release workflow gains three real steps. verify checks the tag
against the workspace and the changelog and runs a full publish dry
run, so a crate that does not build from its tarball stops the release
before anything is uploaded. crates-io runs scripts/publish-crates.sh,
which is rudb's: it asks the index what is up, excludes it, and waits
out the new crate limit, so the first release is about an hour and a
half once and a re-run finishes a partial one. The GitHub release is
created after crates.io, with the changelog section as its notes and
provenance on the archives.

The token is a secret of the crates-io environment, which only a v*
tag can deploy to, and sits in the environment of one step. CI gains a
package job running the same dry run on every pull request.

The rustdoc model keeps calling the engine crate zu, so model.json does
not move. The semver gate skips a baseline that has no zudb package,
which is only this change.

* Name the CLI's arrow feature by the package it is now

A feature on the command line is spelled package/feature, and the package is zudb-cli.

* Point the fuzz crate at the renamed packages

It is outside the workspace, so it names each package itself rather than through the workspace table.

* Bring the API model up to fixed_octets

#785 made zu1::props::fixed_octets public without regenerating docs/api/model.json, which is why api-model is red on main.
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