feat: widen output decimal type for decimal ceil/floor - #24703
Conversation
|
@neilconway, @Jefffrey I'd appreciate your thoughts on this experimental PR The mentioned Spark UDF PR is #21933 - that logic could be unified into the core UDF later if we choose the Spark behaviour (1). |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #24703 +/- ##
==========================================
- Coverage 81.52% 81.52% -0.01%
==========================================
Files 1123 1123
Lines 405970 406086 +116
Branches 405970 406086 +116
==========================================
+ Hits 330978 331071 +93
- Misses 55627 55646 +19
- Partials 19365 19369 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
ill try find time to take a look at this, but cc @kumarUjjawal i think you worked on something similar for round? |
kumarUjjawal
left a comment
There was a problem hiding this comment.
Thank you @theirix for working on this. I have left few comments for your consideration.
| } | ||
|
|
||
| /// Compute the return precision for floor/ceil result to accommodate the result | ||
| pub(super) fn decimal_floor_ceil_precision( |
There was a problem hiding this comment.
can we reuse calculate_new_precision_scale with 0 decimal places?
There was a problem hiding this comment.
Unfortunately, no. Based on the code, it will provide the same precision as the input one if places = 0. We need to widen it.
| preimage_bounds!(decimal: Decimal128, Decimal128Type, *n, *precision, *scale) | ||
| ScalarValue::Decimal128(Some(n), lit_precision, lit_scale) => { | ||
| let DataType::Decimal128(arg_precision, arg_scale) = | ||
| info.get_data_type(&arg)? |
There was a problem hiding this comment.
we could match on the Ok case and return PreimageResult::None otherwise?
| DataType::Null => Ok(DataType::Float64), | ||
| other => Ok(other.clone()), | ||
| } | ||
| Ok(decimal_floor_ceil_return_type(&arg_types[0])) |
There was a problem hiding this comment.
This changes the output type of a public SQL function. Any query that stores floor(decimal_col) into a fixed schema, or reads arrow_typeof, sees a different type. We should mention in upgrade guide
There was a problem hiding this comment.
Thank you for the review! I've fixed edge cases for the preimage.
Regarding the change - I agree, even if it's a different decimal point type in the decimal domain, it is still a different type. Updated the user-facing changes PR section. If this approach is fine, I'll also add a change to the upgrading notes doc for 56.
| )?; | ||
|
|
||
| // Use rescale_decimal to compute "1" at the argument's scale (avoids manual pow) | ||
| let one_scaled: D::Native = rescale_decimal::<D, D>( |
There was a problem hiding this comment.
this might be already there before the pr but would a guard that returns None when one_scaled is zero be worth adding?
Jefffrey
left a comment
There was a problem hiding this comment.
ive skimmed this but makes sense to me; if we're going to enable this behaviour (of changing scale) we might as well go all the way like spark, unlike duckdb which doesnt tighten it as much as spark does
the clickhouse one seems surprising, is that a bug? 😅
adding a upgrade notice would be good 👍
Yes, this makes sense. I'll try to refactor Spark's UDF implementation to reuse more from the core.
It is usually complicated with ClickHouse - the behaviour is documented, but still contradictory. Turns out, all operations on the decimal type are done on a backing type (int32 if precision is 4, as in the example with ClickHouse is pretty relaxed on overflow checks (works only for 32- and 64-bit decimals but not for wider). Also, it is not universal - you can easily construct an overflown decimal even when it's enabled, so there is a function For example, select version() \G
version(): 26.3.17.4
SET decimal_check_overflow = 1;
SELECT floor(CAST('-99999999.9999', 'DECIMAL32(1)')), toTypeName(floor(CAST('-99999999.9999', 'DECIMAL32(1)')))
-100000000 │ Decimal(9, 1)
select isDecimalOverflow(floor(CAST('-99999999.9999', 'DECIMAL32(1)')));
1
SELECT CAST('-100000000', 'DECIMAL32(1)')
DB::Exception: Decimal value is too big
Done, thank you! |
Which issue does this PR close?
ceil,floorondecimalcan produce spurious overflow #22511FLOORdecimal returns unexpected error #20640Rationale for this change
In edge cases, an extra decimal point is required to accommodate a result. Currently, ceil/floor just fail with an overflow.
For example,
ceil(-999)is-1000, and it cannot fit intoDecimal(4,1)with 3 digits, but only into a decimal type with a lower scale and/or different precision (e.g.Decimal(4,0)).The proposed experimental fix is to widen the input decimal type to zero scale.
How it's done in other engines:
It matches Spark ceil/floor behaviour (limited support in the Spark UDF), when the scale drops to zero, precision is recalculated via
p-s+1.DuckDB performs slightly differently, just dropping the scale to zero and keeping precision as input:
select floor('-999.9'::DECIMAL(4,1));->-1000::DECIMAL(4,0)select ceil(9.9::DECIMAL(2,1));->10::DECIMAL(2,0)SELECT floor(CAST('-999.9', 'DECIMAL(4, 1)'))->-1000::DECIMAL(4,1)From three possible behaviours, we can go with either Spark's or DuckDB's behaviour.
I didn't investigate ClickHouse behaviour yet. Since we already have existing Spark logic in place and it uses precision sparingly, I lean towards it.
What changes are included in this PR?
Decimal(p-s+1, 0)- could be a breaking changeapply_decimal_opto specify output scale (could be different from input scale)Are these changes tested?
Are there any user-facing changes?
Changed
floorandceilUDF output type from the exact input type to a rescaled type with the same bit width. For example, for inputDecimal32(7,2)floor now returnsDecimal32(6,0)