Skip to content

refactor: deduplicate spark_ceil and spark_floor - #7534

Open
FreakingMind wants to merge 7 commits into
apache:mainfrom
FreakingMind:ceil-decimal-boundary-tests
Open

FreakingMind wants to merge 7 commits into
apache:mainfrom
FreakingMind:ceil-decimal-boundary-tests

Conversation

@FreakingMind

@FreakingMind FreakingMind commented Nov 6, 2026 •

Copy link
Copy Markdown

Which issue does this PR close?

Closes #6974.

Rationale for this change

ceil.rs and floor.rs were the same code with ceil and floor swapped. The tests had already drifted: floor tested the switch from i64 to i128 and scale 19, ceil didn't.

What changes are included in this PR?

  1. Adds floor's two decimal tests to ceil. The expected values are different because ceil rounds the other way: 9223372036854.775807 becomes 9223372036855 instead of 9223372036854.
  2. Replaces ceil.rs and floor.rs with one module, math_funcs/ceil_floor.rs, which exports spark_ceil and spark_floor. The direction is a const CEIL: bool parameter, so each direction is still monomorphized into the loop. dispatch_pow10! moved there too, since only ceil and floor use it. utils.rs keeps only the decimal helpers shared with div, round and make_decimal.
  3. Merges the two test modules into one, where each test checks both functions. The input values are the same, and 22 tests become 11. The decimal tests now pass the result precision Spark plans, (33, 0) for a (38, 6) input and (20, 0) for (38, 19).
  4. Adds a bigint column and DECIMAL(38, 6) and DECIMAL(38, 19) columns to ceil.sql and floor.sql, with values on both sides of i64::MAX and i64::MIN, close to the maximum precision, and at scale 19, so these paths are now compared with Spark.

This removes the #[macro_export] macro downcast_compute_op. Nothing in the repo uses it after this change, but it was exported from the published datafusion-comet-spark-expr crate, so anyone calling it from outside will need to inline it.

How are these changes tested?

To check that the tests catch real bugs, I temporarily swapped the rounding direction, broke the i64 fast path and the wide path one at a time, and made the scale 19 fallback ignore the direction. The Rust tests failed each time, and ceil.sql also failed when the wide path rounded the wrong way.

Benchmarks compared to the code before the refactor:

Bench Change
ceil_decimal_18_4 -0.1%
ceil_decimal_38_6 -0.4%
spark_floor decimal128(38,2) +2.0%
spark_floor decimal128(38,18) -5.2%
spark_floor decimal128(38,6) wide values -2.2%

Switching from the marker types to the const bool made no measurable difference: running the two builds alternately, the decimal benches were within 2% of each other, which is about the run-to-run noise on my machine.

@github-actions github-actions Bot added enhancement New feature or request test Testing related area:expressions Expression evaluation labels Nov 6, 2026
@FreakingMind FreakingMind changed the title test: add floor's decimal boundary tests to ceil refactor: deduplicate spark_ceil and spark_floor Nov 6, 2026

@sam-1112 sam-1112 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Could we add run-spark-4.1-tests before queueing? Spark's SQL suites were skipped in the current fork run.

@FreakingMind
FreakingMind force-pushed the ceil-decimal-boundary-tests branch 2 times, most recently from d406171 to b02254d Compare November 9, 2026 12:46
@FreakingMind
FreakingMind force-pushed the ceil-decimal-boundary-tests branch from b02254d to cab6ac9 Compare November 9, 2026 13:00
@FreakingMind

Copy link
Copy Markdown
Author

Thanks for the review! Done, all tests passed

@comphead comphead left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I compared the generic kernel with the old ceil.rs and floor.rs path by path and found no behavior change, and all 22 original test cases are still there. The PR also removes the #[macro_export] macro downcast_compute_op from the crate root. Nothing in native/ uses it any more, but the crate is published, so that is a public API removal worth a line in the description.

(*precision, *scale)
}

/// The rounding direction that distinguishes `ceil` from `floor`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utils.rs holds the decimal helpers that div.rs, round.rs, make_decimal.rs and wide_decimal_binary_expr.rs share. The trait, the markers, the kernel and its tests (about 480 lines) are used only by ceil and floor, and so is dispatch_pow10!. Since ceil.rs and floor.rs are now thin wrappers, could the shared code live in one ceil_floor.rs module that exports spark_ceil and spark_floor, and keep utils.rs for the cross-function helpers?

