srielau commented on code in PR #58299:
URL: https://github.com/apache/spark/pull/58299#discussion_r3863795384


##########
sql/core/src/test/scala/org/apache/spark/sql/CharVarcharTestSuite.scala:
##########
@@ -1243,43 +1243,70 @@ 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")
 
-  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), '-')")
+  // 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 inventoriedCharVarcharLeaks(argumentShapes: Seq[String]): 
Seq[String] = {
+    FunctionRegistry.functionSet.map(_.funcName).toSeq.sorted.flatMap { name =>
+      argumentShapes.map(_.format(name)).filter { 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)
+        keepsCharVarchar &&
+          (!charVarcharPassThroughFunctions.contains(name) ||
+            charVarcharTransformingCalls.contains(call))
+      }
+    }
+  }
+
+  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)
-            }
-          }
+        val leaks = inventoriedCharVarcharLeaks(inventoryScalarShapes)
+        assert(leaks.isEmpty,
+          "these inventoried calls returned a CHAR/VARCHAR type; either fix 
the expression to " +
+            "return plain STRING, add the function to 
charVarcharPassThroughFunctions, or " +
+            "remove it from charVarcharTransformingCalls: " + 
leaks.mkString(", "))

Review Comment:
   If the failing call is in `charVarcharTransformingCalls`, the only correct 
fix is to make the expression return STRING. Removing it from that set while 
the function stays in `charVarcharPassThroughFunctions` would hide a scalar 
leak (`concat(c)` / `reverse(c)`).
   
   Suggest splitting the message: pass-through names vs transforming calls.



-- 
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]

Reply via email to