Repository navigation
Return an empty array from fill_null on an empty input - #10627
Conversation
Filling nulls on a chunked array slices the constant fill to each chunk, and slicing to an empty range returns an empty canonical array rather than a constant. Executing fill_null on an empty chunk then failed with "fill_null fill_value must be a constant/scalar", which the array_ops fuzzer hit by filling a chunked array with empty chunks more than once. An empty input has no nulls to fill, so return an empty array of the result dtype before asking for a scalar fill. Closes vortex-data#10626 Signed-off-by: Vincent <1729065730@qq.com>
| let fill_value = args.get(1)?; | ||
| // An empty input has no nulls to fill. Slicing a constant fill to an empty range yields | ||
| // an empty canonical array rather than a constant, so return before requiring a scalar. | ||
| if input.is_empty() { |
There was a problem hiding this comment.
I think you still need to run the checks on run 143-150 - we should move them to the top
There was a problem hiding this comment.
Moved the constant and non-null fill checks to the top in 25d87fe. The empty-input return now only runs when the fill isn't a constant, which is the empty slice of a constant fill that chunked fill_null hands to an empty chunk, so an empty input with a null constant fill errors again. (AI-assisted reply.)
robert3005
left a comment
There was a problem hiding this comment.
I think we can simplify this
| #[test] | ||
| fn empty_input_ignores_non_constant_fill() -> VortexResult<()> { | ||
| let mut ctx = array_session().create_execution_ctx(); | ||
| let input = PrimitiveArray::from_option_iter(Vec::<Option<i32>>::new()).into_array(); | ||
| let fill = PrimitiveArray::from_iter(Vec::<i32>::new()).into_array(); | ||
|
|
||
| let result = FillNull::try_new(input, fill)? | ||
| .into_array() | ||
| .execute::<ArrayRef>(&mut ctx)?; | ||
|
|
||
| assert_eq!( | ||
| result.dtype(), | ||
| &DType::Primitive(PType::I32, Nullability::NonNullable) | ||
| ); | ||
| assert!(result.is_empty()); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Filling a chunked array slices the constant fill per chunk, and an empty slice is a | ||
| /// canonical array rather than a constant. Stacked fills over an empty chunk must still run. | ||
| #[test] | ||
| fn repeated_fill_over_empty_chunk() -> VortexResult<()> { | ||
| let mut ctx = array_session().create_execution_ctx(); | ||
| let input = ChunkedArray::try_new( | ||
| vec![ | ||
| PrimitiveArray::from_option_iter(Vec::<Option<u16>>::new()).into_array(), | ||
| PrimitiveArray::from_option_iter([Some(7u16), None]).into_array(), | ||
| ], | ||
| DType::Primitive(PType::U16, Nullability::Nullable), | ||
| )? | ||
| .into_array(); | ||
|
|
||
| let result = input | ||
| .fill_null(Scalar::primitive(1u16, Nullability::Nullable))? | ||
| .fill_null(Scalar::primitive(2u16, Nullability::NonNullable))?; | ||
|
|
||
| assert_arrays_eq!(result, PrimitiveArray::from_iter([7u16, 1]), &mut ctx); | ||
| Ok(()) | ||
| } | ||
|
|
There was a problem hiding this comment.
do we need both of the tests?
There was a problem hiding this comment.
No, I dropped empty_input_ignores_non_constant_fill and kept repeated_fill_over_empty_chunk, which is the case the fuzzer hit. (AI-assisted reply.)
Run the constant and non-null fill checks first, and only return an empty array for an empty input when the fill is not a constant (an empty slice of a constant fill). Drop empty_input_ignores_non_constant_fill, since repeated_fill_over_empty_chunk covers the case the fuzzer hit. Signed-off-by: Vincent <1729065730@qq.com>
Merging this PR will degrade performance by 9.61%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | WallTime | filter_neon[8] |
918 ns | 1,304 ns | -29.6% |
| ❌ | WallTime | filter_neon[64] |
1.8 µs | 2.2 µs | -18.26% |
| ❌ | WallTime | filter_neon[80] |
2.5 µs | 3 µs | -15.29% |
| ❌ ℹ️ | Simulation | test_scan_filter_to_arrow_array[int_float] |
3.5 ms | 4.1 ms | -13.75% |
| ❌ ℹ️ | Simulation | test_repeated_scan_filter[int_float] |
3.4 ms | 3.9 ms | -11.03% |
| ❌ ℹ️ | Simulation | test_repeated_scan_filter[int] |
2.4 ms | 2.7 ms | -10.35% |
| ⚡ | WallTime | starts_with_avx512[8, VarBin] |
46.1 µs | 39.2 µs | +17.4% |
| ⚡ ℹ️ | Simulation | test_scan_scalar_at[int] |
888.6 µs | 784.9 µs | +13.22% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ump45nose:fix/fill-null-empty-input (25d87fe) with develop (c48457b)
Footnotes
-
12 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. ↩
Summary
Closes #10626.
The
array_opsfuzzer crashed withfill_null fill_value must be a constant/scalar. The crash input is a nullableu16chunked array whose first two chunks are empty, filled with nulls more than once.When a
fill_nulllands on a chunked input, the constant fill is sliced to each chunk.ArrayRef::slicereturnsCanonical::emptyfor an empty range, so an empty chunk ends up asfill_null(empty chunk, empty PrimitiveArray). Executing that node tries to turn the fill into a constant, gets the empty primitive array back, and fails. An empty input has no nulls to fill, so the fill value doesn't matter there.Changes
FillNull::executenow returns an empty canonical array of the result dtype (the input dtype with the fill value's nullability) when the input is empty, before it asks for a scalar fill. Compare and the boolean operators already return early for empty inputs in the same way.empty_input_ignores_non_constant_fillrunsFillNullon an empty input with an empty, non-constant fill and checks the result is empty and non-nullable.repeated_fill_over_empty_chunkbuilds a chunked array with an empty chunk, fills nulls twice and checks the values. This is the shape the fuzzer found.Both new tests fail on
developwith the error from the issue and pass with this change. I also replayed the crash file from the issue artifact (crash-63a1effbe22f12235e8f9728f3d22005069f43e6) throughrun_fuzz_actionin a temporary test. It errored ondevelopand returnsOkwith this change.cargo test -p vortex-array --libandcargo clippy -p vortex-array --all-targets --all-features -- -D warningspass locally.AI assistance
This PR was written by an AI coding agent working under my account (the code, the tests and this description), and I'm the responsible human for it. One thing for reviewers to check: I put the guard in
FillNull::executerather than changingsliceto keep empty constants constant, since other code may rely on empty slices being canonical.