Skip to content

Add a canonical decimal width and import decimals at it - #10897

Open
connortsui20 wants to merge 5 commits into
developfrom
ct/decimal-width-owner
Open

connortsui20 wants to merge 5 commits into
developfrom
ct/decimal-width-owner

Conversation

@connortsui20

@connortsui20 connortsui20 commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Every decimal precision now has one canonical storage width, DecimalDType::canonical_values_type(), and the Arrow and DuckDB imports produce arrays at that width. Before this, the Arrow import kept whatever width Arrow used (a Decimal128 column at precision 10 arrived as i128), and the DuckDB import narrowed precision 1 and 2 with a lossy as cast.

This doesn't make the canonical width an invariant yet. DecimalArray still accepts any storage width, and every kernel, writer and exporter already handles mixed widths, so only the two imports change behavior. Enforcing the width in try_new comes later in #10701, once DecimalByteParts decoding, the read path for old files and the compressor stop producing other widths.

Changes

canonical_values_type() replaces DecimalType::smallest_decimal_value_type with the same precision-to-width table, so the migrated call sites behave as before. Two shared helpers in vortex_array::arrays::decimal do the copies that the imports need now and that the later steps reuse. copy_to_canonical_width copies values into the canonical width, and a narrowing copy fails on a valid value outside the precision and writes zeros in null slots. check_precision checks values in place.

