Repository navigation
Conversation
DecimalArray::scalar_at and the decimal min_max aggregate built their results with Scalar::decimal, which panics when a stored value does not fit the precision of the dtype. DecimalArray construction does not check its values and file reads trust the stored precision, so such a value could make a read panic. Add Scalar::try_decimal and use it in both places so they return an error instead. Signed-off-by: Vincent <1729065730@qq.com>
60db592 to
8cb9bd3
Compare
Merging this PR will degrade performance by 3.57%3 warnings, 1 note
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
connortsui20
left a comment
There was a problem hiding this comment.
Thanks for picking this up! The two changed call sites look correct. scalar_at only runs on valid rows (probe_scalar checks validity first), min_max filters out null slots, and checking only the min and max is enough because precision bounds are symmetric.
The panic from #10749 is still reachable through cached stats. min_max returns cached Stat::Min/Stat::Max before it reaches compute_min_max (min_max/mod.rs:90). Those stats are built by stat_scalar (stats/stats_set.rs:241), which calls Scalar::validate(..).vortex_expect(..). Decimal stats decoded from a file go through bytes_from_proto (scalar/proto.rs:406), which only checks the byte length, not the precision. So a file with a decimal(2, 0) column and a stored exact max of 100i8 still panics in min_max, and in any other stats consumer.
That points at the root cause: file reads trust the stored precision for both values and stats. Checking at that boundary (stats decode in StatsSet::from_flatbuffer, plus a value check in the decimal deserialize or the read plugin planned in #10701) would close the whole class instead of one call site at a time. If that's too much for this PR, it's fine to keep the scope here, but please say in the description that the stats path is still open and link a follow-up issue.
Smaller follow-ups that fit this PR:
DecimalByteParts::scalar_at(encodings/decimal-byte-parts/src/decimal_byte_parts/array.rs:400) builds the same fallible scalar by hand withScalar::try_new. It can callScalar::try_decimalso every fallible decimal scalar goes through one helper.- The fuzz reference in
fuzz/src/array/scalar_at.rs:51still usesScalar::decimal. On the input this PR fixes, the reference now panics where the implementation returns an error, so the fuzzer reports a harness crash instead of comparing the two. It can useScalar::try_decimal(..)?. vortex-python/src/scalar/factory.rsstill callsScalar::decimalon user input, sovortex.scalar(100, dtype=decimal(2, 0))raises aPanicExceptioninstead of aValueError. The FFI constructors already return an error for this case.
The inline comments cover the safety-contract question and the tests.
Generated by Claude Code
- Match the precision error message in the out-of-range tests, return VortexResult, and split the cases with rstest. Add a negative min case and a nullable case where the out-of-range value sits in a null slot. - Document on DecimalArray::new_unchecked that values are not required to fit the precision and are not checked on construction or file reads. - Use Scalar::try_decimal in DecimalByteParts::scalar_at, the fuzz scalar_at reference, and the Python scalar factory (ValueError instead of a panic). Signed-off-by: Vincent <1729065730@qq.com>
|
Thanks for the detailed review. Pushed 0ee4df9:
I couldn't build |
connortsui20
left a comment
There was a problem hiding this comment.
Thanks, this covers everything from the last round. The tests now reach the precision guard, the null-slot cases are covered, and the DecimalByteParts and Python changes look right (ValueError is a better fit than the default RuntimeError mapping for bad user input).
Two things left, both inline: the new contract docs on DecimalData::new_unchecked decide a question that's still open, and the fuzz claim in the description doesn't hold. The rest is a nit.
Keeping #10807 separate is the right call. It's one case of a broader question that Vortex hasn't settled yet: which parts of a file are validated on read and which are trusted (#10756 and the width decoding in #10755 are others). Whichever way that goes, bad data from a file should return an error and not panic, which is what this PR does for these call sites.
Generated by Claude Code
connortsui20
left a comment
There was a problem hiding this comment.
This is a style and structure pass on the full diff at 0ee4df9. The open threads from the last round still apply. Nothing here changes behavior.
array.rs: the# Safetysections onDecimalData::new_uncheckedandnew_unchecked_handlenow list only avalidityduty for a parameter neither function takes. They also leave out the length and alignment checks thatvalidateactually performs.operations.rs: the "stored values are not guaranteed to fit" comment is copied in two places, and it restates the contract owned byDecimalData::new_unchecked.min_max/decimal.rs: withtry_decimal,compute_min_maxreads better as a guard plus one constructor pair. The caller doesn't need itsOk(match .. ?)wrapper.- One nit on the
vortex_expectmessage.
The tests follow the patterns already in their modules, so I have no further comments on them.
Generated by Claude Code
Signed-off-by: Vincent <1729065730@qq.com>
8dd16d8 to
9389322
Compare
Summary
DecimalArray::scalar_atand the decimalmin_maxaggregate panicked when a stored value did not fit the precision of the dtype, because both built their results withScalar::decimal, which callsvortex_expecton the precision check. SinceDecimalArrayconstruction doesn't check its values and file reads trust the stored precision, a file holding such a value could make a read panic. With this change both calls return an error instead.Still open: the panic is still reachable through cached stats.
min_maxreturns a cachedStat::Min/Stat::Maxbefore it reaches the kernel, and decimal stats decoded from a file are not checked against the precision, so an out-of-range stored stat still panics instat_scalar. That needs a check at the read boundary and is tracked in #10807. The otherScalar::decimalcallers on compute paths (sum,mean,cast,fill_null,between, the DecimalByteParts kernels) are also left for follow-ups.Changes
Scalar::try_decimal, a fallible version ofScalar::decimal.Scalar::decimalnow calls it and still panics on a bad value, with the panic documented.DecimalArray::scalar_atusesScalar::try_decimaland returns the error.min_maxaggregate usesScalar::try_decimalfor the min and max and returns the error.DecimalByteParts::scalar_atusesScalar::try_decimalinstead of building the scalar by hand withScalar::try_new.scalar_atreference usesScalar::try_decimal(..)?to match the implementation. Its callers stillvortex_expectthe result, so the harness still panics on such an input, which is intended:random_scalaronly generates in-range decimals, so an out-of-range value there means an encoding produced bad data.vortex.scalar(100, dtype=decimal(2, 0))in the Python bindings now raisesValueErrorinstead ofPanicException.DecimalArray::new_unchecked/new_unchecked_handle/new_unchecked_from_byte_bufferno longer list "values fit the precision" as a caller invariant. They now say values are not checked on construction or file reads, and that code turning stored values into scalars must use a fallible path. This matches whattry_newactually checks.test_scalar_at_value_outside_precision_errorsandtest_decimal_value_outside_precision_errorsmatch on thedoes not fit in precisionmessage, returnVortexResult<()>, and use#[rstest]cases (one element, min/max, and a negative min of-100i8). New nullable tests put100i8in a null slot of adecimal(2, 0)array with[5, 100]and check thatmin_maxreturns min = max = 5 and thatscalar_aton the null index returns a null scalar.Tested with
cargo test -p vortex-array --lib(4433 passed, 1 ignored),cargo test -p vortex-decimal-byte-parts --lib(288 passed),cargo clippy -p vortex-array -p vortex-decimal-byte-parts --all-targets --all-features -- -D warnings, andrustfmt +nightly-2026-09-10 --checkon the changed files. I could not buildvortex-fuzzorvortex-pythonlocally becausecustom-labelsneeds libclang for bindgen on my machine, so those two small changes rely on CI.AI assistance: I used an AI coding assistant to help write this change and the description; I reviewed the code and ran the tests above myself.