You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by dt...@apache.org on 2026/08/26 17:36:49 UTC
(spark) branch master updated: [SPARK-59016][SQL] Expand CHAR/VARCHAR leak inventory to nested shapes
This is an automated email from the ASF dual-hosted git repository.
dtenedor pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 9f99c395ef1a [SPARK-59016][SQL] Expand CHAR/VARCHAR leak inventory to nested shapes
9f99c395ef1a is described below
commit 9f99c395ef1a8c89f884e63e072fa0111dccecd0
Author: Serge Rielau <se...@rielau.com>
AuthorDate: Wed Aug 26 10:36:30 2026 -0700
[SPARK-59016][SQL] Expand CHAR/VARCHAR leak inventory to nested shapes
### What changes were proposed in this pull request?
[SPARK-59016](https://issues.apache.org/jira/browse/SPARK-59016) expands the FunctionRegistry CHAR/VARCHAR leak inventory to cover additional arities and nested array, map, and struct shapes.
The allowlist is shape-aware: legitimate collection pass-through calls are distinguished from scalar transforming calls such as `reverse(c)` and `concat(c, ...)`, which must return plain STRING. The original scalar inventory remains under parent [SPARK-58794](https://issues.apache.org/jira/browse/SPARK-58794), while the expanded templates are covered by SPARK-59016.
### Why are the changes needed?
The previous inventory used only seven argument templates, so a CHAR/VARCHAR leak at another arity or nested shape would not fail CI. A function-name-only allowlist could also hide an invalid scalar leak when another shape of the same function is a legitimate pass-through.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
- `build/sbt "sql/testOnly org.apache.spark.sql.BasicCharVarcharTestSuite -- -z inventoried"`
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor Grok 4.6
Closes #58299 from srielau/serge-rielau_data/SPARK-58794-p1.
Authored-by: Serge Rielau <se...@rielau.com>
Signed-off-by: Daniel Tenedorio <da...@databricks.com>
---
.../spark/sql/catalyst/optimizer/expressions.scala | 2 +
.../apache/spark/sql/CharVarcharTestSuite.scala | 82 +++++++++++++++-------
2 files changed, 57 insertions(+), 27 deletions(-)
diff --git a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
index 580c92f6eed5..7e8e818e37b8 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala
@@ -1158,6 +1158,8 @@ object FoldablePropagation extends Rule[LogicalPlan] {
object SimplifyCasts extends Rule[LogicalPlan] {
def apply(plan: LogicalPlan): LogicalPlan = plan.transformAllExpressionsWithPruning(
_.containsPattern(CAST), ruleId) {
+ // Annotated STRING (flag off) is not unconstrained STRING. Dropping CAST(... AS STRING)
+ // would hide the type change. First-class CHAR/VARCHAR already fail e.dataType == StringType.
case c @ Cast(e: NamedExpression, StringType, _, _)
if e.dataType == StringType && e.metadata.contains(CHAR_VARCHAR_TYPE_STRING_METADATA_KEY) => c
case Cast(e, dataType, _, _) if e.dataType == dataType => e
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
index a9e3c0626e0f..7de934ef2af6 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala
@@ -1243,43 +1243,71 @@ class BasicCharVarcharTestSuite extends SharedSparkSession {
}
}
- // Allowlist for the inventory below: pass-through and container cases that may keep
- // CHAR(n)/VARCHAR(n): aggregates/ordering that return an input unchanged, null-handling,
- // element access, array/map/struct constructors, and collection rearrangements that keep
- // element types. Coverage is limited to the seven fixed argumentShapes templates in the test;
- // a leak only at another arity or nested shape would not fail here. For those shapes,
- // anything not listed must reduce to plain STRING.
+ // Pass-through and container functions that may keep CHAR(n)/VARCHAR(n): aggregates
+ // and ordering that return an input unchanged, null-handling, element access,
+ // array/map/struct constructors, and collection rearrangements that keep element types.
+ // Legitimacy is still per shape: reverse(array(c)) may keep CHAR, reverse(c) must not.
private val charVarcharPassThroughFunctions = Set(
"any_value", "approx_top_k", "approx_top_k_accumulate", "array", "array_agg", "array_compact",
"array_distinct", "array_max", "array_min", "array_repeat", "array_sort", "arrays_zip",
"coalesce", "collect_list", "collect_set", "collect_union", "concat", "explode",
- "explode_outer", "first", "first_value", "get", "greatest", "ifnull", "last", "last_value",
- "least", "map", "max", "max_by", "measure", "min", "min_by", "mode", "named_struct", "nullif",
- "nullifzero", "nvl", "reverse", "shuffle", "sort_array", "struct", "trim_array", "when")
+ "explode_outer", "first", "first_value", "flatten", "get", "greatest", "ifnull", "last",
+ "last_value", "least", "map", "map_concat", "map_entries", "map_keys", "map_values", "max",
+ "max_by", "measure", "min", "min_by", "mode", "named_struct", "nullif", "nullifzero", "nvl",
+ "nvl2", "reverse", "shuffle", "sort_array", "struct", "trim_array", "when")
+
+ // String-transforming shapes of otherwise pass-through functions. These must reduce to
+ // unconstrained STRING even though the same function keeps CHAR on collection inputs.
+ private val charVarcharTransformingCalls = Set(
+ "concat(c)", "concat(c, c)", "concat(c, c, c)", "concat(c, 'x')", "concat('x', c)",
+ "reverse(c)")
+
+ private val inventoryScalarShapes = Seq(
+ "%s(c)", "%s(c, c)", "%s(c, 'x')", "%s('x', c)", "%s(c, 1)", "%s(array(c))",
+ "%s(array(c), '-')")
+
+ private val inventoryNestedShapes = Seq(
+ "%s(c, c, c)", "%s(array(array(c)))", "%s(named_struct('x', c))",
+ "%s(map(c, 1))", "%s(map(1, c))")
+
+ private def assertNoInventoriedCharVarcharLeaks(argumentShapes: Seq[String]): Unit = {
+ val (passThroughLeaks, transformingLeaks) =
+ FunctionRegistry.functionSet.map(_.funcName).toSeq.sorted.flatMap { name =>
+ argumentShapes.map(_.format(name)).flatMap { call =>
+ // Most shapes do not typecheck for a given function; those are simply not evidence.
+ val keepsCharVarchar =
+ Try(sql(s"SELECT $call AS r FROM std_inventory").schema.head.dataType)
+ .toOption
+ .exists(CharVarcharUtils.hasCharVarchar)
+ Option.when(keepsCharVarchar &&
+ (!charVarcharPassThroughFunctions.contains(name) ||
+ charVarcharTransformingCalls.contains(call)))((name, call))
+ }
+ }.partition { case (_, call) => !charVarcharTransformingCalls.contains(call) }
- test("SPARK-58794: inventoried shapes do not leak CHAR/VARCHAR under standardSemantics") {
- val argumentShapes = Seq(
- "%s(c)", "%s(c, c)", "%s(c, 'x')", "%s('x', c)", "%s(c, 1)", "%s(array(c))",
- "%s(array(c), '-')")
+ assert(passThroughLeaks.isEmpty,
+ "these inventoried calls returned a CHAR/VARCHAR type; if they legitimately pass through " +
+ "their input type, add the function name to charVarcharPassThroughFunctions: " +
+ passThroughLeaks.map(_._2).mkString(", "))
+ assert(transformingLeaks.isEmpty,
+ "these transforming calls returned a CHAR/VARCHAR type; fix the expression to return " +
+ "plain STRING: " + transformingLeaks.map(_._2).mkString(", "))
+ }
+ test("SPARK-58794: inventoried shapes do not leak CHAR/VARCHAR under standardSemantics") {
withTable("std_inventory") {
sql("CREATE TABLE std_inventory (c CHAR(5)) USING parquet")
withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
- val leaks = FunctionRegistry.functionSet.map(_.funcName).toSeq.sorted
- .filterNot(charVarcharPassThroughFunctions.contains)
- .flatMap { name =>
- argumentShapes.map(_.format(name)).filter { call =>
- // Most shapes do not typecheck for a given function; those are simply not evidence.
- Try(sql(s"SELECT $call AS r FROM std_inventory").schema.head.dataType)
- .toOption
- .exists(CharVarcharUtils.hasCharVarchar)
- }
- }
+ assertNoInventoriedCharVarcharLeaks(inventoryScalarShapes)
+ }
+ }
+ }
- assert(leaks.isEmpty,
- "these inventoried calls returned a CHAR/VARCHAR type; either fix the expression to " +
- "return plain STRING or add it to charVarcharPassThroughFunctions: " +
- leaks.mkString(", "))
+ test("SPARK-59016: nested inventoried shapes do not leak CHAR/VARCHAR") {
+ withTable("std_inventory") {
+ sql("CREATE TABLE std_inventory (c CHAR(5)) USING parquet")
+ withSQLConf(SQLConf.CHAR_VARCHAR_STANDARD_SEMANTICS.key -> "true") {
+ assertNoInventoriedCharVarcharLeaks(inventoryNestedShapes)
}
}
}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org