Skip to content

ScalarUdfImpl::strictly_order_preserving: Allow expression to report whether they keep the same ordering of the input - #23807

Merged
rluvaton merged 11 commits into
apache:mainfrom
rluvaton:debug-why-sort-ordering-get-messed-up
Jul 27, 2026
Merged

ScalarUdfImpl::strictly_order_preserving: Allow expression to report whether they keep the same ordering of the input#23807
rluvaton merged 11 commits into
apache:mainfrom
rluvaton:debug-why-sort-ordering-get-messed-up

Conversation

@rluvaton

@rluvatonrluvaton commented Jul 22, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Related to:

Rationale for this change

To be able to keep the same sorting order allowing for more optimizations

Now comet own cast implementation can be recognized as not modifying sort order in the same cases that datafusion cast does.

What changes are included in this PR?

added strictly_order_preserving property to ExprProperties + varius other places
and replaced the hard coded logic for cast (substitute_cast_ordering) about keeping input order with more generic approach that now any expression can implement and have the same advantage of sort elimination

also marked from_unixtime as keeping ordering to show a case of this optimization

Are these changes tested?

yes

Are there any user-facing changes?

yes, breaking change, added strictly_order_preserving property to ExprProperties and to FFI_ExprProperties

this property means that given expression f and 2 values from the input column a and b the following variants are kept:

  1. a.cmp(b) == f(a).cmp(f(b))
  2. nulls maps to nulls

Example of satisfying expression:
cast(col_a as BIGINT) where col_a is INT it is keeping the properties

Example of not satisfying:

floor - floor can not

array_repeat(my_col, 2) which might look like at first glance as keeping the property as well but in fact it does not.

the reason is that array_repeat(null, 2) will output list of 2 nulls which breaks the 2nd property that nulls must be kept as nulls

how to migrate:

Option 1 - keeping the old behavior (safest but least performant)
set strictly_order_preserving to false

Option 2 - Using the new optimization that this opens up:
set strictly_order_preserving only if the expression keep both variants

⚠️ Also look for code that blindly copy child properties since it will copy the child strictly order preserving as well which will lead to wrong results

fnget_properties(&self,children:&[ExprProperties]) -> Result<ExprProperties>{// ⚠️ Clone here is BAD since you take the following 2 properties from your child that might not be true in your case:// - `preserves_lex_ordering`// - `strictly_order_preserving`// both of them should never be taken from the child as is since they talk about **this** expression Ok(children[0].clone())}

@github-actionsgithub-actionsBot added logical-expr Logical plan and expressions physical-expr Changes to the physical-expr crates core Core DataFusion crate functions Changes to functions implementation ffi Changes to the ffi crate labels Jul 22, 2026
@rluvatonrluvaton changed the title Debug why sort ordering get messed upAllow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b)) opening up for more optimizationJul 22, 2026
@rluvaton
rluvatonforce-pushed the debug-why-sort-ordering-get-messed-up branch from e0c40b1 to 7305547CompareJuly 22, 2026 15:13
@rluvatonrluvaton added the api change Changes the API exposed to users of the crate label Jul 22, 2026
@rluvaton
rluvaton marked this pull request as ready for review July 22, 2026 15:19
@github-actions

github-actionsBot commented Jul 22, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
 Cloning apache/main
