viirya commented on code in PR #58978:
URL: https://github.com/apache/spark/pull/58978#discussion_r4088322208


##########
dev/spark-test-image/python-312/Dockerfile:
##########
@@ -44,6 +44,7 @@ RUN apt-get update && apt-get install -y \
     libssl-dev \
     openjdk-17-jdk-headless \
     python3.12 \
+    python3.12-dev \

Review Comment:
   The headers are needed when building JEP's JNI library from source, as this 
image does. They are not required to run an already-built compatible JEP 
installation; the matching Python shared library is still needed at runtime. 
I've clarified this distinction in the dependency documentation.



##########
dev/spark-test-image/python-312/Dockerfile:
##########
@@ -66,3 +67,10 @@ RUN python3.12 -m pip install -U pip
 RUN python3.12 -m pip install --group ml_torch --index-url 
https://download.pytorch.org/whl/cpu && \
     python3.12 -m pip install --group ci_classic_standard --group 
ci_connect_standard && \
     python3.12 -m pip cache purge
+
+# Exercise in-process UDFs in the existing pyspark-sql job. JEP needs Python
+# headers and a JDK when building its JNI library; no separate test job is 
needed.
+RUN JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")" 
\
+    python3.12 -m pip install --no-cache-dir "jep==4.3.1" cffi

Review Comment:
   Updated to 4.3.2 in the POM, test image, and documentation/benchmark setup 
