Skip to content

Conversation

@liamzwbao
Copy link
Contributor

@liamzwbao liamzwbao commented Oct 22, 2025

Which issue does this PR close?

Rationale for this change

We currently have two separate code paths that both handle decimal casting between different (precision, scale) pairs. Without unifying the logic, a fix in one place often needs to be duplicated in the other (e.g., #8579 fixed the arrow-cast
and #8552 fixed the parquet-variant-compute), which can easily lead to divergence when contributors lack full context. This PR consolidates the decimal rescale logic for both arrow-cast and parquet-variant-compute.

What changes are included in this PR?

  1. Extract the shared array-unary logic from convert_to_smaller_scale_decimal and convert_to_bigger_or_equal_scale_decimal into apply_decimal_cast
  2. Move the rescale-closure creation into make_upscaler and make_downscaler so that they can be used in parquet-compute-variant
  3. rework rescale_decimal in parquet-compute-variant to use the new make_upscaler and make_downscaler utilities.

One challenge is incorporating the large-scale reduction path (aka the delta_scale cannot fit into I::MAX_PRECISION) into make_downscaler without hurting performance. Returning 0 directly is usually cheaper than applying a unary operation to return zero. Therefore, make_downscaler may return None, and it is the caller’s responsibility to handle this case appropriately based on the documented behavior.

Are these changes tested?

Covered by existing tests

Are there any user-facing changes?

No

@github-actions github-actions bot added arrow Changes to the arrow crate parquet-variant parquet-variant* crates labels Oct 22, 2025
Copy link
Contributor Author

@liamzwbao liamzwbao left a comment

Choose a reason for hiding this comment

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

This is ready for review. Please take a look when you get chance, @scovich @alamb.

Also, this PR may need a regression test for parse_decimal in arrow-cast. Thanks!

}

