cloud-fan commented on code in PR #58549:
URL: https://github.com/apache/spark/pull/58549#discussion_r3944526360
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/python/EvaluatePython.scala:
##########
@@ -207,6 +207,18 @@ object EvaluatePython {
case c: Int => c.toLong
}
+ case c: CharType => (obj: Any) => nullSafeConvert(obj) {
+ case _ =>
+ CharVarcharCodegenUtils.charTypeWriteSideCheck(
Review Comment:
**Blocking (P1):** These new CHAR/VARCHAR branches bypass
`CharVarcharUtils.shouldApplyWriteSideLengthCheck`. With
`spark.sql.legacy.charVarcharAsString=true` and both first-class modes off, a
`CharType(3)` UDF returning `"a"` is now padded and an over-length
`VarcharType` result now fails, although that mode explicitly promises no
padding or length check. Please gate this conversion and the new scalar output
projection with the existing helper so the legacy path remains unconstrained
STRING while first-class modes retain the checks.
**Recommended change:** Use the existing write-side-check policy for every
new Python scalar conversion and output projection.
**Why this works:** Evaluate shouldApplyWriteSideLengthCheck once for the
active SQLConf and select the unconstrained STRING conversion/projection only
when legacy-as-string is active without either first-class mode.
**Scope:** EvaluatePython.makeFromJava, EvalPythonEvaluatorFactory, and
focused legacy-mode regression tests for explicit-schema creation and scalar
UDF output.
**Compatibility:** Restores the documented legacy behavior without changing
standard-semantics or preserve-type-info behavior.
**Risks:** The bypass must remain limited to the exact policy helper result
so neither first-class mode loses assignment checks.
**Constraints:** Reuse CharVarcharUtils.shouldApplyWriteSideLengthCheck
rather than duplicating configuration precedence.
**Success:** Legacy mode preserves unpadded and over-length STRING values,
while both first-class modes still pad CHAR and reject over-length CHAR/VARCHAR
values.
##########
python/pyspark/sql/tests/arrow/test_arrow_python_udf.py:
##########
@@ -270,18 +272,53 @@ def f(v: float):
rounded = df.select(f("v").alias("d")).first().d
self.assertEqual(rounded, Decimal("1.233999999999999986"))
- def test_err_return_type(self):
- with self.assertRaises(PySparkNotImplementedError) as pe:
- udf(lambda x: x, VarcharType(10), useArrow=True)
-
- self.check_error(
- exception=pe.exception,
- errorClass="NOT_IMPLEMENTED",
- messageParameters={
- "feature": "Invalid return type with Arrow-optimized Python
UDF: VarcharType(10)"
- },
+ def test_char_varchar_results(self):
+ schema = StructType(
+ [
+ StructField("c", CharType(4)),
+ StructField("v", VarcharType(3)),
+ StructField("nested", ArrayType(CharType(2))),
+ StructField("m", MapType(CharType(2), VarcharType(3))),
+ ]
)
+ with self.sql_conf(
+ {
+ "spark.sql.charVarchar.standardSemantics.enabled": "true",
+ "spark.sql.execution.arrow.pythonUDF.columnarInput.enabled":
"true",
+ }
+ ):
+ result = self.spark.range(1).select(
+ udf(
+ lambda _: ("ab", "xyz", ["z"], {"k": "xy"}),
+ schema,
+ useArrow=True,
+ )("id").alias("s")
+ )
+ self.assertEqual(
+ result.first().s,
+ Row(c="ab ", v="xyz", nested=["z "], m={"k ": "xy"}),
+ )
+
+ pandas_result = self.spark.range(1).select(
+ pandas_udf(lambda values: values,
CharType(4))(lit("ab")).alias("c")
+ )
+ self.assertEqual(pandas_result.first().c, "ab ")
+
+ invalid = self.spark.range(1).select(
+ udf(lambda _: "abcd", VarcharType(3), useArrow=True)("id")
+ )
+ with self.assertRaisesRegex(Exception, "EXCEED_LIMIT_LENGTH"):
+ invalid.collect()
+
+ with tempfile.TemporaryDirectory() as path:
+ self.spark.range(1).write.parquet(path)
+ columnar_input = self.spark.read.parquet(path)
Review Comment:
**Non-blocking (P2):** A vectorized Parquet scan reaches the evaluator's
documented non-Arrow columnar path, so this case never exercises the new
`isArrow && !hasCharVarcharOutput` guard for `ArrowColumnVector` input. Please
add the CHAR/VARCHAR case to `ArrowColumnarPythonUDFSuite` using its
`readArrowSource` fixture, assert that `ArrowEvalPythonExec` still has an
Arrow-backed columnar child, and cover both padding and over-length rejection.
##########
sql/core/src/main/scala/org/apache/spark/sql/execution/arrow/ArrowConverters.scala:
##########
@@ -557,13 +559,15 @@ private[sql] object ArrowConverters extends Logging {
val rdd = session.sparkContext
.parallelize(batchesInDriver.toImmutableArraySeq,
batchesInDriver.length)
.mapPartitions { batchesInExecutors =>
- ArrowConverters.fromBatchIterator(
+ val rows = ArrowConverters.fromBatchIterator(
batchesInExecutors,
schema,
timeZoneId,
errorOnDuplicatedFieldNames,
largeVarTypes,
TaskContext.get())
+ val projection = UnsafeProjection.create(checkedAttrs, attrs)
+ rows.map(row => projection(row).copy(): InternalRow)
Review Comment:
**Non-blocking (P2):** For schemas without CHAR/VARCHAR, `checkedAttrs` is
identical to `attrs`, but this large-input RDD branch still runs every row
through an `UnsafeProjection` and then deep-copies it. That adds CPU and
allocations to all Arrow-backed DataFrame creation above
`arrowLocalRelationThreshold`, even when the feature is unused. Please retain
the original direct `fromBatchIterator` path unless `hasCharVarchar(schema)`
requires the checked projection.
##########
python/pyspark/sql/pandas/types.py:
##########
@@ -133,7 +135,7 @@ def to_arrow_type(
arrow_type = pa.float64()
elif isinstance(dt, DecimalType):
arrow_type = pa.decimal128(dt.precision, dt.scale)
- elif isinstance(dt, StringType):
+ elif isinstance(dt, (StringType, CharType, VarcharType)):
Review Comment:
**Blocking (P1):** This shared mapping is also the capability check for
`mapInPandas`, `mapInArrow`, grouped/cogrouped map, and aggregate UDFs. Those
JVM output consumers still use identity projections and never call
`stringLengthCheck`, so a declared `VarcharType(3)` can return `"abcd"`
unchanged and an under-length CHAR remains unpadded. Please keep CHAR/VARCHAR
rejected for non-scalar eval types until each corresponding output consumer
converts from physical STRING and applies the recursive assignment checks.
**Recommended change:** Limit the new CHAR/VARCHAR acceptance to scalar eval
types and other boundaries whose consumers already enforce assignment semantics.
**Why this works:** Separate Arrow transport mapping from eval-type
capability validation and recursively reject CHAR/VARCHAR in non-scalar return
schemas before calling the shared mapper.
**Scope:** PySpark UDF return-type validation and focused negative tests for
map, grouped, cogrouped, and aggregate pandas/Arrow eval types.
**Compatibility:** Preserves newly implemented scalar and DataFrame/Arrow
support while restoring the prior unsupported-type failure for non-scalar APIs
that cannot yet enforce the contract.
**Risks:** Capability checks can drift from JVM support if eval-type
ownership is not kept explicit.
**Constraints:** Preserve recursive CHAR/VARCHAR detection inside structs,
arrays, and maps. Do not disable the shared mapping used by DataFrame creation,
toArrow, or supported scalar UDF paths.
**Success:** Every accepted CHAR/VARCHAR producer applies recursive padding
and overflow checks; non-scalar eval types remain rejected until their JVM
output paths provide those semantics.
--
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]