instructions. The local integration suite also passed with JEP 4.3.2.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/ExtractInProcessPythonUDFs.scala:
##########
@@ -0,0 +1,101 @@
+/*
+ * 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.execution.python
+
+import scala.collection.mutable
+
+import org.apache.spark.sql.catalyst.expressions.{Attribute, 
AttributeReference, Expression, ExternalUserDefinedFunction, NamedExpression, 
PythonUDF, WindowExpression}
+import org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression
+import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}
+import org.apache.spark.sql.catalyst.rules.Rule
+
+/**
+ * Extracts [[InProcessPythonUDF]] expressions from logical plan nodes, 
rewriting the plan
+ * so each batch of UDFs is evaluated in a dedicated [[InProcessEvalPython]] 
node.
+ *
+ * Simpler than [[ExtractPythonUDFs]]: in-process UDFs are scalar and don't 
support
+ * iterator mode, aggregate mode, or nested chaining in Phase 1.
+ *
+ * Example rewrite:
+ *   Project [double(a), triple(b)]
+ *     Scan
+ *   becomes:
+ *   Project [inprocessUDF0, inprocessUDF1]
+ *     InProcessEvalPython [double(a), triple(b)] to [inprocessUDF0, 
inprocessUDF1]
+ *       Scan
+ */
+object ExtractInProcessPythonUDFs extends Rule[LogicalPlan] {
+
+  private def hasInProcessUDF(e: Expression): Boolean =
+    e.exists(_.isInstanceOf[InProcessPythonUDF])
+
+  override def apply(plan: LogicalPlan): LogicalPlan = plan.transformUp {
+    // Already extracted - skip to avoid double-wrapping
+    case p: InProcessEvalPython => p
+
+    case node: LogicalPlan if node.expressions.exists(hasInProcessUDF) =>
+      extract(node)
+  }
+
+  private def extract(plan: LogicalPlan): LogicalPlan = {
+    // Collect all distinct InProcessPythonUDFs from this plan's expressions
+    val udfs = plan.expressions
+      .flatMap(_.collect { case u: InProcessPythonUDF => u })
+      .distinct
+
+    if (udfs.isEmpty) return plan
+
+    udfs.foreach { udf =>
+      require(!udf.children.exists(_.exists {
+        case _: InProcessPythonUDF | _: PythonUDF | _: 
ExternalUserDefinedFunction |
+            _: AggregateExpression | _: WindowExpression => true
+        case _ => false
+      }), "In-process Python UDFs do not support nested UDF, aggregate or 
window arguments")
+    }
+
+    // Map each UDF to a fresh AttributeReference that will hold its result
+    val attributeMap = mutable.LinkedHashMap[InProcessPythonUDF, 
NamedExpression]()
+
+    // For each child plan, find UDFs whose inputs are fully satisfied by that 
child
+    val newChildren = plan.children.map { child =>
+      val validUdfs = udfs.filter { udf =>

Review Comment:
   Removed the custom extraction rule and switched to a regular `PythonUDF` 
with the new in-process eval type. `ExtractPythonUDFFromAggregate` now handles 
these expressions and places their evaluation above the aggregate. Added 
regression tests for UDFs over grouping keys, aggregate results, and constants.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/ExtractInProcessPythonUDFs.scala:
##########
@@ -0,0 +1,101 @@
+/*
+ * 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.execution.python
+
+import scala.collection.mutable
+
+import org.apache.spark.sql.catalyst.expressions.{Attribute, 
AttributeReference, Expression, ExternalUserDefinedFunction, NamedExpression, 
PythonUDF, WindowExpression}
+import org.apache.spark.sql.catalyst.expressions.aggregate.AggregateExpression
+import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, Project}
+import org.apache.spark.sql.catalyst.rules.Rule
+
+/**
+ * Extracts [[InProcessPythonUDF]] expressions from logical plan nodes, 
rewriting the plan
+ * so each batch of UDFs is evaluated in a dedicated [[InProcessEvalPython]] 
node.
+ *
+ * Simpler than [[ExtractPythonUDFs]]: in-process UDFs are scalar and don't 
support
+ * iterator mode, aggregate mode, or nested chaining in Phase 1.
+ *
+ * Example rewrite:
+ *   Project [double(a), triple(b)]
+ *     Scan
+ *   becomes:
+ *   Project [inprocessUDF0, inprocessUDF1]
+ *     InProcessEvalPython [double(a), triple(b)] to [inprocessUDF0, 
inprocessUDF1]
+ *       Scan
+ */
+object ExtractInProcessPythonUDFs extends Rule[LogicalPlan] {
+
+  private def hasInProcessUDF(e: Expression): Boolean =
+    e.exists(_.isInstanceOf[InProcessPythonUDF])
+
+  override def apply(plan: LogicalPlan): LogicalPlan = plan.transformUp {
+    // Already extracted - skip to avoid double-wrapping
+    case p: InProcessEvalPython => p
+
+    case node: LogicalPlan if node.expressions.exists(hasInProcessUDF) =>
+      extract(node)
+  }
+
+  private def extract(plan: LogicalPlan): LogicalPlan = {
+    // Collect all distinct InProcessPythonUDFs from this plan's expressions
+    val udfs = plan.expressions
+      .flatMap(_.collect { case u: InProcessPythonUDF => u })
+      .distinct
+
+    if (udfs.isEmpty) return plan
+
+    udfs.foreach { udf =>
+      require(!udf.children.exists(_.exists {

Review Comment:
   These now go through the existing Python UDF extraction rules. Aggregate 
arguments are handled by `ExtractPythonUDFFromAggregate`, and dependent 
in-process calls are extracted into separate evaluation nodes. Added coverage 
for collapsed projects, UDFs over aggregate results, nested calls, and mixed 
worker/in-process UDFs. Removed the separate-stage workaround from the docs.



##########
python/pyspark/inprocess/runtime.py:
##########
@@ -0,0 +1,96 @@
+#
+# 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.
+#
+
+
+"""
+In-process Python UDF runtime entry point.
+
+``_inprocess_invoke`` is imported into the jep SharedInterpreter's global 
namespace
+during executor initialization (see ``InProcessPythonRuntime.initialize()``), 
then called
+directly from the JVM via ``interp.invoke("_inprocess_invoke", ...)``.
+
+Both input and output use the Arrow C Data Interface (CDI). The JVM 
pre-allocates
+ArrowArray/ArrowSchema C structs for every input column and for the output, 
passing
+their native addresses as Python ints. Input arrays are reconstructed via
+``pa.Array._import_from_c`` (zero-copy). The output is written via 
``arr._export_to_c``
+into the JVM-owned structs (zero-copy).
+
+jep type conversions (Java -> Python):
+    byte[]                -> bytes (or sequence of signed ints; masked to 
unsigned below)
+    List<Long> (boxed)    -> list of Python ints
+    Long                  -> int
+"""
+
+import traceback as _traceback
+from functools import lru_cache
+
+import pyarrow as pa
+
+from pyspark import cloudpickle
+from pyspark.sql.pandas.types import to_arrow_type
+from pyspark.sql.types import _parse_datatype_json_string
+
+_UDF_TRACEBACK_SENTINEL = "__INPROCESS_UDF_TRACEBACK__:"
+
+
+@lru_cache(maxsize=128)
+def _load_udf(serialized_udf: bytes, return_type_json: str, timezone: str):
+    return (
+        cloudpickle.loads(serialized_udf),
+        to_arrow_type(_parse_datatype_json_string(return_type_json), 
timezone=timezone),
+    )
+
+
+def _validate_result(result, expected_rows: int, expected_type: pa.DataType) 
-> None:
+    if not isinstance(result, pa.Array):
+        raise TypeError(f"In-process UDF must return a pyarrow.Array, got 
{type(result).__name__}")
+    if len(result) != expected_rows:
+        raise ValueError(f"In-process UDF returned {len(result)} rows; 
expected {expected_rows}")
+    if result.type != expected_type:
+        raise TypeError(f"In-process UDF returned {result.type}; expected 
{expected_type}")
+    result.validate()
+
+
+def _inprocess_invoke(
+    serialized_udf,
+    input_array_ptrs,
+    input_schema_ptrs,
+    output_array_ptr: int,
+    output_schema_ptr: int,
+    expected_rows: int,
+    return_type_json: str,
+    timezone: str,
+) -> None:
+    """Consume input CDI structs and export a validated, row-preserving result.
+
+    The caller owns the struct memory and releases any unconsumed exports on 
failure.
+    Imported input arrays and exported output buffers follow Arrow's release 
callbacks.
+    """
+    udf_key = bytes(b & 0xFF for b in serialized_udf)
+    udf_func, expected_type = _load_udf(udf_key, return_type_json, timezone)
+    if len(input_array_ptrs) != len(input_schema_ptrs):
+        raise ValueError("Mismatched input ArrowArray and ArrowSchema pointer 
counts")
+    input_arrays = [
+        pa.Array._import_from_c(int(ap), int(sp))
+        for ap, sp in zip(input_array_ptrs, input_schema_ptrs)
+    ]
+    try:
+        result = udf_func(*input_arrays)
+        _validate_result(result, int(expected_rows), expected_type)
+        result._export_to_c(int(output_array_ptr), int(output_schema_ptr))

Review Comment:
   Fixed by checking for offsets recursively, including nested children, and 
materializing affected results with `pa.concat_arrays([result])` before CDI 
export. Results without offsets keep the existing transfer path. Added runtime 
and JVM integration coverage for slices, including validity bits, strings, and 
nested child offsets.



##########
python/pyspark/inprocess/runtime.py:
##########
@@ -0,0 +1,96 @@
+#
+# 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.
+#
+
+
+"""
+In-process Python UDF runtime entry point.
+
+``_inprocess_invoke`` is imported into the jep SharedInterpreter's global 
namespace
+during executor initialization (see ``InProcessPythonRuntime.initialize()``), 
then called
+directly from the JVM via ``interp.invoke("_inprocess_invoke", ...)``.
+
+Both input and output use the Arrow C Data Interface (CDI). The JVM 
pre-allocates
+ArrowArray/ArrowSchema C structs for every input column and for the output, 
passing
+their native addresses as Python ints. Input arrays are reconstructed via
+``pa.Array._import_from_c`` (zero-copy). The output is written via 
``arr._export_to_c``
+into the JVM-owned structs (zero-copy).
+
+jep type conversions (Java -> Python):
+    byte[]                -> bytes (or sequence of signed ints; masked to 
unsigned below)
+    List<Long> (boxed)    -> list of Python ints
+    Long                  -> int
+"""
+
+import traceback as _traceback
+from functools import lru_cache
+
+import pyarrow as pa
+
+from pyspark import cloudpickle
+from pyspark.sql.pandas.types import to_arrow_type
+from pyspark.sql.types import _parse_datatype_json_string
+
+_UDF_TRACEBACK_SENTINEL = "__INPROCESS_UDF_TRACEBACK__:"
+
+
+@lru_cache(maxsize=128)
+def _load_udf(serialized_udf: bytes, return_type_json: str, timezone: str):
+    return (
+        cloudpickle.loads(serialized_udf),
+        to_arrow_type(_parse_datatype_json_string(return_type_json), 
timezone=timezone),
+    )
+
+
+def _validate_result(result, expected_rows: int, expected_type: pa.DataType) 
-> None:
+    if not isinstance(result, pa.Array):
+        raise TypeError(f"In-process UDF must return a pyarrow.Array, got 
{type(result).__name__}")
+    if len(result) != expected_rows:
+        raise ValueError(f"In-process UDF returned {len(result)} rows; 
expected {expected_rows}")
+    if result.type != expected_type:
+        raise TypeError(f"In-process UDF returned {result.type}; expected 
{expected_type}")
+    result.validate()
+
+
+def _inprocess_invoke(
+    serialized_udf,
+    input_array_ptrs,
+    input_schema_ptrs,
+    output_array_ptr: int,
+    output_schema_ptr: int,
+    expected_rows: int,
+    return_type_json: str,
+    timezone: str,
+) -> None:
+    """Consume input CDI structs and export a validated, row-preserving result.
+
+    The caller owns the struct memory and releases any unconsumed exports on 
failure.
+    Imported input arrays and exported output buffers follow Arrow's release 
callbacks.
+    """
+    udf_key = bytes(b & 0xFF for b in serialized_udf)
+    udf_func, expected_type = _load_udf(udf_key, return_type_json, timezone)
+    if len(input_array_ptrs) != len(input_schema_ptrs):
+        raise ValueError("Mismatched input ArrowArray and ArrowSchema pointer 
counts")
+    input_arrays = [
+        pa.Array._import_from_c(int(ap), int(sp))
+        for ap, sp in zip(input_array_ptrs, input_schema_ptrs)
+    ]
+    try:
+        result = udf_func(*input_arrays)
+        _validate_result(result, int(expected_rows), expected_type)
+        result._export_to_c(int(output_array_ptr), int(output_schema_ptr))
+    except Exception:

Review Comment:
   Both function registration/deserialization and invocation now catch 
`BaseException` and convert it to a `RuntimeError` carrying the Python 
traceback. This keeps `SystemExit` from reaching JEP's exception handler. Added 
tests for `SystemExit` during invocation and deserialization. This exception 
handling does not cover native process termination such as `os._exit()`.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/InProcessPythonUDF.scala:
##########
@@ -0,0 +1,84 @@
+/*
+ * 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.execution.python
+
+import org.apache.spark.sql.catalyst.expressions.{
+  Attribute, AttributeReference, AttributeSet, Expression, ExprId, 
NamedExpression, Unevaluable
+}
+import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, UnaryNode}
+import org.apache.spark.sql.types.DataType
+
+/**
+ * A cloudpickle-serialized Python function to be executed in-process via jep
+ * (Java Embedded Python).
+ *
+ * Distinct from [[org.apache.spark.sql.catalyst.expressions.PythonUDF]] which 
uses an
+ * out-of-process Python worker connected via socket.
+ *
+ * Evaluated by [[InProcessArrowEvalExec]], which passes Arrow column buffers 
to CPython
+ * as PyArrow arrays via native memory addresses (zero-copy input), then 
imports the
+ * PyArrow result buffers through CDI without copying.
+ *
+ * @param name            display name for plan explain output
+ * @param serializedFunc  cloudpickle-serialized Python function bytes
+ * @param children        input column expressions
+ * @param dataType        declared return type (validated against the Arrow 
result)
+ * @param udfDeterministic whether the UDF is deterministic
+ * @param resultId        unique identifier for this UDF result
+ */
+case class InProcessPythonUDF(
+    name: String,
+    serializedFunc: Array[Byte],
+    children: Seq[Expression],
+    dataType: DataType,
+    udfDeterministic: Boolean = true,
+    resultId: ExprId = NamedExpression.newExprId)
+  extends Expression with Unevaluable {

Review Comment:
   I replaced the custom expression with `PythonUDF` itself. It now matches 
both the existing `isInstanceOf[PythonUDF]` guards and the `PYTHON_UDF` tree 
pattern, including the runtime-filter guard. Added an integration regression 
for partition filters to check that the UDF is not evaluated on the driver.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/InProcessArrowEvalExec.scala:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.execution.python
+
+import scala.collection.mutable.ArrayBuffer
+import scala.jdk.CollectionConverters._
+
+import org.apache.arrow.c.{ArrowArray, ArrowSchema}
+import org.apache.arrow.util.AutoCloseables
+import org.apache.arrow.vector.VectorSchemaRoot
+
+import org.apache.spark.TaskContext
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, 
UnsafeProjection}
+import org.apache.spark.sql.execution.{SparkPlan, UnaryExecNode}
+import org.apache.spark.sql.execution.arrow.ArrowWriter
+import org.apache.spark.sql.types.{StructField, StructType}
+import org.apache.spark.sql.util.ArrowUtils
+import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, 
ColumnVector}
+import org.apache.spark.util.Utils
+
+/**
+ * Evaluates scalar Python UDFs using Arrow CDI in the executor process. Rows 
(including
+ * computed UDF arguments) are written to Arrow once, then input and output 
buffers cross
+ * the JVM/Python boundary without IPC serialization. The runtime owns the JEP 
thread.
+ */
+case class InProcessArrowEvalExec(
+    udfs: Seq[InProcessPythonUDF],
+    resultAttrs: Seq[Attribute],
+    child: SparkPlan) extends UnaryExecNode {
+
+  override def output: Seq[Attribute] = child.output ++ resultAttrs
+
+  override protected def doExecute(): RDD[InternalRow] = {
+    val expressions = ArrayBuffer[Expression](child.output: _*)
+    val inputOrdinals = udfs.map { udf =>
+      udf.children.map {
+        case attr: Attribute if child.output.exists(_.exprId == attr.exprId) =>
+          child.output.indexWhere(_.exprId == attr.exprId)
+        case expr =>
+          expressions += expr
+          expressions.size - 1
+      }
+    }
+    // Synthetic names also allow joins with duplicate output column names.
+    val inputSchema = StructType(expressions.zipWithIndex.map { case (expr, i) 
=>
+      StructField(s"_input$i", expr.dataType, expr.nullable)
+    }.toSeq)
+    val inputExpressions = expressions.toSeq
+    val childOutput = child.output
+    val resultOutput = output
+    val batchSize = conf.arrowMaxRecordsPerBatch
+    val maxBytes = conf.arrowMaxBytesPerBatch
+    val timeZoneId = conf.sessionLocalTimeZone
+
+    child.execute().mapPartitions { rows =>
+      val context = Option(TaskContext.get())
+      def checkCancellation(): Unit = 
context.foreach(_.killTaskIfInterrupted())
+
+      val resultProjection = UnsafeProjection.create(resultOutput, 
resultOutput)
+      val projectInput: InternalRow => InternalRow =
+        if (inputExpressions.size == childOutput.size) {
+          identity[InternalRow]
+        } else {
+          val projection = UnsafeProjection.create(inputExpressions, 
childOutput)
+          projection.initialize(TaskContext.getPartitionId())
+          projection
+        }
+      val root = VectorSchemaRoot.create(
+        ArrowUtils.toArrowSchema(inputSchema, timeZoneId, false), 
ArrowUtils.rootAllocator)

Review Comment:
   Only UDF arguments are converted to Arrow now, so unrelated pass-through 
structs retain their original Spark representation. For actual UDF arguments, 
`failDuplicatedFieldNames` runs before Arrow schema/vector creation; duplicate 
names in declared results are rejected during registration as well. Added tests 
for a pass-through struct with duplicate names and rejection when such a struct 
is a UDF argument.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/InProcessArrowEvalExec.scala:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.execution.python
+
+import scala.collection.mutable.ArrayBuffer
+import scala.jdk.CollectionConverters._
+
+import org.apache.arrow.c.{ArrowArray, ArrowSchema}
+import org.apache.arrow.util.AutoCloseables
+import org.apache.arrow.vector.VectorSchemaRoot
+
+import org.apache.spark.TaskContext
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, 
UnsafeProjection}
+import org.apache.spark.sql.execution.{SparkPlan, UnaryExecNode}
+import org.apache.spark.sql.execution.arrow.ArrowWriter
+import org.apache.spark.sql.types.{StructField, StructType}
+import org.apache.spark.sql.util.ArrowUtils
+import org.apache.spark.sql.vectorized.{ArrowColumnVector, ColumnarBatch, 
ColumnVector}
+import org.apache.spark.util.Utils
+
+/**
+ * Evaluates scalar Python UDFs using Arrow CDI in the executor process. Rows 
(including
+ * computed UDF arguments) are written to Arrow once, then input and output 
buffers cross
+ * the JVM/Python boundary without IPC serialization. The runtime owns the JEP 
thread.
+ */
+case class InProcessArrowEvalExec(
+    udfs: Seq[InProcessPythonUDF],
+    resultAttrs: Seq[Attribute],
+    child: SparkPlan) extends UnaryExecNode {
+
+  override def output: Seq[Attribute] = child.output ++ resultAttrs
+
+  override protected def doExecute(): RDD[InternalRow] = {
+    val expressions = ArrayBuffer[Expression](child.output: _*)

Review Comment:
   Changed the operator to project only the UDF arguments into Arrow and keep 
the original rows in a spillable `HybridRowQueue`, then join the results back 
to those rows. This removes the Arrow conversion of unrelated columns. Added a 
regression with a pass-through calendar interval whose microseconds would 
overflow the Arrow nanosecond conversion, as well as the duplicate-field 
pass-through case.



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