[
https://issues.apache.org/jira/browse/SPARK-58792?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
James Xu updated SPARK-58792:
-----------------------------
Description:
h3. Problem
Two distinct optimizer-produced shapes corrupt the same shared, mutable Hive
UDF instance. Both reduce to one pattern: the optimizer creates two copies of a
single Hive UDF expression, both copies share one GenericUDF instance, and
their initialize() calls disagree with each other.
_Bug 1: ClassCastException or silently constant results from duplicated filter
conjuncts (SPARK-57437 trigger)._
SPARK-57437 (merged to master, ships with 4.3.0) substitutes literal bindings
into predicates, manufacturing a copy of a UDF conjunct with a constant
argument while keeping the original column-argument conjunct in the same
Filter. The two copies call initialize() on one shared instance with different
argument inspectors; the loser evaluates against state built for the winner.
Observed in production:
{code:java}
java.lang.ClassCastException:
org.apache.hadoop.hive.serde2.io.TimestampWritable cannot be cast to
java.sql.Timestamp
{code}
or, with the opposite initialization order, silently wrong results: the shared
instance ends up with the constant argument cached, so the column-argument copy
returns the constant for every row.
Example shape ({{{}ts_within_days{}}} is a Hive GenericUDF; {{pt}} is a DATE
partition column):
{code:sql}
SELECT ... FROM t
WHERE pt = '2024\-09\-10'
AND ts\_within\_days\(CAST\(pt AS TIMESTAMP\), created\_at, 31\)
{code}
_Bug 2: Swapped constant-folding results across UNION branches (stock rules;
Spark 3.x only)._
No custom rules and no custom UDF are needed. One UDF expression written once
above a UNION of literals is pushed into both branches and constant-folded on
the driver:
{code:sql}
SELECT dt, n, hive\_date\_add\(dt, n\) FROM \(
SELECT '2023\-12\-25' AS dt, 1 AS n
UNION ALL SELECT DATE '2024\-06\-30', 200\) t
{code}
On Spark 3.x (verified on public 3.3.1) the folded literals baked into the plan
are corrupted — both rows fold to 2023-12-26, or fully swapped (2025-01-16 /
2023-12-26), varying run to run — while every individual UDF evaluate() call
returns the correct value. Silent, user-visible, no exception.
This flavor does NOT reproduce on Spark 4.x, but only incidentally: SPARK-51466
(4.0.0) reworked {{HiveGenericUDFEvaluator.returnInspector}} for an unrelated
reason (avoiding Hive FunctionRegistry static initialization), and as a side
effect each copy's initialize+evaluate+capture now runs contiguously, so the
shared mutable output always holds the copy's own value at capture time. The
sharing itself is unchanged on 4.x — the shared instance is still created and
still initialized once per copy — so the 4.x safety is an ordering side effect,
not a designed guarantee.
h3. Root Cause
The sharing chain has three links, all old:
{{HiveGenericUDF.withNewChildrenInternal}} is a plain {{{}copy(children =
...){}}}, so every copied expression node shares one {{HiveFunctionWrapper}}
(present since the first Catalyst commit, SPARK-1251, 2014).
{{HiveFunctionWrapper.createFunction}} caches the GenericUDF instance for
non-simple UDFs (since SPARK-6909, 2015). Each copied node builds its own
evaluator, but every evaluator's lazy {{function}} resolves to the same cached
instance.
{{GenericUDF.initialize()}} is stateful: it derives argument inspectors,
converters, cached constant values, and mutable output holders from the
arguments it is given. Two copies that legitimately differ in argument
constness (attribute vs. literal) therefore initialize one instance with two
different inspector sets. Last initialization wins.
The inspector divergence is structural: {{HiveInspectors}} maps {{Literal(ts,
TimestampType)}} to a constant writable inspector (whose deferred object yields
a {{{}TimestampWritable{}}}) but a non-literal TimestampType to a Java
inspector (which performs {{{}(java.sql.Timestamp) object{}}}). Whichever
inspector set the shared instance was last initialized with, one of the two
copies is mismatched — hence the ClassCastException, or the cached-constant
silent wrong results.
Note that merely checking {{HiveGenericUDF.foldable}} or {{.dataType}} already
mutates the shared instance: both force {{{}returnInspector{}}}, which calls
{{{}initializeAndFoldConstants{}}}. No evaluation is required for the
corruption to start.
Bug 2 uses a different corruption channel on top of the same sharing. For
all-constant arguments, {{initializeAndFoldConstants}} pre-evaluates into the
UDF's mutable output field ({{{}GenericUDFDateAdd{}}} computes into a single
{{private final DateWritable output}} and returns it). The constant return
inspectors created for that result alias the same output object with no copy,
and Spark's {{unwrapperFor}} for a constant inspector captures the constant
eagerly, once per node — so the literal baked into the plan is whatever the
shared output happened to hold at capture time. On 3.x the two copies'
initialize/evaluate/capture sequences interleave (both copies' inspectors are
forced before either capture), so one copy captures the other's value. On 4.x
the SPARK-51466 rework happens to keep each copy's sequence contiguous, which
is the only reason Bug 2 does not manifest there.
The defect has been latent for a decade because no stock rule both duplicated a
UDF expression and diverged its argument constness while keeping both copies
live. In-place substitution rules ({{{}ConstantPropagation{}}},
{{{}FoldablePropagation{}}}) are safe — the old node is discarded. SPARK-57437
is the first rule to create the original-plus-constant-copy shape; the
UNION-fold flavor reaches it through {{PushProjectionThroughUnion}} +
{{CollapseProject}} with all-constant branches.
h3. Solution
Isolate the GenericUDF instance at the evaluator level — the single consumption
point of the cached instance — rather than at the (many, unknowable) copy sites:
* In {{{}HiveGenericUDFEvaluator{}}}, override the lazy {{function}} to return
an independent clone of the cached instance. Each expression copy then
initializes and mutates its own instance; cross-initialization becomes
impossible regardless of which rule created the copies.
* The base evaluator class must hold {{funcWrapper}} as a {{{}protected
val{}}}; referencing the constructor parameter from the subclass capture would
not survive task serialization (executor NPE).
The clone must be a verbatim copy of Hive's
{{{}FunctionRegistry.cloneGenericUDF{}}}, placed in
{{HiveFunctionRegistryUtils}} (the class SPARK-51466 created for
FunctionRegistry-avoiding forks). It must NOT call FunctionRegistry itself:
invoking any of its static methods triggers {{{}FunctionRegistry.{}}}, which
registers all Hive built-in UDFs and fails with NoClassDefFoundError on the 4.x
runtime classpath (hive-llap-common is absent since SPARK-51029) — the exact
failure SPARK-51466 eliminated from this path. The copied method handles every
GenericUDF flavor and has not changed since Hive 2.0.0 (verified against 2.3.x
/ 3.1.x bytecode and master source):
* plain GenericUDF -> fresh instance of the same class;
* GenericUDFBridge -> rebuilt with the wrapped UDF's name/className;
* GenericUDFMacro -> new macro with its body cloned (a naive
new-instance-by-class-name would silently lose the macro body);
* {{copyToNewInstance}} and SettableUDF typeInfo propagation preserved (the
parameterized-cast UDFs carry char/varchar length and decimal precision/scale
in their TypeInfo).
Scope notes:
* {{HiveSimpleUDF}} needs no change: createFunction never caches simple UDF
instances.
* {{HiveGenericUDTF}} shares the same pattern and is a lower-priority
follow-up (Generate nodes are rarely duplicated with divergent inspector sets,
and UDTFs are not constant-folded).
* GenericUDAF resolvers are also cached, but per-argument inspector state
lives in the UDAF evaluator created fresh per aggregation, not in the resolver.
h3. Expected Impact
* Bug 1: queries that currently fail with ClassCastException — or worse,
return silently constant results — return correct rows. This is the only shape
with a merged upstream trigger (SPARK-57437 ships in 4.3.0), so master is
directly exposed.
* Bug 2: on Spark 3.x, deterministically wrong folded literals become correct.
4.x behavior is unchanged (already incidentally correct).
* Cost: one extra UDF instantiation per used expression node per JVM (the
clone is lazy and @transient, recreated after deserialization on executors) —
negligible, since per-node initialize() already happens today.
h3. Related work
* SPARK-57437: the trigger rule for Bug 1; semantically sound by itself.
* SPARK-51466: performance-motivated returnInspector rework that incidentally
suppresses Bug 2 on 4.x.
* SPARK-53038: repeated initialize() on the shared instance via copied
evaluators (InlineCTE/DeduplicateRelations), filed as a performance issue; the
correctness dimension went unnoticed.
* SPARK-4244: constant folding vs. GenericUDF initialize ordering within one
expression (2014-era).
was:
h3. Problem
Two distinct optimizer\-produced shapes corrupt the same shared, mutable Hive
UDF instance. Both reduce to one pattern: the optimizer creates two copies of a
single Hive UDF expression, both copies share one GenericUDF instance, and
their initialize\(\) calls disagree with each other.
_Bug 1: ClassCastException or silently constant results from duplicated filter
conjuncts \(SPARK\-57437 trigger\)._
SPARK\-57437 \(merged to master, ships with 4.3.0\) substitutes literal
bindings into predicates, manufacturing a copy of a UDF conjunct with a
constant argument while keeping the original column\-argument conjunct in the
same Filter. The two copies call initialize\(\) on one shared instance with
different argument inspectors; the loser evaluates against state built for the
winner. Observed in production:
{code}
java.lang.ClassCastException:
org.apache.hadoop.hive.serde2.io.TimestampWritable cannot be cast to
java.sql.Timestamp
{code}
or, with the opposite initialization order, silently wrong results: the shared
instance ends up with the constant argument cached, so the column\-argument
copy returns the constant for every row.
Example shape \({{ts\_within\_days}} is a Hive GenericUDF; {{pt}} is a DATE
partition column\):
{code:sql}
SELECT ... FROM t
WHERE pt = '2024\-09\-10'
AND ts\_within\_days\(CAST\(pt AS TIMESTAMP\), created\_at, 31\)
{code}
_Bug 2: Swapped constant\-folding results across UNION branches \(stock rules;
Spark 3.x only\)._
No custom rules and no custom UDF are needed. One UDF expression written once
above a UNION of literals is pushed into both branches and constant\-folded on
the driver:
{code:sql}
SELECT dt, n, hive\_date\_add\(dt, n\) FROM \(
SELECT '2023\-12\-25' AS dt, 1 AS n
UNION ALL SELECT DATE '2024\-06\-30', 200\) t
{code}
On Spark 3.x \(verified on public 3.3.1\) the folded literals baked into the
plan are corrupted — both rows fold to 2023\-12\-26, or fully swapped
\(2025\-01\-16 / 2023\-12\-26\), varying run to run — while every individual
UDF evaluate\(\) call returns the correct value. Silent, user\-visible, no
exception.
This flavor does NOT reproduce on Spark 4.x, but only incidentally:
SPARK\-51466 \(4.0.0\) reworked {{HiveGenericUDFEvaluator.returnInspector}} for
an unrelated reason \(avoiding Hive FunctionRegistry static initialization\),
and as a side effect each copy's initialize\+evaluate\+capture now runs
contiguously, so the shared mutable output always holds the copy's own value at
capture time. The sharing itself is unchanged on 4.x — the shared instance is
still created and still initialized once per copy — so the 4.x safety is an
ordering side effect, not a designed guarantee.
h3. Root Cause
The sharing chain has three links, all old:
h1. {{HiveGenericUDF.withNewChildrenInternal}} is a plain {{copy\(children =
...\)}}, so every copied expression node shares one {{HiveFunctionWrapper}}
\(present since the first Catalyst commit, SPARK\-1251, 2014\).
h1. {{HiveFunctionWrapper.createFunction}} caches the GenericUDF instance for
non\-simple UDFs \(since SPARK\-6909, 2015\). Each copied node builds its own
evaluator, but every evaluator's lazy {{function}} resolves to the same cached
instance.
h1. {{GenericUDF.initialize\(\)}} is stateful: it derives argument inspectors,
converters, cached constant values, and mutable output holders from the
arguments it is given. Two copies that legitimately differ in argument
constness \(attribute vs. literal\) therefore initialize one instance with two
different inspector sets. Last initialization wins.
The inspector divergence is structural: {{HiveInspectors}} maps {{Literal\(ts,
TimestampType\)}} to a constant writable inspector \(whose deferred object
yields a {{TimestampWritable}}\) but a non\-literal TimestampType to a Java
inspector \(which performs {{\(java.sql.Timestamp\) object}}\). Whichever
inspector set the shared instance was last initialized with, one of the two
copies is mismatched — hence the ClassCastException, or the cached\-constant
silent wrong results.
Note that merely checking {{HiveGenericUDF.foldable}} or {{.dataType}} already
mutates the shared instance: both force {{returnInspector}}, which calls
{{initializeAndFoldConstants}}. No evaluation is required for the corruption to
start.
Bug 2 uses a different corruption channel on top of the same sharing. For
all\-constant arguments, {{initializeAndFoldConstants}} pre\-evaluates into the
UDF's mutable output field \({{GenericUDFDateAdd}} computes into a single
{{private final DateWritable output}} and returns it\). The constant return
inspectors created for that result alias the same output object with no copy,
and Spark's {{unwrapperFor}} for a constant inspector captures the constant
eagerly, once per node — so the literal baked into the plan is whatever the
shared output happened to hold at capture time. On 3.x the two copies'
initialize/evaluate/capture sequences interleave \(both copies' inspectors are
forced before either capture\), so one copy captures the other's value. On 4.x
the SPARK\-51466 rework happens to keep each copy's sequence contiguous, which
is the only reason Bug 2 does not manifest there.
The defect has been latent for a decade because no stock rule both duplicated a
UDF expression and diverged its argument constness while keeping both copies
live. In\-place substitution rules \({{ConstantPropagation}},
{{FoldablePropagation}}\) are safe — the old node is discarded. SPARK\-57437 is
the first rule to create the original\-plus\-constant\-copy shape; the
UNION\-fold flavor reaches it through {{PushProjectionThroughUnion}} \+
{{CollapseProject}} with all\-constant branches.
h3. Solution
Isolate the GenericUDF instance at the evaluator level — the single consumption
point of the cached instance — rather than at the \(many, unknowable\) copy
sites:
* In {{HiveGenericUDFEvaluator}}, override the lazy {{function}} to return an
independent clone of the cached instance. Each expression copy then initializes
and mutates its own instance; cross\-initialization becomes impossible
regardless of which rule created the copies.
* The base evaluator class must hold {{funcWrapper}} as a {{protected val}};
referencing the constructor parameter from the subclass capture would not
survive task serialization \(executor NPE\).
The clone must be a verbatim copy of Hive's
{{FunctionRegistry.cloneGenericUDF}}, placed in {{HiveFunctionRegistryUtils}}
\(the class SPARK\-51466 created for FunctionRegistry\-avoiding forks\). It
must NOT call FunctionRegistry itself: invoking any of its static methods
triggers {{FunctionRegistry.}}, which registers all Hive built\-in UDFs and
fails with NoClassDefFoundError on the 4.x runtime classpath
\(hive\-llap\-common is absent since SPARK\-51029\) — the exact failure
SPARK\-51466 eliminated from this path. The copied method handles every
GenericUDF flavor and has not changed since Hive 2.0.0 \(verified against 2.3.x
/ 3.1.x bytecode and master source\):
* plain GenericUDF \-> fresh instance of the same class;
* GenericUDFBridge \-> rebuilt with the wrapped UDF's name/className;
* GenericUDFMacro \-> new macro with its body cloned \(a naive
new\-instance\-by\-class\-name would silently lose the macro body\);
* {{copyToNewInstance}} and SettableUDF typeInfo propagation preserved \(the
parameterized\-cast UDFs carry char/varchar length and decimal precision/scale
in their TypeInfo\).
Scope notes:
* {{HiveSimpleUDF}} needs no change: createFunction never caches simple UDF
instances.
* {{HiveGenericUDTF}} shares the same pattern and is a lower\-priority
follow\-up \(Generate nodes are rarely duplicated with divergent inspector
sets, and UDTFs are not constant\-folded\).
* GenericUDAF resolvers are also cached, but per\-argument inspector state
lives in the UDAF evaluator created fresh per aggregation, not in the resolver.
h3. Expected Impact
* Bug 1: queries that currently fail with ClassCastException — or worse, return
silently constant results — return correct rows. This is the only shape with a
merged upstream trigger \(SPARK\-57437 ships in 4.3.0\), so master is directly
exposed.
* Bug 2: on Spark 3.x, deterministically wrong folded literals become correct.
4.x behavior is unchanged \(already incidentally correct\).
* Cost: one extra UDF instantiation per used expression node per JVM \(the
clone is lazy and @transient, recreated after deserialization on executors\) —
negligible, since per\-node initialize\(\) already happens today.
h3. Related work
* SPARK\-57437: the trigger rule for Bug 1; semantically sound by itself.
* SPARK\-51466: performance\-motivated returnInspector rework that incidentally
suppresses Bug 2 on 4.x.
* SPARK\-53038: repeated initialize\(\) on the shared instance via copied
evaluators \(InlineCTE/DeduplicateRelations\), filed as a performance issue;
the correctness dimension went unnoticed.
* SPARK\-4244: constant folding vs. GenericUDF initialize ordering within one
expression \(2014\-era\).
> [SQL] Isolate Hive GenericUDF instances per expression copy to fix wrong
> results and ClassCastException from optimizer-duplicated UDF expressions
> -------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: SPARK-58792
> URL: https://issues.apache.org/jira/browse/SPARK-58792
> Project: Spark
> Issue Type: Bug
> Components: SQL
> Affects Versions: 3.3.1
> Reporter: James Xu
> Priority: Major
>
> h3. Problem
> Two distinct optimizer-produced shapes corrupt the same shared, mutable Hive
> UDF instance. Both reduce to one pattern: the optimizer creates two copies of
> a single Hive UDF expression, both copies share one GenericUDF instance, and
> their initialize() calls disagree with each other.
> _Bug 1: ClassCastException or silently constant results from duplicated
> filter conjuncts (SPARK-57437 trigger)._
> SPARK-57437 (merged to master, ships with 4.3.0) substitutes literal bindings
> into predicates, manufacturing a copy of a UDF conjunct with a constant
> argument while keeping the original column-argument conjunct in the same
> Filter. The two copies call initialize() on one shared instance with
> different argument inspectors; the loser evaluates against state built for
> the winner. Observed in production:
> {code:java}
> java.lang.ClassCastException:
> org.apache.hadoop.hive.serde2.io.TimestampWritable cannot be cast to
> java.sql.Timestamp
> {code}
> or, with the opposite initialization order, silently wrong results: the
> shared instance ends up with the constant argument cached, so the
> column-argument copy returns the constant for every row.
> Example shape ({{{}ts_within_days{}}} is a Hive GenericUDF; {{pt}} is a DATE
> partition column):
> {code:sql}
> SELECT ... FROM t
> WHERE pt = '2024\-09\-10'
> AND ts\_within\_days\(CAST\(pt AS TIMESTAMP\), created\_at, 31\)
> {code}
> _Bug 2: Swapped constant-folding results across UNION branches (stock rules;
> Spark 3.x only)._
> No custom rules and no custom UDF are needed. One UDF expression written once
> above a UNION of literals is pushed into both branches and constant-folded on
> the driver:
> {code:sql}
> SELECT dt, n, hive\_date\_add\(dt, n\) FROM \(
> SELECT '2023\-12\-25' AS dt, 1 AS n
> UNION ALL SELECT DATE '2024\-06\-30', 200\) t
> {code}
> On Spark 3.x (verified on public 3.3.1) the folded literals baked into the
> plan are corrupted — both rows fold to 2023-12-26, or fully swapped
> (2025-01-16 / 2023-12-26), varying run to run — while every individual UDF
> evaluate() call returns the correct value. Silent, user-visible, no exception.
> This flavor does NOT reproduce on Spark 4.x, but only incidentally:
> SPARK-51466 (4.0.0) reworked {{HiveGenericUDFEvaluator.returnInspector}} for
> an unrelated reason (avoiding Hive FunctionRegistry static initialization),
> and as a side effect each copy's initialize+evaluate+capture now runs
> contiguously, so the shared mutable output always holds the copy's own value
> at capture time. The sharing itself is unchanged on 4.x — the shared instance
> is still created and still initialized once per copy — so the 4.x safety is
> an ordering side effect, not a designed guarantee.
> h3. Root Cause
> The sharing chain has three links, all old:
> {{HiveGenericUDF.withNewChildrenInternal}} is a plain {{{}copy(children =
> ...){}}}, so every copied expression node shares one {{HiveFunctionWrapper}}
> (present since the first Catalyst commit, SPARK-1251, 2014).
> {{HiveFunctionWrapper.createFunction}} caches the GenericUDF instance for
> non-simple UDFs (since SPARK-6909, 2015). Each copied node builds its own
> evaluator, but every evaluator's lazy {{function}} resolves to the same
> cached instance.
> {{GenericUDF.initialize()}} is stateful: it derives argument inspectors,
> converters, cached constant values, and mutable output holders from the
> arguments it is given. Two copies that legitimately differ in argument
> constness (attribute vs. literal) therefore initialize one instance with two
> different inspector sets. Last initialization wins.
> The inspector divergence is structural: {{HiveInspectors}} maps {{Literal(ts,
> TimestampType)}} to a constant writable inspector (whose deferred object
> yields a {{{}TimestampWritable{}}}) but a non-literal TimestampType to a Java
> inspector (which performs {{{}(java.sql.Timestamp) object{}}}). Whichever
> inspector set the shared instance was last initialized with, one of the two
> copies is mismatched — hence the ClassCastException, or the cached-constant
> silent wrong results.
> Note that merely checking {{HiveGenericUDF.foldable}} or {{.dataType}}
> already mutates the shared instance: both force {{{}returnInspector{}}},
> which calls {{{}initializeAndFoldConstants{}}}. No evaluation is required for
> the corruption to start.
> Bug 2 uses a different corruption channel on top of the same sharing. For
> all-constant arguments, {{initializeAndFoldConstants}} pre-evaluates into the
> UDF's mutable output field ({{{}GenericUDFDateAdd{}}} computes into a single
> {{private final DateWritable output}} and returns it). The constant return
> inspectors created for that result alias the same output object with no copy,
> and Spark's {{unwrapperFor}} for a constant inspector captures the constant
> eagerly, once per node — so the literal baked into the plan is whatever the
> shared output happened to hold at capture time. On 3.x the two copies'
> initialize/evaluate/capture sequences interleave (both copies' inspectors are
> forced before either capture), so one copy captures the other's value. On 4.x
> the SPARK-51466 rework happens to keep each copy's sequence contiguous, which
> is the only reason Bug 2 does not manifest there.
> The defect has been latent for a decade because no stock rule both duplicated
> a UDF expression and diverged its argument constness while keeping both
> copies live. In-place substitution rules ({{{}ConstantPropagation{}}},
> {{{}FoldablePropagation{}}}) are safe — the old node is discarded.
> SPARK-57437 is the first rule to create the original-plus-constant-copy
> shape; the UNION-fold flavor reaches it through
> {{PushProjectionThroughUnion}} + {{CollapseProject}} with all-constant
> branches.
> h3. Solution
> Isolate the GenericUDF instance at the evaluator level — the single
> consumption point of the cached instance — rather than at the (many,
> unknowable) copy sites:
> * In {{{}HiveGenericUDFEvaluator{}}}, override the lazy {{function}} to
> return an independent clone of the cached instance. Each expression copy then
> initializes and mutates its own instance; cross-initialization becomes
> impossible regardless of which rule created the copies.
> * The base evaluator class must hold {{funcWrapper}} as a {{{}protected
> val{}}}; referencing the constructor parameter from the subclass capture
> would not survive task serialization (executor NPE).
> The clone must be a verbatim copy of Hive's
> {{{}FunctionRegistry.cloneGenericUDF{}}}, placed in
> {{HiveFunctionRegistryUtils}} (the class SPARK-51466 created for
> FunctionRegistry-avoiding forks). It must NOT call FunctionRegistry itself:
> invoking any of its static methods triggers {{{}FunctionRegistry.{}}}, which
> registers all Hive built-in UDFs and fails with NoClassDefFoundError on the
> 4.x runtime classpath (hive-llap-common is absent since SPARK-51029) — the
> exact failure SPARK-51466 eliminated from this path. The copied method
> handles every GenericUDF flavor and has not changed since Hive 2.0.0
> (verified against 2.3.x / 3.1.x bytecode and master source):
> * plain GenericUDF -> fresh instance of the same class;
> * GenericUDFBridge -> rebuilt with the wrapped UDF's name/className;
> * GenericUDFMacro -> new macro with its body cloned (a naive
> new-instance-by-class-name would silently lose the macro body);
> * {{copyToNewInstance}} and SettableUDF typeInfo propagation preserved (the
> parameterized-cast UDFs carry char/varchar length and decimal precision/scale
> in their TypeInfo).
> Scope notes:
> * {{HiveSimpleUDF}} needs no change: createFunction never caches simple UDF
> instances.
> * {{HiveGenericUDTF}} shares the same pattern and is a lower-priority
> follow-up (Generate nodes are rarely duplicated with divergent inspector
> sets, and UDTFs are not constant-folded).
> * GenericUDAF resolvers are also cached, but per-argument inspector state
> lives in the UDAF evaluator created fresh per aggregation, not in the
> resolver.
> h3. Expected Impact
> * Bug 1: queries that currently fail with ClassCastException — or worse,
> return silently constant results — return correct rows. This is the only
> shape with a merged upstream trigger (SPARK-57437 ships in 4.3.0), so master
> is directly exposed.
> * Bug 2: on Spark 3.x, deterministically wrong folded literals become
> correct. 4.x behavior is unchanged (already incidentally correct).
> * Cost: one extra UDF instantiation per used expression node per JVM (the
> clone is lazy and @transient, recreated after deserialization on executors) —
> negligible, since per-node initialize() already happens today.
> h3. Related work
> * SPARK-57437: the trigger rule for Bug 1; semantically sound by itself.
> * SPARK-51466: performance-motivated returnInspector rework that
> incidentally suppresses Bug 2 on 4.x.
> * SPARK-53038: repeated initialize() on the shared instance via copied
> evaluators (InlineCTE/DeduplicateRelations), filed as a performance issue;
> the correctness dimension went unnoticed.
> * SPARK-4244: constant folding vs. GenericUDF initialize ordering within one
> expression (2014-era).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]