Building datafusion v54.1.0 (current)
Built [ 110.793s] (current)
Parsing datafusion v54.1.0 (current)
Parsed [ 0.037s] (current)
Building datafusion v54.1.0 (baseline)
Built [ 109.080s] (baseline)
Parsing datafusion v54.1.0 (baseline)
Parsed [ 0.038s] (baseline)
Checking datafusion v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 0.907s] 223 checks: 223 pass, 30 skip
Summary no semver update required
Finished [ 222.448s] datafusion
Building datafusion-expr v54.1.0 (current)
Built [ 26.512s] (current)
Parsing datafusion-expr v54.1.0 (current)
Parsed [ 0.081s] (current)
Building datafusion-expr v54.1.0 (baseline)
Built [ 26.338s] (baseline)
Parsing datafusion-expr v54.1.0 (baseline)
Parsed [ 0.082s] (baseline)
Checking datafusion-expr v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 1.863s] 223 checks: 223 pass, 30 skip
Summary no semver update required
Finished [ 56.283s] datafusion-expr
Building datafusion-expr-common v54.1.0 (current)
Built [ 18.621s] (current)
Parsing datafusion-expr-common v54.1.0 (current)
Parsed [ 0.020s] (current)
Building datafusion-expr-common v54.1.0 (baseline)
Built [ 18.459s] (baseline)
Parsing datafusion-expr-common v54.1.0 (baseline)
Parsed [ 0.021s] (baseline)
Checking datafusion-expr-common v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 0.282s] 223 checks: 222 pass, 1 fail, 0 warn, 30 skip
--- failure constructible_struct_adds_field: externally-constructible struct adds field ---
Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/constructible_struct_adds_field.ron
Failed in:
field ExprProperties.strictly_order_preserving in /home/runner/work/datafusion/datafusion/datafusion/expr-common/src/sort_properties.rs:198
Summary semver requires new major version: 1 major and 0 minor checks failed
Finished [ 38.131s] datafusion-expr-common
Building datafusion-ffi v54.1.0 (current)
Built [ 59.438s] (current)
Parsing datafusion-ffi v54.1.0 (current)
Parsed [ 0.065s] (current)
Building datafusion-ffi v54.1.0 (baseline)
Built [ 60.755s] (baseline)
Parsing datafusion-ffi v54.1.0 (baseline)
Parsed [ 0.068s] (baseline)
Checking datafusion-ffi v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 0.330s] 223 checks: 223 pass, 30 skip
Summary no semver update required
Finished [ 122.024s] datafusion-ffi
Building datafusion-functions v54.1.0 (current)
Built [ 30.721s] (current)
Parsing datafusion-functions v54.1.0 (current)
Parsed [ 0.092s] (current)
Building datafusion-functions v54.1.0 (baseline)
Built [ 31.054s] (baseline)
Parsing datafusion-functions v54.1.0 (baseline)
Parsed [ 0.095s] (baseline)
Checking datafusion-functions v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 0.619s] 223 checks: 223 pass, 30 skip
Summary no semver update required
Finished [ 63.807s] datafusion-functions
Building datafusion-physical-expr v54.1.0 (current)
Built [ 29.108s] (current)
Parsing datafusion-physical-expr v54.1.0 (current)
Parsed [ 0.053s] (current)
Building datafusion-physical-expr v54.1.0 (baseline)
Built [ 29.378s] (baseline)
Parsing datafusion-physical-expr v54.1.0 (baseline)
Parsed [ 0.054s] (baseline)
Checking datafusion-physical-expr v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 0.486s] 223 checks: 223 pass, 30 skip
Summary no semver update required
Finished [ 59.929s] datafusion-physical-expr
Building datafusion-sqllogictest v54.1.0 (current)
Built [ 191.897s] (current)
Parsing datafusion-sqllogictest v54.1.0 (current)
Parsed [ 0.023s] (current)
Building datafusion-sqllogictest v54.1.0 (baseline)
Built [ 185.890s] (baseline)
Parsing datafusion-sqllogictest v54.1.0 (baseline)
Parsed [ 0.024s] (baseline)
Checking datafusion-sqllogictest v54.1.0 -> v54.1.0 (no change; assume patch)
Checked [ 0.120s] 223 checks: 223 pass, 30 skip
Summary no semver update required
Finished [ 380.933s] datafusion-sqllogictest

@github-actionsgithub-actionsBot added the auto detected api change Auto detected API change label Jul 22, 2026
@rluvaton

Copy link
Copy Markdown
MemberAuthor

