sunchao commented on code in PR #56575:
URL: https://github.com/apache/spark/pull/56575#discussion_r3807660938
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala:
##########
@@ -2690,29 +2690,40 @@ case class Substring(str: Expression, pos: Expression,
len: Expression)
since = "2.3.0",
group = "string_funcs")
// scalastyle:on line.size.limit
-case class Right(str: Expression, len: Expression) extends RuntimeReplaceable
- with ImplicitCastInputTypes with BinaryLike[Expression] {
-
- override lazy val replacement: Expression = If(
- IsNull(str),
- Literal(null, str.dataType),
- If(
- LessThanOrEqual(len, Literal(0)),
- Literal(UTF8String.EMPTY_UTF8, str.dataType),
- new Substring(str, UnaryMinus(len, failOnError = false))
- )
- )
+object Right extends DelegateFunction {
+ override val name: String = "right"
override def inputTypes: Seq[AbstractDataType] =
- Seq(
- StringTypeWithCollation(supportsTrimCollation = true),
- IntegerType
- )
- override def left: Expression = str
- override def right: Expression = len
- override protected def withNewChildrenInternal(
- newLeft: Expression, newRight: Expression): Expression = {
- copy(str = newLeft, len = newRight)
+ Seq(StringTypeWithCollation(supportsTrimCollation = true), IntegerType)
+
+ // At build time `str` is the not-yet-coerced argument (wrapped in an
`ImplicitCastInput` marker
+ // that delegates `dataType` to its child), so `str.dataType` is the *input*
type, which is not
+ // necessarily a string yet -- e.g. `right(12345, 2)` has an `IntegerType`
child the implicit cast
+ // will turn into a string. Use it for the null/empty branch literals only
when it is already a
+ // string-family type, so a CHAR(N)/VARCHAR(N) result (under
+ // `spark.sql.preserveCharVarcharTypeInfo`) or a non-default collation is
preserved through the
+ // `If` branch unification; otherwise fall back to plain `StringType`, the
type the implicit cast
+ // produces. Typing a UTF8String literal with a non-string type would be
invalid.
+ override def lower(args: Seq[Expression]): Expression = {
+ val str = args(0)
+ val len = args(1)
+ val litType = str.dataType match {
+ case _: StringType | _: CharType | _: VarcharType => str.dataType
+ case _ => StringType
+ }
+ // Keep both arguments single-use while the analyzer extracts window
expressions. The length
+ // is bound inside the non-null branch so right's null short-circuit is
preserved.
+ With(str) { case Seq(strRef) =>
Review Comment:
[P2] Preserve analysis-time evaluation of constant right calls
`VALUES (right('abc', 1))` previously produced one row containing `c`, but
now fails with
`INVALID_INLINE_TABLE.CANNOT_EVALUATE_EXPRESSION_IN_INLINE_TABLE`. The new
definition starts with `With`, which is unevaluable and non-foldable.
Inline-table analysis checks `prepareForEval(e).foldable`, and that helper only
unwraps `RuntimeReplaceable`; the new delegate is therefore rejected before the
optimizer can rewrite its definition. This is independent of the nested-`With`
optimizer crash. I reproduced the analysis failure and verified that the
previous foldable `If`/`Substring` definition still passes the same
inline-table validation. Please preserve this constant-expression behavior and
add a `VALUES` regression.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:
##########
@@ -178,72 +178,79 @@ object GetJsonObject {
}
/**
- * Extracts multiple simple object-key and array-index paths from a JSON
string in one parse. This
- * is an internal expression used to share sibling [[GetJsonObject]]
expressions; unsupported and
- * prefix-conflicting JSON paths remain as independent GetJsonObject
expressions.
+ * Builds the internal expression that extracts multiple simple object-key and
array-index paths
+ * from a JSON string in one parse, used to share sibling [[GetJsonObject]]
expressions; unsupported
+ * and prefix-conflicting paths remain as independent `GetJsonObject`
expressions.
+ *
+ * It is inserted by `OptimizeCsvJsonExprs` (after analysis, so its inputs are
resolved), and is the
+ * optimizer-constructed showcase for [[DelegateExpression]]: instead of
hand-written
+ * eval/doGenCode, it builds a typed delegate directly -- the high-level call
+ * `multi_get_json_object(json, p1, ..., pn)` stays visible via `inputs`,
while the `definition`
+ * delegates evaluation to [[MultiGetJsonObjectEvaluator]] through an
`Invoke`. The delegate stays
+ * in logical plans and is lowered to its definition before physical planning.
*/
-case class MultiGetJsonObject(
- json: Expression,
- fallbackPaths: Seq[String])
- extends UnaryExpression
- with ExpectsInputTypes {
-
- // OptimizeCsvJsonExprs caps shared path depth to keep evaluator recursion
stack-safe.
- require(fallbackPaths.nonEmpty)
-
- override def child: Expression = json
-
- override def inputTypes: Seq[AbstractDataType] =
- Seq(StringTypeWithCollation(supportsTrimCollation = true))
-
- override lazy val dataType: DataType = StructType(fallbackPaths.indices.map
{ index =>
- StructField(s"_$index", StringType, nullable = true)
- })
-
- override def nullable: Boolean = true
-
- // This internal unary expression always returns null when its JSON child is
null.
- override def nullIntolerant: Boolean = true
-
- override def prettyName: String = "multi_get_json_object"
-
- final override val nodePatterns: Seq[TreePattern] = Seq(GET_JSON_OBJECT)
-
- @transient
- private lazy val simplePaths = fallbackPaths.map { path =>
- GetJsonObject.simplePath(UTF8String.fromString(path)).getOrElse {
- throw new IllegalArgumentException(s"Unsupported shared JSON path:
$path")
+object MultiGetJsonObject {
+ val name: String = "multi_get_json_object"
+
+ def apply(json: Expression, fallbackPaths: Seq[String]): DelegateExpression
= {
+ // OptimizeCsvJsonExprs caps shared path depth to keep evaluator recursion
stack-safe.
+ require(fallbackPaths.nonEmpty)
+ val resultType = StructType(fallbackPaths.indices.map { index =>
+ StructField(s"_$index", StringType, nullable = true)
+ })
+ val utf8Paths = fallbackPaths.map(UTF8String.fromString)
+ val simplePaths = utf8Paths.map { path =>
+ GetJsonObject.simplePath(path).getOrElse {
+ throw new IllegalArgumentException(s"Unsupported shared JSON path:
$path")
+ }
}
+ val evaluator = MultiGetJsonObjectEvaluatorHolder(utf8Paths, simplePaths)
+ // `propagateNull = true` reproduces the old null-intolerant behavior:
null json -> null result.
+ val definition = Invoke(
+ evaluator,
+ "evaluate",
+ resultType,
+ Seq(json),
+ Seq(json.dataType),
+ returnNullable = true)
+ // `inputs` keeps the high-level call visible: the json plus one string
literal per path.
+ val pathInputs = utf8Paths.map(Literal(_, StringType))
+ DelegateExpression(name, json +: pathInputs, definition)
}
- override def stateful: Boolean = true
+ /** Recovers `(json, fallbackPaths)` from a delegate produced by `apply`. */
+ def unapply(e: Expression): Option[(Expression, Seq[String])] = e match {
+ case d: DelegateExpression if d.name == name =>
+ val paths = d.inputs.tail.map {
+ case Literal(p: UTF8String, _: StringType) => p.toString
+ case other => throw new IllegalStateException(s"Unexpected path input:
$other")
+ }
+ Some((d.inputs.head, paths))
+ case _ => None
+ }
- @transient
- private lazy val evaluator = MultiGetJsonObjectEvaluator(
- fallbackPaths.map(UTF8String.fromString),
- simplePaths)
+ def isInstance(e: Expression): Boolean = unapply(e).isDefined
- override def eval(input: InternalRow): Any = {
- evaluator.evaluate(json.eval(input).asInstanceOf[UTF8String])
+ def pathsOf(e: Expression): Seq[String] = unapply(e) match {
+ case Some((_, paths)) => paths
+ case None => throw new IllegalArgumentException(s"Not a
multi_get_json_object: $e")
}
+}
- override protected def doGenCode(ctx: CodegenContext, ev: ExprCode):
ExprCode = {
- val refEvaluator = ctx.addReferenceObj("evaluator", evaluator)
- val jsonEval = json.genCode(ctx)
- val resultType = CodeGenerator.javaType(dataType)
- ev.copy(code = code"""
- |${jsonEval.code}
- |boolean ${ev.isNull} = ${jsonEval.isNull};
- |$resultType ${ev.value} = ${CodeGenerator.defaultValue(dataType)};
- |if (!${ev.isNull}) {
- | ${ev.value} = ($resultType)
$refEvaluator.evaluate(${jsonEval.value});
- | ${ev.isNull} = ${ev.value} == null;
- |}
- |""".stripMargin)
- }
+/** Holds one mutable JSON evaluator per fresh expression copy. */
+private case class MultiGetJsonObjectEvaluatorHolder(
+ paths: Seq[UTF8String],
+ simplePaths: Seq[Seq[GetJsonObject.SimpleJsonPathSegment]])
+ extends LeafExpression with CodegenFallback {
- override protected def withNewChildInternal(newChild: Expression):
MultiGetJsonObject =
- copy(json = newChild)
+ @transient private lazy val evaluator = MultiGetJsonObjectEvaluator(paths,
simplePaths)
+
+ override def dataType: DataType =
ObjectType(classOf[MultiGetJsonObjectEvaluator])
+ override def nullable: Boolean = false
+ override def stateful: Boolean = true
+ override def eval(input: InternalRow): Any = evaluator
+ override protected def withNewChildrenInternal(
Review Comment:
[P1] Keep the leaf copy override public
`LeafLike.withNewChildrenInternal` is public, so this `protected` override
reduces its visibility and prevents Catalyst from compiling. The exact-head
[SBT precompile
job](https://github.com/cloud-fan/spark/actions/runs/32165746499/job/95805272577)
reports that the override has weaker access privileges and should be public.
Please remove `protected` while retaining `copy()`; the fresh-holder behavior
is still needed.
##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/DelegateExpressionSuite.scala:
##########
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.catalyst.expressions
+
+import org.apache.spark.SparkFunSuite
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.analysis.{RemoveInputTypeMarkers,
UnresolvedAttribute}
+import org.apache.spark.sql.catalyst.analysis.TypeCheckResult.DataTypeMismatch
+import org.apache.spark.sql.catalyst.expressions.objects.Invoke
+import org.apache.spark.sql.types.{AbstractDataType, AnyDataType, IntegerType,
StringType, StructField, StructType}
+import org.apache.spark.unsafe.types.UTF8String
+
+/**
+ * Validates [[DelegateExpression]] transparency (eval + codegen, via
`checkEvaluation` which runs
+ * both paths) and that [[DelegateFunction]] supports all three input-type
contracts.
+ */
+class DelegateExpressionSuite extends SparkFunSuite with ExpressionEvalHelper {
+
+ // ---- transparency: every behavior delegates to `definition` ----
+
+ test("delegates eval and codegen to its definition (foldable)") {
+ val expr = DelegateExpression("inc", Seq(Literal(10)), Add(Literal(10),
Literal(1)))
+ checkEvaluation(expr, 11)
+ }
+
+ test("delegates eval and codegen with a non-foldable input") {
+ val ref = BoundReference(0, IntegerType, nullable = true)
+ val expr = DelegateExpression("inc", Seq(ref), Add(ref, Literal(1)))
+ checkEvaluation(expr, 11, InternalRow(10))
+ checkEvaluation(expr, null, InternalRow(null))
+ }
+
+ test("delegates type/nullability/foldability/determinism and canonicalizes
to its definition") {
+ val ref = BoundReference(0, IntegerType, nullable = true)
+ val expr = DelegateExpression("inc", Seq(ref), Add(ref, Literal(1)))
+ assert(expr.dataType == IntegerType)
+ assert(expr.nullable)
+ assert(!expr.foldable)
+ assert(expr.deterministic)
+ assert(expr.canonicalized == Add(ref, Literal(1)).canonicalized)
+ assert(DelegateExpression("inc", Seq(Literal(10)), Add(Literal(10),
Literal(1))).foldable)
+ }
+
+ // ---- input-type contracts ----
+
+ private object CastFn extends DelegateFunction {
+ override val name = "castfn"
+ override def inputTypes: Seq[AbstractDataType] = Seq(StringType)
+ override def implicitCast: Boolean = true
+ override def lower(args: Seq[Expression]): Expression = args.head
+ }
+
+ private object CheckFn extends DelegateFunction {
+ override val name = "checkfn"
+ override def inputTypes: Seq[AbstractDataType] = Seq(IntegerType)
+ override def implicitCast: Boolean = false
+ override def lower(args: Seq[Expression]): Expression = args.head
+ }
+
+ private object AnyFn extends DelegateFunction {
+ override val name = "anyfn"
+ // no inputTypes -> accepts any type
+ override def lower(args: Seq[Expression]): Expression = args.head
+ }
+
+ // The input-type contracts are the analysis-time `build` path, which
inserts the markers.
+ private def buildDef(fn: DelegateFunction, args: Expression*): Expression =
+ fn.build(fn.name, args).asInstanceOf[DelegateExpression].definition
+
+ test("contract 1 (implicit cast): args wrapped in ImplicitCastInput (cast
happens via " +
+ "the standard coercion rule)") {
+ val shim = buildDef(CastFn, Literal(1)).asInstanceOf[ImplicitCastInput]
+ assert(shim.expectedType == StringType)
+ // The shim IS an ImplicitCastInputTypes node, so TypeCoercion will cast
its child.
+ assert(shim.isInstanceOf[ImplicitCastInputTypes])
+ }
+
+ test("contract 2 (type check only): args wrapped in TypeCheckInput, mismatch
rejected, " +
+ "no cast") {
+ val ok = buildDef(CheckFn, Literal(1)).asInstanceOf[TypeCheckInput]
+ assert(ok.checkInputDataTypes().isSuccess)
+ // A Long is NOT cast down to Int -- it is rejected.
+ val bad = buildDef(CheckFn, Literal(1L)).asInstanceOf[TypeCheckInput]
+ assert(bad.checkInputDataTypes().isFailure)
+ assert(!bad.isInstanceOf[ImplicitCastInputTypes])
+ }
+
+ test("contract 3 (any type): no inputTypes -> no shim, arg passed through
unchanged") {
+ assert(buildDef(AnyFn, Literal(1L)) == Literal(1L))
+ }
+
+ test("nullIntolerant is delegated to the definition") {
+ // An Invoke with the default propagateNull = true is null-intolerant; a
bare Literal is not.
+ val invoke = Invoke(Literal("x"), "toString", StringType)
+ assert(invoke.nullIntolerant)
+ assert(DelegateExpression("f", Seq(Literal("x")), invoke).nullIntolerant,
+ "the wrapper should report its null-intolerant definition's
null-intolerance")
+ assert(!DelegateExpression("g", Seq(Literal(1)),
Literal(1)).nullIntolerant)
+ }
+
+ test("build validates argument count against the inputTypes arity") {
+ // CheckFn declares one typed input, so build rejects any other arity with
WRONG_NUM_ARGS rather
+ // than indexing past the args (too few) or silently ignoring extras (too
many).
+ Seq(Seq.empty[Expression], Seq(Literal(1), Literal(2))).foreach { args =>
+ val e = intercept[AnalysisException](CheckFn.build(CheckFn.name, args))
+ assert(e.getCondition == "WRONG_NUM_ARGS.WITHOUT_SUGGESTION")
+ }
+ // AnyFn has no inputTypes -> it is variadic and `lower` owns the arg
handling, so no arity
+ // check.
+ assert(AnyFn.build(AnyFn.name, Seq(Literal(1),
Literal(2))).isInstanceOf[DelegateExpression])
+ }
+
+ test("a surviving marker reports transparently: delegate-call sql and the
real argument index") {
+ // A marker only survives when its type check failed, and `CheckAnalysis`
(bottom-up) reports it
+ // before the enclosing delegate. It must therefore look like the
high-level call, not an
+ // internal shim: its `sql` is the delegate call, and its type-check error
carries the true
+ // argument position rather than the marker's only-child index 0 ("first").
+ val marker = ImplicitCastInput(
+ Literal(1L), StringType, funcName = "castfn", argIndex = 1, callSql =
"castfn(x, 1)")
+ assert(marker.sql == "castfn(x, 1)")
+ marker.checkInputDataTypes() match {
+ case m: DataTypeMismatch =>
+ assert(m.messageParameters("paramIndex") == "second",
+ s"expected the real argument index, got
${m.messageParameters("paramIndex")}")
+ case other => fail(s"expected a DataTypeMismatch, got $other")
+ }
+ // With no supplied context (direct construction), `sql` falls back to the
plain node rendering.
+ assert(
+ ImplicitCastInput(Literal(1L), StringType).sql ==
s"implicitcastinput(${Literal(1L).sql})")
+ }
+
+ test("RemoveInputTypeMarkers keeps a failed type-check marker for
CheckAnalysis to report") {
+ // A resolved marker has served its purpose and is unwrapped to its child
...
+ val okDelegate = CheckFn.build(CheckFn.name, Seq(Literal(1)))
+
assert(!RemoveInputTypeMarkers.removeMarkers(okDelegate).exists(_.isInstanceOf[TypeCheckInput]),
+ "a resolved TypeCheckInput should be unwrapped")
+ // ... but a type-mismatched (unresolved) marker is left in place, so its
ExpectsInputTypes
+ // failure stays visible to CheckAnalysis instead of exposing a resolved
child of a wrong type.
+ val badDelegate = CheckFn.build(CheckFn.name, Seq(Literal(1L)))
+ val cleaned = RemoveInputTypeMarkers.removeMarkers(badDelegate)
+ assert(cleaned.exists(_.isInstanceOf[TypeCheckInput]),
+ s"a failed TypeCheckInput must be preserved for CheckAnalysis, got
$cleaned")
+ }
+
+ private object MixedFn extends DelegateFunction {
+ override val name = "mixedfn"
+ override def inputTypes: Seq[AbstractDataType] = Seq(StringType,
AnyDataType)
+ override def lower(args: Seq[Expression]): Expression = CreateArray(args)
+ }
+
+ test("input-type contract is per argument: AnyDataType position opts out of
shimming") {
+ val args = buildDef(MixedFn, Literal(1),
Literal(2)).asInstanceOf[CreateArray].children
+ assert(args(0).isInstanceOf[ImplicitCastInput]) // StringType -> shimmed
+ assert(args(1) == Literal(2)) // AnyDataType -> raw
+ }
+
+ test("apply (direct construction) inserts no markers; args must already be
typed") {
+ // Unlike `build`, `apply` is construct-anywhere and never produces
input-type markers.
+ assert(CastFn(Literal("x")).definition == Literal("x"))
+ assert(
+ MixedFn(Literal("s"), Literal(2)).definition ==
CreateArray(Seq(Literal("s"), Literal(2))))
+ }
+
+ test("apply rejects unresolved arguments") {
+ intercept[IllegalArgumentException](CastFn(UnresolvedAttribute("x")))
+ }
+
+ // ---- definition is a real child (the safety property the whole design
rests on) ----
+
+ test("transform reaches into the definition and withNewChildren replaces
it") {
+ val ref = BoundReference(0, IntegerType, nullable = true)
+ val expr = DelegateExpression("inc", Seq(ref), Add(ref, Literal(1)))
+ // tree traversal descends into `definition`
+ val bumped = expr.transform { case Literal(1, IntegerType) => Literal(2) }
+ assert(bumped == DelegateExpression("inc", Seq(ref), Add(ref, Literal(2))))
+ // withNewChildren swaps the single child, which is the definition
+ val replaced =
expr.withNewChildren(Seq(Literal(99))).asInstanceOf[DelegateExpression]
+ assert(replaced.definition == Literal(99))
+ assert(replaced.inputs == Seq(ref)) // inputs are metadata, untouched
+ }
+
+ test("references come from the definition, not from inputs") {
+ val a = AttributeReference("a", IntegerType)()
+ val b = AttributeReference("b", IntegerType)()
+ // `b` appears only as display metadata; the real child references `a`
+ val expr = DelegateExpression("f", Seq(b), Add(a, Literal(1)))
+ assert(expr.references == AttributeSet(a))
+ }
+
+ test("sql and prettyName reflect the high-level call") {
+ val expr = DelegateExpression("myfunc", Seq(Literal(1), Literal("x")),
Literal(0))
+ assert(expr.prettyName == "myfunc")
+ assert(expr.sql == "myfunc(1, 'x')")
+ }
+
+ test("DelegateFunction.unapply round-trips apply") {
+ assert(CastFn.unapply(CastFn(Literal("x"))).contains(Seq(Literal("x"))))
+ assert(AnyFn.unapply(Literal(1)).isEmpty)
+ }
+
+ // ---- input-type markers are transient, unevaluable, and transparent in
type ----
+
+ test("input-type markers are Unevaluable and delegate type/nullability to
their child") {
+ val marker = ImplicitCastInput(BoundReference(0, IntegerType, nullable =
true), StringType)
+ assert(marker.dataType == IntegerType) // delegates to child until
coercion casts it
Review Comment:
[P2] Update the marker test for the new fallback type
The latest `InputTypeMarker.dataType` returns
`expectedType.defaultConcreteType` when the child's type check fails. This
marker has an integer child but expects `StringType`, so its data type is now
`StringType`, not `IntegerType`. After removing only the production compile
blocker, the existing `DelegateExpressionSuite` fails here with `StringType did
not equal IntegerType`. Please update the assertion and test description to
match the new contract; a separate matching-type case can retain coverage that
valid markers expose their child's type.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/stringExpressions.scala:
##########
@@ -2690,29 +2690,40 @@ case class Substring(str: Expression, pos: Expression,
len: Expression)
since = "2.3.0",
group = "string_funcs")
// scalastyle:on line.size.limit
-case class Right(str: Expression, len: Expression) extends RuntimeReplaceable
- with ImplicitCastInputTypes with BinaryLike[Expression] {
-
- override lazy val replacement: Expression = If(
- IsNull(str),
- Literal(null, str.dataType),
- If(
- LessThanOrEqual(len, Literal(0)),
- Literal(UTF8String.EMPTY_UTF8, str.dataType),
- new Substring(str, UnaryMinus(len, failOnError = false))
- )
- )
+object Right extends DelegateFunction {
+ override val name: String = "right"
override def inputTypes: Seq[AbstractDataType] =
- Seq(
- StringTypeWithCollation(supportsTrimCollation = true),
- IntegerType
- )
- override def left: Expression = str
- override def right: Expression = len
- override protected def withNewChildrenInternal(
- newLeft: Expression, newRight: Expression): Expression = {
- copy(str = newLeft, len = newRight)
+ Seq(StringTypeWithCollation(supportsTrimCollation = true), IntegerType)
+
+ // At build time `str` is the not-yet-coerced argument (wrapped in an
`ImplicitCastInput` marker
+ // that delegates `dataType` to its child), so `str.dataType` is the *input*
type, which is not
+ // necessarily a string yet -- e.g. `right(12345, 2)` has an `IntegerType`
child the implicit cast
+ // will turn into a string. Use it for the null/empty branch literals only
when it is already a
+ // string-family type, so a CHAR(N)/VARCHAR(N) result (under
+ // `spark.sql.preserveCharVarcharTypeInfo`) or a non-default collation is
preserved through the
+ // `If` branch unification; otherwise fall back to plain `StringType`, the
type the implicit cast
+ // produces. Typing a UTF8String literal with a non-string type would be
invalid.
+ override def lower(args: Seq[Expression]): Expression = {
+ val str = args(0)
+ val len = args(1)
+ val litType = str.dataType match {
+ case _: StringType | _: CharType | _: VarcharType => str.dataType
+ case _ => StringType
+ }
+ // Keep both arguments single-use while the analyzer extracts window
expressions. The length
+ // is bound inside the non-null branch so right's null short-circuit is
preserved.
+ With(str) { case Seq(strRef) =>
+ If(
+ IsNull(strRef),
+ Literal(null, litType),
+ With(len) { case Seq(lenRef) =>
+ If(
+ LessThanOrEqual(lenRef, Literal(0)),
+ Literal(UTF8String.EMPTY_UTF8, litType),
+ new Substring(strRef, UnaryMinus(lenRef, failOnError = false)))
Review Comment:
[P1] Preserve outer references when rewriting the nested With
The inner `With(len)` contains `strRef` from the outer `With`. For a `With`
in a conditional branch, `RewriteWithExpression` builds a map containing only
that inner node's definitions, then replaces every `CommonExpressionRef` using
`refToExpr(ref.id)`. It therefore looks up the outer string reference in a map
containing only the length reference. After fixing the compilation issue,
optimizing `SELECT right('abc', 1) AS r` reproducibly throws
`NoSuchElementException: key not found: CommonExpressionId(0,false)`. Could we
make the substitution scope-aware, or otherwise avoid this cross-scope shape,
while preserving null short-circuiting and single-use window inputs?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]