pub(crate) fn cast_decimal_to_decimal_error<I, O>(
fn cast_decimal_to_decimal_error<I, O>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

downgrade the visibility since it's only used in this file

let is_infallible_cast = (input_precision as i8) + delta_scale <= (output_precision as i8);
let f_infallible = is_infallible_cast
.then_some(move |x| O::Native::from_decimal(x).unwrap().mul_wrapping(mul));
Some((f, f_infallible))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chose to return f_infallible instead of is_infallible_cast because, unlike make_downscaler, we cannot derive an infallible closure from f. So to keep the interface consistent, I applied the same approach to make_downscaler to return (f, f_infallible) as well.

@liamzwbao liamzwbao marked this pull request as ready for review October 22, 2025 23:38
@liamzwbao
Copy link
Contributor Author

Benchmarked locally on an M3 Max Mac, and the results show no performance improvement or regression against the main branch.

command used:

git checkout a7572eb6
cargo bench -p arrow-cast --bench parse_decimal -- --save-baseline main

git checkout issue-8670-decimal-cast-refactor
cargo bench -p arrow-cast --bench parse_decimal -- --baseline main

@alamb
Copy link
Contributor

alamb commented Oct 23, 2025

🤖 ./gh_compare_arrow.sh Benchmark Script Running
Linux aal-dev 6.14.0-1017-gcp #18~24.04.1-Ubuntu SMP Tue Sep 23 17:51:44 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Comparing issue-8670-decimal-cast-refactor (e32483c) to a7572eb diff
BENCH_NAME=parse_decimal
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental --bench parse_decimal
BENCH_FILTER=
BENCH_BRANCH_NAME=issue-8670-decimal-cast-refactor
Results will be posted here when complete

@alamb
Copy link
Contributor

alamb commented Oct 23, 2025

🤖: Benchmark completed

Details

group                      issue-8670-decimal-cast-refactor       main
-----                      --------------------------------       ----
-.123                      1.00     20.8±0.06ns        ? ?/sec    1.00     20.8±0.12ns        ? ?/sec
-00.1                      1.00     31.5±0.05ns        ? ?/sec    1.00     31.5±0.22ns        ? ?/sec
-12.                       1.00     36.5±0.04ns        ? ?/sec    1.00     36.5±0.07ns        ? ?/sec
-123                       1.00     36.2±0.09ns        ? ?/sec    1.00     36.2±0.06ns        ? ?/sec
-123.                      1.00     38.9±0.05ns        ? ?/sec    1.00     38.9±0.10ns        ? ?/sec
-123.1                     1.00     37.6±0.07ns        ? ?/sec    1.00     37.6±0.06ns        ? ?/sec
-123.123                   1.00     28.8±0.06ns        ? ?/sec    1.00     28.8±0.04ns        ? ?/sec
-123.1234                  1.00     30.2±0.21ns        ? ?/sec    1.00     30.2±0.04ns        ? ?/sec
-12345678912345678.1234    1.00     67.7±0.11ns        ? ?/sec    1.00     67.7±0.33ns        ? ?/sec
-99999999999999999.999     1.00     66.1±0.14ns        ? ?/sec    1.00     66.1±0.36ns        ? ?/sec
.123                       1.00     19.7±0.02ns        ? ?/sec    1.00     19.7±0.11ns        ? ?/sec
0.0000123                  1.00     24.7±0.05ns        ? ?/sec    1.00     24.7±0.02ns        ? ?/sec
00.1                       1.00     30.4±0.04ns        ? ?/sec    1.00     30.4±0.03ns        ? ?/sec
12.                        1.00     35.2±0.16ns        ? ?/sec    1.00     35.1±0.06ns        ? ?/sec
123                        1.00     34.9±0.05ns        ? ?/sec    1.00     34.9±0.05ns        ? ?/sec
123.                       1.00     37.8±0.03ns        ? ?/sec    1.00     37.8±0.26ns        ? ?/sec
123.1                      1.00     36.5±0.04ns        ? ?/sec    1.00     36.5±0.04ns        ? ?/sec
123.123                    1.00     27.4±0.06ns        ? ?/sec    1.00     27.4±0.03ns        ? ?/sec
123.1234                   1.00     29.0±0.05ns        ? ?/sec    1.00     29.0±0.14ns        ? ?/sec
12345678912345678.1234     1.00     65.9±0.23ns        ? ?/sec    1.00     65.9±0.39ns        ? ?/sec
99999999999999999.999      1.00     64.2±0.14ns        ? ?/sec    1.00     64.3±0.40ns        ? ?/sec

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

Thank you @liamzwbao -- I ran benchmarks and agree with your assesment that this PR does not change the performance

However, I am somewhat concerned with the APIs that are proposed to make pub -- they implementations seem to rely on the caller correctly comparing / calling make_upscaler / make_downscaler which seems error prone to me (and the code does not really validate any of the inputs)

What if you left make_upscaler / make_downscaler private, and instead added a single new API like this, which validated the input and then called make_upscaler / make_downscaler appropriately?

pub fn make_scaler<I: DecimalType, O: DecimalType>(
    input_precision: u8,
    input_scale: i8,
    output_precision: u8,
    output_scale: i8,
) -> Result<Option<(
    impl Fn(I::Native) -> Option<O::Native>,
    Option<impl Fn(I::Native) -> O::Native>,
)>>
{
}

// then an increase of scale by 3 will have the following effect on the representation:
// [xxxxx] -> [xxxxx000], so for the cast to be infallible, the output type
// needs to provide at least 8 digits precision
let is_infallible_cast = (input_precision as i8) + delta_scale <= (output_precision as i8);
Copy link
Contributor

Choose a reason for hiding this comment

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

I see the old code did this too, but it seems like the cast as i8 could porentially convert a number larger than 128 to a negative number : -- maybe that is ok

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this becomes an issue once we expose the API. Normally it isn’t a concern, since the precision is guaranteed to stay under 128. Let me redesign the public interface

@liamzwbao liamzwbao force-pushed the issue-8670-decimal-cast-refactor branch from 4fe7d19 to 252eee7 Compare October 24, 2025 01:53
Copy link
Contributor Author

@liamzwbao liamzwbao left a comment

Choose a reason for hiding this comment

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

Hi @alamb, I attempted to unify the API, but the return types of make_upscaler and make_downscaler are closures with incompatible types unless we box them. Because of that, I decided to move rescale_decimal from parquet-variant-compute into arrow-cast and expose it for use in the variant conversion.

I’ll add some tests for the new rescale_decimal API next. WDYT?

@liamzwbao liamzwbao force-pushed the issue-8670-decimal-cast-refactor branch from 252eee7 to 49e72cd Compare October 24, 2025 02:01
@alamb
Copy link
Contributor

alamb commented Oct 25, 2025

🤖 ./gh_compare_arrow.sh Benchmark Script Running
Linux aal-dev 6.14.0-1017-gcp #18~24.04.1-Ubuntu SMP Tue Sep 23 17:51:44 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
Comparing issue-8670-decimal-cast-refactor (2a73ffb) to a7572eb diff
BENCH_NAME=parse_decimal
BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental --bench parse_decimal
BENCH_FILTER=
BENCH_BRANCH_NAME=issue-8670-decimal-cast-refactor
Results will be posted here when complete

Copy link
Contributor

@alamb alamb left a comment

Choose a reason for hiding this comment

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

Thank you @liamzwbao

I kicked off some benchmarks to verify that the performance is the same, but I expect that it will be

I::Native: DecimalCast + ArrowNativeTypeOp,
O::Native: DecimalCast + ArrowNativeTypeOp,
{
let array = if let Some(f_infallible) = f_infallible {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is a very nice formulation now

@alamb
Copy link
Contributor

alamb commented Oct 25, 2025

🤖: Benchmark completed

Details

group                      issue-8670-decimal-cast-refactor       main
-----                      --------------------------------       ----
-.123                      1.00     20.7±0.02ns        ? ?/sec    1.00     20.8±0.32ns        ? ?/sec
-00.1                      1.00     31.4±0.11ns        ? ?/sec    1.00     31.4±0.03ns        ? ?/sec
-12.                       1.00     36.5±0.07ns        ? ?/sec    1.00     36.5±0.04ns        ? ?/sec
-123                       1.00     36.2±0.10ns        ? ?/sec    1.00     36.3±0.33ns        ? ?/sec
-123.                      1.00     39.0±0.36ns        ? ?/sec    1.00     39.0±0.07ns        ? ?/sec
-123.1                     1.00     37.6±0.07ns        ? ?/sec    1.00     37.6±0.05ns        ? ?/sec
-123.123                   1.00     28.7±0.04ns        ? ?/sec    1.00     28.8±0.22ns        ? ?/sec
-123.1234                  1.00     30.2±0.07ns        ? ?/sec    1.00     30.2±0.21ns        ? ?/sec
-12345678912345678.1234    1.00     74.8±0.15ns        ? ?/sec    1.00     74.8±0.20ns        ? ?/sec
-99999999999999999.999     1.00     72.4±0.12ns        ? ?/sec    1.00     72.5±0.62ns        ? ?/sec
.123                       1.00     19.7±0.04ns        ? ?/sec    1.00     19.7±0.05ns        ? ?/sec
0.0000123                  1.00     24.8±0.14ns        ? ?/sec    1.00     24.8±0.07ns        ? ?/sec
00.1                       1.00     30.4±0.07ns        ? ?/sec    1.00     30.4±0.05ns        ? ?/sec
12.                        1.00     35.4±0.13ns        ? ?/sec    1.00     35.4±0.12ns        ? ?/sec
123                        1.00     35.0±0.16ns        ? ?/sec    1.00     34.9±0.10ns        ? ?/sec
123.                       1.00     38.4±0.18ns        ? ?/sec    1.00     38.4±0.12ns        ? ?/sec
123.1                      1.00     36.7±0.20ns        ? ?/sec    1.00     36.6±0.07ns        ? ?/sec
123.123                    1.00     27.7±0.06ns        ? ?/sec    1.00     27.7±0.20ns        ? ?/sec
123.1234                   1.00     29.6±0.09ns        ? ?/sec    1.00     29.6±0.12ns        ? ?/sec
12345678912345678.1234     1.00     74.8±0.78ns        ? ?/sec    1.00     74.7±0.14ns        ? ?/sec
99999999999999999.999      1.00     72.5±0.10ns        ? ?/sec    1.00     72.4±0.10ns        ? ?/sec

@alamb alamb merged commit 7a92be5 into apache:main Oct 27, 2025
29 checks passed
@alamb
Copy link
Contributor

alamb commented Oct 27, 2025

Thanks again @liamzwbao and @scovich

@liamzwbao liamzwbao deleted the issue-8670-decimal-cast-refactor branch October 27, 2025 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arrow Changes to the arrow crate parquet-variant parquet-variant* crates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor arrow-cast decimal casting to unify the rescale logic used in Parquet variant casts

3 participants