@rluvatonrluvaton changed the title Allow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b)) opening up for more optimizationAllow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b))) opening up for more optimizationJul 22, 2026
if is_order_preserving_cast_family(&child.range.data_type(), target_type) {
Ok(child.clone().with_range(unbounded))
let source_type = child.range.data_type();
// A widening cast is additionally one-to-one, so it is strictly

@getChangetChanJul 22, 2026

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 may not have full context, so please feel free to ignore this if it's a misunderstanding.

CastExpr::check_bigger_cast includes Int32 -> Float32 and Int64 -> Float64. Due to float mantissa limits, large integers lose precision and can collapse to the same float value (e.g. 16_777_216_i32 as f32 == 16_777_217_i32 as f32), so they are not strictly 1-to-1

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

You are right, unfortunately this moved from here (so no logic change has been made):

&& CastExpr::check_bigger_cast(cast_expr.cast_type(), expr_type))

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.

filed. #23808

preserves_lex_ordering: value.preserves_lex_ordering,
// Not carried over the FFI boundary (it would break the ABI);
// stay conservative.
strictly_order_preserving: false,

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Should I make breaking change in FFI (by adding the field) while I'm here to avoid 2 breaking changes? I'm not really familiar with this module

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.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I've read .ai/skills/datafusion-ffi/SKILL.md (ironic) and saw that it is ok to make breaking change, just not release them in a patch version

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

so I added the field here

if is_order_preserving_cast_family(&child.range.data_type(), target_type) {
Ok(child.clone().with_range(unbounded))
let source_type = child.range.data_type();
// A widening cast is additionally one-to-one, so it is strictly

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

You are right, unfortunately this moved from here (so no logic change has been made):

&& CastExpr::check_bigger_cast(cast_expr.cast_type(), expr_type))

@codecov-commenter

codecov-commenter commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.59794% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.66%. Comparing base (7576762) to head (ad7d27b).
⚠️ Report is 2 commits behind head on main.

Files with missing linesPatch %Lines
...tafusion/physical-expr/src/equivalence/ordering.rs72.00%0 Missing and 7 partials ⚠️
...on/physical-expr/src/equivalence/properties/mod.rs84.84%0 Missing and 5 partials ⚠️
datafusion/physical-expr/src/scalar_function.rs50.00%0 Missing and 1 partial ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #23807 +/- ##
==========================================
- Coverage 80.66% 80.66% -0.01% 
==========================================
Files 1094 1095 +1 Lines 371671 372376 +705 Branches 371671 372376 +705 ==========================================
+ Hits 299817 300380 +563 - Misses 53958 54067 +109 - Partials 17896 17929 +33 

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mbutrovich
mbutrovich self-requested a review July 24, 2026 16:29

@alambalamb 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.

Thank you @rluvaton -- this idea here seems sound to me

Can we get some sql logic / explain test that show some of the optimizations that this allows for (that preserves_lex_ordering does not)?

I think having those examples would make sure we have a good / minimal API to build on -- without such "end visible facing" tests I fear we may implement some features that are not ever used

Ok(false)
}

/// Returns true if the function is strictly order-preserving with respect

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.

How is this related to the output_ordering method? I am wondering if we can extend that rather tan introduce a new method

https://docs.rs/datafusion/latest/datafusion/logical_expr/trait.ScalarUDFImpl.html#method.output_ordering

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

output_ordering does not tell you whether you can reuse the existing ordering when you have multiple order by keys (with how ties are handled)

Comment threaddatafusion/expr/src/udf.rs Outdated
///
/// This is not simply a stricter [`Self::preserves_lex_ordering`] — the
/// two properties also assume different input orderings, and with
/// multiple ordered inputs neither implies the other. Monotone but

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.

It would have helped me to move these examples up to the top of the docs

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

moved

preserves_lex_ordering: value.preserves_lex_ordering,
// Not carried over the FFI boundary (it would break the ABI);
// stay conservative.
strictly_order_preserving: false,

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.

@rluvaton

Copy link
Copy Markdown
MemberAuthor

Thank you @rluvaton -- this idea here seems sound to me

Can we get some sql logic / explain test that show some of the optimizations that this allows for (that preserves_lex_ordering does not)?

I think having those examples would make sure we have a good / minimal API to build on -- without such "end visible facing" tests I fear we may implement some features that are not ever used

I did not want to add uses of this api in this PR to reduce the amount of code to review, do you still want me to do so?

@rluvaton

rluvaton commented Jul 25, 2026

Copy link
Copy Markdown
MemberAuthor

Can we get some sql logic / explain test that show some of the optimizations that this allows for (that preserves_lex_ordering does not)?

Done, array_repeat is now no longer remove the ordering
actually array_repeat does not map null to null, changed to from_unixtime

…d-ordering
# Conflicts:
#	datafusion/expr-common/src/sort_properties.rs
#	datafusion/expr/src/udf.rs
@github-actionsgithub-actionsBot added the sqllogictest SQL Logic Tests (.slt) label Jul 25, 2026
@rluvaton

Copy link
Copy Markdown
MemberAuthor

@alamb can you please take another look?

rluvaton pushed a commit to rluvaton/datafusion that referenced this pull request Jul 26, 2026
…xpr::check_bigger_cast (apache#23808) (apache#23809)
## Which issue does this PR close?
- Closesapache#23808.
## Rationale for this change
ref. apache#23807
`CastExpr::check_bigger_cast` is used to determine whether a cast is a
widening (order-preserving) conversion. Currently, it classifies `Int32
-> Float32`, `UInt32 -> Float32`, `Int64 -> Float64`, and `UInt64 ->
Float64` as widening casts.
However, integer-to-float conversions for 32-bit and 64-bit integers
lose precision when values exceed the mantissa bit limit (24 bits for
`Float32`, 53 bits for `Float64`). For example:
`16_777_216_i32 as f32 == 16_777_217_i32 as f32` (both yield
16777216.0f32).
Because distinct integer inputs can collapse to the same float output,
these casts are not strictly 1-to-1 (injective) and can break suffix
sort key ordering in multi-column ordering analysis (e.g. `[CAST(a AS
Float32), b]`).
## What changes are included in this PR?
- Updated `CastExpr::check_bigger_cast` to exclude precision-losing
integer-to-float conversions (`Int32/UInt32 -> Float32` and
`Int64/UInt64 -> Float64`).
- Added unit test `test_check_bigger_cast_precision_loss` to verify
precision-losing casts return `false` while exact conversions (`Int16 ->
Float32`, `Int32 -> Float64`, etc.) continue to return `true`.
## Are these changes tested?
Yes, new unit test `test_check_bigger_cast_precision_loss` in `cast.rs`.
## Are there any user-facing changes?
No breaking API changes. Internal optimizer behavior fix.

@alambalamb 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.

Thank you @rluvaton - this looks good to me

I suggest we file a ticket / tickets about marking other functions as strictly_order_preserving so that we don't forget / so we can take advantage

Comment threaddatafusion/expr-common/src/sort_properties.rs
Comment threaddatafusion/expr-common/src/sort_properties.rs

/// Returns true if the function preserves lexicographical ordering based on
/// the input ordering.
///

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 think it would be ok to keep these descriptions short and refer readers to the very nice SortProperties description

Eg. something like

Suggested change
///
/// See [`ExprProperties::preserves_lex_ordering`] for more details

Comment on lines +998 to +1037
///
/// # Examples
///
/// * `from_unixtime(a)` returns `true`: it reinterprets the input integer
/// as a timestamp without changing the value, so distinct inputs yield
/// distinct, identically-ordered outputs.
/// * `floor(a)` and `date_trunc('day', a)` must return `false`: they are
/// monotone, but collapse distinct inputs into equal outputs.
/// * An addition over ordered, overflow-free inputs may return `true`
/// even though it does not preserve *lexicographical* ordering.
///
/// # Definition
///
/// Assuming the `Ordered` inputs advance simultaneously (component-wise,
/// i.e. all of them are sorted in the data), the output is ordered in the
/// same direction, equal outputs can only result from equal values of
/// those inputs (i.e. the mapping is one-to-one), and nulls map to nulls.
///
/// This is not simply a stricter [`Self::preserves_lex_ordering`] - the
/// two properties also assume different input orderings, and with
/// multiple ordered inputs neither implies the other (see the examples
/// above). See [`ExprProperties::strictly_order_preserving`] for a
/// detailed comparison.
///
/// # Relationship to [`Self::output_ordering`]
///
/// [`Self::output_ordering`] describes *whether and in which direction*
/// the output is ordered, which justifies using the expression as the
/// **last** (or only) sort key. This property is an additional,
/// independent claim of injectivity that justifies keeping the **suffix**
/// sort keys as well: optimizers rely on it to substitute a sort key with
/// an expression computed from it - if data is sorted by `[x, y]`, it is
/// also sorted by `[expr(x), y]`, which only holds when equal `expr(x)`
/// values imply equal `x` values.
///
/// When in doubt, return `false` (the default). The `inputs` properties
/// allow conditional answers, e.g. based on the ranges of the inputs.
fn strictly_order_preserving(&self, _inputs: &[ExprProperties]) -> Result<bool> {
Ok(false)
}

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.

Similarly I think here it would be ok to point back to ExprProperties to make sure the text stays consistent

Suggested change
///
/// # Examples
///
/// * `from_unixtime(a)` returns `true`: it reinterprets the input integer
/// as a timestamp without changing the value, so distinct inputs yield
/// distinct, identically-ordered outputs.
/// * `floor(a)` and `date_trunc('day', a)` must return `false`: they are
/// monotone, but collapse distinct inputs into equal outputs.
/// * An addition over ordered, overflow-free inputs may return `true`
/// even though it does not preserve *lexicographical* ordering.
///
/// # Definition
///
/// Assuming the `Ordered` inputs advance simultaneously (component-wise,
/// i.e. all of them are sorted in the data), the output is ordered in the
/// same direction, equal outputs can only result from equal values of
/// those inputs (i.e. the mapping is one-to-one), and nulls map to nulls.
///
/// This is not simply a stricter [`Self::preserves_lex_ordering`] - the
/// two properties also assume different input orderings, and with
/// multiple ordered inputs neither implies the other (see the examples
/// above). See [`ExprProperties::strictly_order_preserving`] for a
/// detailed comparison.
///
/// # Relationship to [`Self::output_ordering`]
///
/// [`Self::output_ordering`] describes *whether and in which direction*
/// the output is ordered, which justifies using the expression as the
/// **last** (or only) sort key. This property is an additional,
/// independent claim of injectivity that justifies keeping the **suffix**
/// sort keys as well: optimizers rely on it to substitute a sort key with
/// an expression computed from it - if data is sorted by `[x, y]`, it is
/// also sorted by `[expr(x), y]`, which only holds when equal `expr(x)`
/// values imply equal `x` values.
///
/// When in doubt, return `false` (the default). The `inputs` properties
/// allow conditional answers, e.g. based on the ranges of the inputs.
fn strictly_order_preserving(&self,_inputs:&[ExprProperties]) -> Result<bool>{
Ok(false)
}
///
/// See [`ExprProperties::strictly_order_preserving`] for more details
fn strictly_order_preserving(&self,_inputs:&[ExprProperties]) -> Result<bool>{
Ok(false)
}

}

fn strictly_order_preserving(&self, _inputs: &[ExprProperties]) -> Result<bool> {
// `from_unixtime` stores the input's exact `Int64` value as a

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.

👍

let col_b = col("b", &schema)?;
let asc = SortOptions::default();
let ordering = vec![
PhysicalSortExpr::new(Arc::clone(&col_a), asc),

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.

We could probably reduce the text here (and make it easier to read) maybe by creating

let ordering_a_b = PhysicalSortExpr::new(...,asc),

?

Arc::clone(&narrowing),
asc
)])?);
assert!(!eq_properties.ordering_satisfy(vec![

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.

It does still satisfy col_a I think -- it might be nice to add an assert here that it does satisfy

@alambalamb changed the title Allow expression to report whether they keep the same ordering of the input (a.cmp(b) == f(a).cmp(f(b))) opening up for more optimizationScalarUdfImpl::strictly_order_preserving: Allow expression to report whether they keep the same ordering of the inputJul 27, 2026
@rluvaton
rluvaton added this pull request to the merge queueJul 27, 2026
Merged via the queue into apache:main with commit 1c3232cJul 27, 2026
41 checks passed
@rluvaton
rluvaton deleted the debug-why-sort-ordering-get-messed-up branch July 27, 2026 13:26
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
…xpr::check_bigger_cast (apache#23808) (apache#23809)
## Which issue does this PR close?
- Closesapache#23808.
## Rationale for this change
ref. apache#23807
`CastExpr::check_bigger_cast` is used to determine whether a cast is a
widening (order-preserving) conversion. Currently, it classifies `Int32
-> Float32`, `UInt32 -> Float32`, `Int64 -> Float64`, and `UInt64 ->
Float64` as widening casts.
However, integer-to-float conversions for 32-bit and 64-bit integers
lose precision when values exceed the mantissa bit limit (24 bits for
`Float32`, 53 bits for `Float64`). For example:
`16_777_216_i32 as f32 == 16_777_217_i32 as f32` (both yield
16777216.0f32).
Because distinct integer inputs can collapse to the same float output,
these casts are not strictly 1-to-1 (injective) and can break suffix
sort key ordering in multi-column ordering analysis (e.g. `[CAST(a AS
Float32), b]`).
## What changes are included in this PR?
- Updated `CastExpr::check_bigger_cast` to exclude precision-losing
integer-to-float conversions (`Int32/UInt32 -> Float32` and
`Int64/UInt64 -> Float64`).
- Added unit test `test_check_bigger_cast_precision_loss` to verify
precision-losing casts return `false` while exact conversions (`Int16 ->
Float32`, `Int32 -> Float64`, etc.) continue to return `true`.
## Are these changes tested?
Yes, new unit test `test_check_bigger_cast_precision_loss` in `cast.rs`.
## Are there any user-facing changes?
No breaking API changes. Internal optimizer behavior fix.
kosiew pushed a commit to kosiew/datafusion that referenced this pull request Aug 12, 2026
…t whether they keep the same ordering of the input (apache#23807)
## Which issue does this PR close?
- Closesapache#23798
Related to:
- apache#16217
## Rationale for this change
To be able to keep the same sorting order allowing for more
optimizations
Now comet own `cast` implementation can be recognized as not modifying
sort order in the same cases that datafusion cast does.
## What changes are included in this PR?
added `strictly_order_preserving` property to `ExprProperties` + varius
other places
and replaced the hard coded logic for cast (`substitute_cast_ordering`)
about keeping input order with more generic approach that now any
expression can implement and have the same advantage of sort elimination
also marked from_unixtime as keeping ordering to show a case of this
optimization
## Are these changes tested?
yes
## Are there any user-facing changes?
yes, breaking change, added `strictly_order_preserving` property to
`ExprProperties` and to `FFI_ExprProperties`
this property means that given expression `f` and 2 values from the
input column `a` and `b` the following variants are kept:
1. `a.cmp(b) == f(a).cmp(f(b))`
2. nulls maps to nulls
Example of satisfying expression: `cast(col_a as BIGINT)` where `col_a` is `INT` it is keeping the
properties
Example of not satisfying:
`floor` - floor can not
`array_repeat(my_col, 2)` which might look like at first glance as
keeping the property as well but in fact it does not.
the reason is that `array_repeat(null, 2)` will output list of 2 nulls
which breaks the 2nd property that nulls must be kept as nulls
how to migrate:
Option 1 - keeping the old behavior (safest but least performant)
set `strictly_order_preserving` to false
Option 2 - Using the new optimization that this opens up:
set `strictly_order_preserving` only if the expression keep both
variants
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api changeChanges the API exposed to users of the crateauto detected api changeAuto detected API changecoreCore DataFusion crateffiChanges to the ffi cratefunctionsChanges to functions implementationlogical-exprLogical plan and expressionsphysical-exprChanges to the physical-expr cratessqllogictestSQL Logic Tests (.slt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProjectExec does not keep sorting information for passthrough expression

4 participants

@rluvaton@codecov-commenter@alamb@getChan