cloud-fan commented on code in PR #58549:
URL: https://github.com/apache/spark/pull/58549#discussion_r4113743112


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/ColumnarArrowEvalPythonEvaluatorFactory.scala:
##########
@@ -189,12 +216,64 @@ private[python] class 
ColumnarArrowEvalPythonEvaluatorFactory(
         columnIndices: Array[Int]): Iterator[ColumnarBatch] = {
 
       val passThruQueue =
-        new ArrayDeque[(Array[ColumnVector], Int)]()
+        new ConcurrentLinkedQueue[(Array[ColumnVector], Int)]()
+      val passThruClosed = new AtomicBoolean(false)
+
+      def closeCols(cols: Array[ColumnVector]): Unit = {
+        var i = 0
+        while (i < cols.length) {
+          cols(i).close()
+          i += 1
+        }
+      }
+
+      def drainPassThru(): Unit = {
+        passThruClosed.set(true)
+        var entry = passThruQueue.poll()
+        while (entry != null) {
+          closeCols(entry._1)
+          entry = passThruQueue.poll()
+        }
+      }
+
+      context.addTaskCompletionListener[Unit] { _ =>

Review Comment:
   **Non-blocking (P2):** The added tests consume one input batch completely, 
so they never exercise repeated queue pairing or task completion while 
transferred vectors remain queued. A multi-batch case plus an early-termination 
case is needed to make ordering errors and completion-time native-vector leaks 
regression-sensitive.
   
   **Verification:**
   - **Regression:** Drive at least two Arrow input batches in one task and 
assert pass-through values align with every Python result batch.
   - **Regression:** Terminate consumption with queued batches outstanding and 
assert task completion returns native allocation to baseline.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/ColumnarArrowEvalPythonEvaluatorFactory.scala:
##########
@@ -189,12 +216,64 @@ private[python] class 
ColumnarArrowEvalPythonEvaluatorFactory(
         columnIndices: Array[Int]): Iterator[ColumnarBatch] = {
 
       val passThruQueue =
-        new ArrayDeque[(Array[ColumnVector], Int)]()
+        new ConcurrentLinkedQueue[(Array[ColumnVector], Int)]()
+      val passThruClosed = new AtomicBoolean(false)
+
+      def closeCols(cols: Array[ColumnVector]): Unit = {
+        var i = 0
+        while (i < cols.length) {
+          cols(i).close()
+          i += 1
+        }
+      }
+
+      def drainPassThru(): Unit = {
+        passThruClosed.set(true)
+        var entry = passThruQueue.poll()
+        while (entry != null) {
+          closeCols(entry._1)
+          entry = passThruQueue.poll()
+        }
+      }
+
+      context.addTaskCompletionListener[Unit] { _ =>
+        drainPassThru()
+      }
 
       val bufferedIter = inputIter.map { batch =>
-        val passThruCols = childOutput.indices.map(
-          i => batch.column(i)).toArray
-        passThruQueue.add((passThruCols, batch.numRows()))
+        // The input reader owns and may close its vectors as soon as the 
Python runner consumes
+        // the input iterator. Create independent vector views whose buffers 
remain valid until
+        // the corresponding output batch is closed.
+        val cols = Array.ofDim[ColumnVector](childOutput.length)
+        var allocated = 0
+        try {
+          var i = 0
+          while (i < childOutput.length) {
+            val vector = 
batch.column(i).asInstanceOf[ArrowColumnVector].getValueVector
+            val transferPair = vector.getTransferPair(ArrowUtils.rootAllocator)
+            transferPair.splitAndTransfer(0, batch.numRows())

Review Comment:
   **Non-blocking (P2):** `getTo` has already allocated the destination vector 
here, but the catch can close it only after the later wrapper assignment 
increments `allocated`. If transfer or wrapping throws, this current target 
leaks off-heap memory; establish cleanup ownership immediately after acquiring 
the target and release it on either failure.
   
   **Verification:**
   - **Regression:** Inject a failure after target allocation and verify all 
acquired Arrow buffers return to the allocator baseline.



##########
python/pyspark/sql/tests/connect/test_parity_udf.py:
##########
@@ -46,6 +46,10 @@ def test_udf_with_input_file_name_for_hadooprdd(self):
     def test_same_accumulator_in_udfs(self):
         super().test_same_accumulator_in_udfs()
 
+    @unittest.skip("Spark Connect resolves both UDFs when the plan is 
submitted.")

Review Comment:
   **Non-blocking (P2):** This skip exposes a supported Connect correctness 
gap: the plan carries the logical result type but not the expression-local 
CHAR/VARCHAR check policy. When expressions created under different policies 
are submitted together, the server rebuilds both under the final SQLConf, which 
can drop CHAR padding or VARCHAR overflow validation for an individual 
expression.
   
   **Recommended change:** Serialize captured CHAR/VARCHAR checked-result state 
with each Connect PythonUDF and consume it during server reconstruction.
   
   **Why this works:** Resolve the policy when the Connect expression is 
created, encode the checked logical result (or an equivalent explicit policy), 
and have the server build PythonUDF from that state instead of recomputing it 
from submission-time SQLConf.
   
   **Scope:** Extend the Connect UDF representation, Python plan serialization, 
JVM reconstruction, and mixed-policy parity coverage.
   
   **Compatibility:** Pure legacy-as-string expressions remain unconstrained, 
while standard and preserve-type-info expressions remain checked.
   
   **Risks:** Wire compatibility must preserve behavior for older clients that 
omit the new state.
   
   **Constraints:** Do not change non-Connect Classic capture timing or 
grouped/non-scalar capability rules.
   
   **Success:** Each Connect scalar UDF retains its construction-time 
CHAR/VARCHAR assignment policy. Checked and unchecked UDFs submitted in one 
plan produce the same result as Classic.
   
   **Verification:**
   - **Regression:** Construct checked CHAR and unchecked VARCHAR expressions 
under opposite policies, submit them together after a configuration change, and 
assert padding and overflow behavior remain expression-local.
   - **Compatibility:** Verify a client that omits the new optional state 
retains the established default reconstruction behavior.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/UserDefinedPythonFunction.scala:
##########
@@ -86,6 +87,9 @@ case class UserDefinedPythonFunction(
     val optionInputTypes: List[List[String]] =
       transpiledInputTypes.asScala.map(_.asScala.toList).toList
 
+    if (CharVarcharUtils.hasCharVarcharInUDT(dataType)) {

Review Comment:
   **Non-blocking (P2):** This authoritative JVM boundary only rejects 
CHAR/VARCHAR hidden in UDT storage before grouped aggregates branch into 
`PythonUDAF`. A direct JVM or Connect caller can still supply a logical 
CHAR/VARCHAR aggregate result and bypass the Python client fence, producing 
unchecked output instead of the promised unsupported-return error.
   
   **Verification:**
   - **Regression:** Construct each grouped aggregate eval type through the 
authoritative JVM boundary with CHAR/VARCHAR output and assert the classed 
unsupported-return error.
   - **Compatibility:** Verify scalar row, Arrow, and pandas eval types 
continue to accept and check supported CHAR/VARCHAR output.



##########
python/pyspark/sql/types.py:
##########
@@ -2877,8 +2877,38 @@ def _has_type(dt: DataType, dts: Union[type, Tuple[type, 
...]]) -> bool:
         return _has_type(dt.elementType, dts)
     elif isinstance(dt, MapType):
         return _has_type(dt.keyType, dts) or _has_type(dt.valueType, dts)
+    else:

Review Comment:
   Fixed in 69ef259f915. `_has_type` again descends through UDT storage, while 
logical CHAR/VARCHAR capability checks continue to use `_has_logical_type`.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:4043362978","thread_id":"inline:4043362978","verdict_sha256":"346739c2117e6dac2e9b2e51f34743d7c2bf72cb63a4aa89bf3f0b6bb75b6578"}
 -->



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/UserDefinedPythonFunction.scala:
##########
@@ -106,9 +110,32 @@ case class UserDefinedPythonFunction(
           throw 
QueryCompilationErrors.invalidIncrementalPythonAggregatorBufferError(
             name, bufferType)
       }
+      if (CharVarcharUtils.physicalTypeHasCharVarchar(bufferStruct)) {
+        throw 
QueryCompilationErrors.invalidPythonAggregatorBufferType(bufferStruct)
+      }
       PythonAggregate(name, func, dataType, e, udfDeterministic, bufferStruct)
     } else {
-      PythonUDF(name, func, dataType, e, pythonEvalType, udfDeterministic)
+      val conf = SQLConf.get
+      val resolvedDataType = if (conf.charVarcharFirstClassTypes) {

Review Comment:
   **Non-blocking (P2):** The policy matrix does not cover preservation enabled 
together with standard semantics disabled and legacy-as-string enabled. That 
interaction is the precedence case in this branch; without a scalar-UDF and 
explicit-schema Arrow assertion, a regression can silently lower the schema to 
STRING or remove padding and overflow checks while every adjacent mode test 
stays green.
   
   **Verification:**
   - **Regression:** Disable standard semantics, enable legacy-as-string and 
preservation together, then assert first-class schema exposure, padding, 
overflow rejection, and checked Arrow routing.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/EvaluatePython.scala:
##########
@@ -267,19 +314,27 @@ object EvaluatePython {
       }
 
     case MapType(keyType, valueType, _) =>
-      val keyFromJava = makeFromJava(keyType)
-      val valueFromJava = makeFromJava(valueType)
+      val keyFromJava = makeFromJava(keyType, applyCharVarcharChecks)
+      val valueFromJava = makeFromJava(valueType, applyCharVarcharChecks)
 
       (obj: Any) => nullSafeConvert(obj) {
         case javaMap: java.util.Map[_, _] =>
-          ArrayBasedMapData(
-            javaMap,
-            (key: Any) => keyFromJava(key),
-            (value: Any) => valueFromJava(value))
+          if (applyCharVarcharChecks && 
CharVarcharUtils.hasCharVarchar(keyType)) {

Review Comment:
   **Non-blocking (P2):** `keyType` and `applyCharVarcharChecks` are fixed when 
this converter is built, but `hasCharVarchar(keyType)` recursively rescans the 
schema for every converted map. Compute the combined branch condition once 
outside the returned closure so row conversion does not repeat invariant schema 
work.
   
   **Verification:**
   - **Behavior:** Exercise checked CHAR keys, unconstrained keys, 
duplicate-after-padding keys, and repeated row conversion with identical 
results and one schema decision.



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