The Arrow import stays zero-copy when the Arrow width already matches and copies otherwise. Either way, every valid value is checked against the precision, so a value outside it now fails at import instead of reaching scalar_at or min_max, which can panic on it (#10749). The error points at arrow-rs PrimitiveArray::null_if_overflow_precision for callers that would rather get nulls. The dtype is built with DecimalDType::try_new, so an invalid precision or scale from FFI is an error rather than a panic. The DuckDB import goes through the same helpers.

This overlaps with #10305, which touches many of the same files. For review, read the commits one at a time.

API Changes

DecimalType::smallest_decimal_value_type is removed in favor of DecimalDType::canonical_values_type(). Arrow and DuckDB decimal imports return arrays at the canonical width and fail on a valid value outside the declared precision.

@connortsui20 connortsui20 added the changelog/break A breaking API change label Oct 27, 2026
@connortsui20 connortsui20 changed the title Give each decimal precision one canonical width, and check decimal imports Add a canonical decimal width and import decimals at it Oct 27, 2026
@github-actions
github-actions Bot temporarily deployed to docs-preview/pr-10897 October 27, 2026 19:26 Inactive
@codspeed

codspeed Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 14.94%

3 warnings

⚠️ Walltime results may be unreliable

Some benchmarks used the Walltime instrument outside CodSpeed Macro Runners, which can make the results inconsistent.

Learn more about Macro Runners

See affected benchmarks

⚠️ 3 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

⚡ 5 improved benchmarks
❌ 11 regressed benchmarks
✅ 2192 untouched benchmarks
⏩ 366 skipped benchmarks1
🗄️ 73 archived benchmarks run2

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ CPU Simulation filter_powerlaw_by_random[10000] 71.7 ns 812.7 ns -91.18%
❌ CPU Simulation test_repeated_scan_scalar_at[int_str] 336.2 µs 412.1 µs -18.41%
❌ CPU Simulation bench_compare_varbin[(50000, 2)] 134.5 µs 164.8 µs -18.38%
❌ CPU Simulation bench_compare_varbin[(50000, 64)] 136.2 µs 166.4 µs -18.14%
❌ CPU Simulation bench_compare_sliced_dict_varbinview[(7500, 10000)] 195.6 µs 230.2 µs -15.02%
❌ CPU Simulation bench_compare_sliced_dict_varbinview[(1000, 10000)] 81.2 µs 92.5 µs -12.22%
❌ CPU Simulation bench_compare_sliced_dict_varbinview[(10000, 10000)] 207.9 µs 236.3 µs -12.02%
❌ Walltime add_constant_shapes_neon[(32768, PerRowConstant)] 14.4 µs 16.1 µs -10.92%
❌ Walltime add_constant_shapes_neon[(32768, PerRowNullableConstant)] 14.9 µs 16.7 µs -10.65%
❌ Walltime add_constant_shapes_neon[(32768, ConstantPerRow)] 14.4 µs 16.1 µs -10.48%
❌ Walltime starts_with_avx512[8, VarBinView] 68.4 µs 76.3 µs -10.34%
⚡ Walltime mul_u64_nonnull_neon 39.1 µs 20.9 µs +87.38%
⚡ CPU Simulation missing_stats[16] 88.2 µs 71.6 µs +23.12%
⚡ CPU Simulation encode_primitives[u8, (2000, 4)] 164.3 µs 135.6 µs +21.19%
⚡ Walltime starts_with_avx512[8, VarBin] 46.1 µs 39.4 µs +17.08%
⚡ Walltime starts_with_avx512[64, VarBin] 60.6 µs 53.2 µs +13.87%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing ct/decimal-width-owner (9836fef) with develop (487bdba)

Open in CodSpeed

Footnotes

  1. 366 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. 73 benchmarks were run, but are now archived. If they were deleted in another branch, consider rebasing to remove them from the report. Instead if they were added back, click here to restore them. ↩

The canonical width of a decimal values buffer depends only on the
precision. Add `DecimalDType::canonical_values_type()` as the one owner
of that mapping, so that later changes can ask the dtype for its width
instead of a free function on `DecimalType`.

Test every precision boundary between two widths.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
Migrate every call of `DecimalType::smallest_decimal_value_type` to
`DecimalDType::canonical_values_type()` with the same dtype, and remove
the old function. Arithmetic keeps its work dtype and casts keep their
target dtype. `is_compatible_decimal_value_type` now compares against
the canonical width.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
Imports and decoders need one shared way to move decimal values to the
canonical width of their dtype. copy_to_canonical_width copies values into
DecimalDType::canonical_values_type(). A narrowing copy checks every valid
value against the target precision, not only the width, and writes zeros
into null slots. A widening or same-width copy checks nothing. The copy
never rescales.

check_precision fails on a valid value outside the precision and ignores
null slots. It runs a branchless fold first and looks for the failing row
only when the fold finds a value outside the precision.

Both functions are public through vortex_array::arrays::decimal. The
try_downcast macro drops its local BigCast import, because the module now
imports it.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
The Arrow decimal import kept the Arrow width, so a Decimal128 array of
precision 5 became an i128 Vortex array, and no import checked that the
values fit the precision. One private function now serves all four
from_arrow_decimal* functions. When the Arrow width is the canonical
width of the precision, it keeps the Arrow buffer and runs
check_precision. Otherwise it copies with copy_to_canonical_width, which
writes zeros into null slots when it narrows. The dtype now comes from
DecimalDType::try_new, so an invalid precision or scale from FFI returns
an error instead of a panic. The error names arrow-rs
PrimitiveArray::null_if_overflow_precision, which turns values outside
the precision into nulls.

The expected arrays of the Decimal128 and Decimal256 conversion tests
now use the canonical widths (i64 for precision 10, i128 for precision
38), and the tests assert the width.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
The DuckDB decimal import narrowed i16 storage into i8 for precision 1
to 2 with a lossy `as` cast, so a value outside the precision, or garbage
in a null slot, wrapped silently. It also checked no value against the
precision. The i16, i32 and i64 widths now copy through
copy_to_canonical_width, which checks a narrowing copy and writes zeros
into the null slots. The same-width copies and the i128 buffer are
checked with check_precision. The array is then built with the unchecked
constructor.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>

This branch was successfully deployed

1 active deployment
docs-preview/pr-10897 — 9836fef3 Deployed Oct 27, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/break A breaking API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant