You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[EPIC] ScalarUDFImpl::equals default implementation is error-prone #16677
When you look at ScalarUDFImpl::equals, there is nothing wrong with it.
Problems happen when you do not look at it:
case a)
add a normal struct implementing a function, i.e. implement ScalarUDFImpl trait
add tests, perhaps with slt
... some time later ...
make the function parameterized (add new field, like "time zone", "default value" or similar)
add tests, perhaps with slt
case b)
in the same LogicalPlan put two functions implemented by two different structs, for example regex_replace(string, string, string) with different regex engines
In both cases everything works great in tests, yet the real queries may fail, because the default implementation of ScalarUDFImpl::equals silently incorrect.
In case (a) it was because of evolution of the code.
In case (b) it was merely because two different functions can have same name & signature.
Expected
Default implementation of a public trait method should be less easy to get wrong.
That PR adds missing equality implementation to many functions and improves default implementation to consider type_id and aliases. It well shows how "wrong by default" equality turned out to be a real problem in practice even within this code base. We don't know about other codebases, but for dbt it was a problem as well, leading to hard-to-understand query failures.
#[derive(Debug,Clone,PartialEq,Hash)]structMyFunc{state:SomeEquitableStateType,}implScalarUDFImplforMyFunc{// nothing eq/hash related needed here}
As shown, the end result is that ScalarUDFImpl can be hashed and compared for equality and all the implementor needs is #[derive(PartialEq, Hash)] to make it work.
Notes
UdfEq accepts any Any
UdfEq accepts any Any so it's possible to call it with instances other than &dyn ScalarUDFImpl.
I don't know how to avoid this.
In particular, this is not allowed in Rust:
// Error: cycle detected when computing the super predicates of `ScalarUDFImpl` [E0391]traitScalarUDFImpl:PartialEq<dynScalarUDFImpl>{/* ... */}
We can cover this with additional eq/hash methods directly in ScalarUDFImpl trait that will have default implementation and will not need to be reimplemented.
As with any trait with blanket implementations (such as Into), when you don't implement it yet, the compiler won't tell you how you should implement.
For example, if a function lacks PartialEq implementation, the compiler will complain about UdfEq instead.
// Missing eq implementation#[derive(Debug,Clone,/*PartialEq,*/Hash)]structMyFunc{safe:bool,}// Error: the trait bound `MyFunc: UdfEq` is not satisfiedimplScalarUDFImplforMyFunc{/* ... */}
It can be mitigated by a good documentation for the UdfEq, UdfHash traits informing about blanket implementation and that they should not be implemented directly.
#16842 is a POC how we can convert hand-written equals, hash_value implementations into PartialEq and Hash traits, which would be good preparatory step for the plan proposed above.
I have defined sub tasks in this issue.
I want to use ScalarUDFImpl as the design driver / place where review happens. The AggregateUDFImpl and WindowUDFImpl can be worked on independently.
The code author or the first PR was generally well-aware of existence of equals/hash_value contract on UDFs and so were the reviewers, yet the bug was made. Subtle but dangerous.
To me it makes it clear that we really must fix the design and make it much harder to write incorrect code, even at the cost of downstream projects needing an update.
changed the title [-]ScalarUDFImpl::equals default implementation is error-prone[/-][+][EPIC] ScalarUDFImpl::equals default implementation is error-prone[/+]on Aug 8, 2025
Activity
alamb commentedon Jul 4, 2025
🤔 That is a tricky business
Maybe ScalarUDFImpl needs some way to have
eqcalled on it too 🤔findepi commentedon Jul 18, 2025
Worth noting
That PR adds missing equality implementation to many functions and improves default implementation to consider type_id and aliases. It well shows how "wrong by default" equality turned out to be a real problem in practice even within this code base. We don't know about other codebases, but for dbt it was a problem as well, leading to hard-to-understand query failures.
findepi commentedon Jul 19, 2025
How we can solve this:
Add two new traits
with blanket implementations
Make
ScalarUDFImpldepend on theseUsage
As shown, the end result is that
ScalarUDFImplcan be hashed and compared for equality and all the implementor needs is#[derive(PartialEq, Hash)]to make it work.Notes
UdfEqaccepts anyAnyUdfEqaccepts anyAnyso it's possible to call it with instances other than&dyn ScalarUDFImpl.I don't know how to avoid this.
In particular, this is not allowed in Rust:
We can cover this with additional eq/hash methods directly in
ScalarUDFImpltrait that will have default implementation and will not need to be reimplemented.Compiler errors don't make it obvious
As with any trait with blanket implementations (such as
Into), when you don't implement it yet, the compiler won't tell you how you should implement.For example, if a function lacks
PartialEqimplementation, the compiler will complain aboutUdfEqinstead.It can be mitigated by a good documentation for the
UdfEq,UdfHashtraits informing about blanket implementation and that they should not be implemented directly.findepi commentedon Jul 21, 2025
#16842 is a POC how we can convert hand-written equals, hash_value implementations into
PartialEqandHashtraits, which would be good preparatory step for the plan proposed above.alamb commentedon Jul 21, 2025
I think this idea sounds great -- as long as it is documented (which I think we can given your description) it will be great
ScalarUDFImpl) equality from PartialEq, Hash #16865AggregateUDFImpl) equality from PartialEq, Hash #16866WindowUDFImpl) equality from PartialEq, Hash #16867ScalarUDFImpl) #16868AggregateUDFImpl) #16869WindowUDFImpl) #16870ScalarUDFImpl::{equals,hash_value}withUdfHash,UdfEqtraits #16871AggregateUDFImpl::{equals,hash_value}withUdfHash,UdfEqtraits #16872WindowUDFImpl::{equals,hash_value}withUdfHash,UdfEqtraits #16873findepi commentedon Jul 23, 2025
I have defined sub tasks in this issue.
I want to use
ScalarUDFImplas the design driver / place where review happens. TheAggregateUDFImplandWindowUDFImplcan be worked on independently.findepi commentedon Aug 7, 2025
The error-proneness is very well exemplified by
array_aggaggregations #16625 introducing a bugThe code author or the first PR was generally well-aware of existence of equals/hash_value contract on UDFs and so were the reviewers, yet the bug was made. Subtle but dangerous.
To me it makes it clear that we really must fix the design and make it much harder to write incorrect code, even at the cost of downstream projects needing an update.
cc @alamb
findepi commentedon Aug 8, 2025
This PR introduces the proposed change for window functions:
WindowUDFImplequality, hash fromEq,Hashtraits #17081[-]ScalarUDFImpl::equals default implementation is error-prone[/-][+][EPIC] ScalarUDFImpl::equals default implementation is error-prone[/+]findepi commentedon Aug 11, 2025
Aggregates
AggregateUDFImplequality, hash fromEq,Hashtraits #17130