@FreakingMind FreakingMind Nov 11, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed, dispatch_pow10! and the tests are in ceil_floor.rs now, and utils.rs only differs from main by the removed macro

Comment on lines +151 to +157
pub(crate) trait RoundingMode {
const NAME: &'static str;

fn round_f32(x: f32) -> f32;
fn round_f64(x: f64) -> f64;
fn div_i64(x: i64, div: i64) -> i64;
fn div_i128(x: i128, div: i128) -> i128;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

div_ceil and div_floor from num::integer are already generic over Integer, and f32/f64 both have ceil/floor through num::Float. So div_i64/div_i128 and round_f32/round_f64 could each be one generic method, which halves the trait and both impls. A const CEIL: bool parameter would also fold away at compile time and remove the marker types. I have not compiled either variant.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, done with const CEIL: bool, I ran the old and new builds alternately and the benches are almost the same

Comment on lines +502 to +541
#[test]
fn test_decimal128_i64_boundary_array() -> Result<()> {
// decimal_round_pow10 switches from the i64 fast path to decimal_round_wide exactly at
// i64::MAX / i64::MIN. Straddling both edges pins that the two paths agree: each pair
// below differs by one unscaled unit but rounds to the same value, so a divergence
// between the 64-bit and 128-bit divisions would show up as a mismatch here.
let input = [
Some(i64::MAX as i128), // 9223372036854.775807 -> fast path
Some(i64::MAX as i128 + 1), // 9223372036854.775808 -> wide path
Some(i64::MIN as i128), // -9223372036854.775808 -> fast path
Some(i64::MIN as i128 - 1), // -9223372036854.775809 -> wide path
];
check_decimal_array(
spark_ceil,
&input,
(38, 6),
38,
vec![
Some(9_223_372_036_855_i128),
Some(9_223_372_036_855_i128),
Some(-9_223_372_036_854_i128),
Some(-9_223_372_036_854_i128),
],
)?;
check_decimal_array(
spark_floor,
&input,
(38, 6),
38,
vec![
Some(9_223_372_036_854_i128),
Some(9_223_372_036_854_i128),
Some(-9_223_372_036_855_i128),
Some(-9_223_372_036_855_i128),
],
)
}

#[test]
fn test_decimal128_large_scale_array() -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ceil.sql and floor.sql only cover FLOAT, DOUBLE and DECIMAL(5, 2), so a BIGINT column and a wide decimal column (DECIMAL(38, 6), DECIMAL(38, 19)) with values past i64::MAX are never compared with Spark. These Rust tests also pass Decimal128(38, 0) as the output type for a (38, 6) input, where Spark plans (33, 0) (p - s + 1). Could a few such rows be added to the two SQL files, read from a table so constant folding does not hide the native path? The i64 boundary and scale 19 cases could then be checked there against Spark too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the precisions to (33, 0) and (20, 0), and added bigint, DECIMAL(38, 6) and DECIMAL(38, 19) rows to both files, read from a table. I also checked that ceil.sql fails if the wide path rounds the wrong way

utils.rs goes back to the decimal helpers that several math functions
share. The rounding kernel, dispatch_pow10! and the tests are only used
by ceil and floor, so they now live in one module that exports both.
num's div_ceil and div_floor are already generic over Integer, and
Float covers ceil and floor for f32 and f64, so two generic helpers
replace the four trait methods. A const CEIL parameter still folds the
direction away at compile time, which removes the trait and the Ceil
and Floor marker types.
Spark plans ceil and floor of DECIMAL(p, s) as DECIMAL(p - s + 1, 0), so
a (38, 6) input produces (33, 0) and a (38, 19) input produces (20, 0).
The tests passed 38 for both.
The SQL tests only covered float, double and DECIMAL(5, 2), so the i64
boundary, the 128-bit path and the scale 19 fallback were never compared
with Spark. The new rows are read from a table so that the native kernel
runs on them.
@FreakingMind

Copy link
Copy Markdown
Author

I compared the generic kernel with the old ceil.rs and floor.rs path by path and found no behavior change, and all 22 original test cases are still there. The PR also removes the #[macro_export] macro downcast_compute_op from the crate root. Nothing in native/ uses it any more, but the crate is published, so that is a public API removal worth a line in the description.

Thanks for checking. Added a line about downcast_compute_op to the description

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:expressions Expression evaluation enhancement New feature or request test Testing related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deduplicate spark_ceil and spark_floor and port floor's decimal boundary tests to ceil

3 participants