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


##########
python/pyspark/sql/tests/connect/test_connect_basic.py:
##########
@@ -491,17 +491,28 @@ def test_schema(self):
         self._check_print_schema(query)
 
     def test_char_varchar_result_schema(self):
-        # SPARK-58794: Python Connect maps first-class CHAR/VARCHAR the same 
as classic.
-        query = "SELECT CAST('ab' AS CHAR(4)) AS c, CAST('cd' AS VARCHAR(6)) 
AS v"
+        # SPARK-59276: Python Connect maps first-class CHAR/VARCHAR the same 
as classic.

Review Comment:
   **Blocking (P1):** A non-empty Python Connect `createDataFrame` still passes 
this logical `CharType`/`VarcharType` schema directly to Arrow, whose type 
mapping handles `StringType` but not these sibling `AtomicType`s. I reproduced 
`UNSUPPORTED_DATA_TYPE_FOR_ARROW_CONVERSION` with `CharType(4, "UTF8_LCASE")`, 
before the request reaches the server; the same empty input bypasses Arrow, so 
the server-policy behavior diverges by row count. The new Python test only 
inspects a SQL-produced result schema and cannot catch this path. Please mirror 
the JVM physical-STRING/logical-schema separation for Python local data and 
cover non-empty caller-provided constrained strings across the server policy 
modes.
   
   **Recommended change:** Mirror the JVM client's physical-versus-logical 
schema separation in Python Connect: recursively replace caller-provided 
CHAR/VARCHAR leaves with collation-preserving STRING leaves only for local 
Arrow conversion, retain the original StructType on the LocalRelation, and add 
Python Connect createDataFrame coverage for non-empty caller-provided 
constrained strings and the server policy outcomes.
   
   **Why this works:** Build Arrow from raw string values under a physical 
STRING schema so to_arrow_schema never receives CharType or VarcharType. 
Serialize the original requested schema separately so existing server 
reconciliation applies standard padding and overflow, legacy-as-string 
normalization, or default rejection consistently for empty and populated input.
   
   **Scope:** python/pyspark/sql/connect, python/pyspark/sql/tests/connect
   
   **Compatibility:** Existing Python result-schema decoding, uncollated schema 
handling, and the server's three-mode CHAR/VARCHAR policy remain unchanged.
   
   **Risks:** A shallow transform could miss nested array, map, or struct 
leaves. Dropping the collation on the physical STRING leaf could change string 
interpretation before server reconciliation. Using the physical schema as the 
LocalRelation's requested schema would silently normalize standard-semantics 
input to STRING.
   
   **Constraints:** Do not change CharType or VarcharType inheritance. Do not 
pad, truncate, or length-check values on the client before server policy 
selection. Preserve nested nullability, field metadata, collation, and the 
original requested logical schema. Keep empty and populated caller data on the 
same server-side policy matrix.
   
   **Success:** A non-empty Python Connect createDataFrame with collated 
CHAR/VARCHAR reaches the server instead of raising 
UNSUPPORTED_DATA_TYPE_FOR_ARROW_CONVERSION. Standard semantics preserves the 
logical constrained schema and applies existing padding and overflow behavior. 
Legacy-as-string returns collation-preserving STRING schema and unpadded raw 
values. The default unsupported mode rejects empty and populated inputs 
consistently. Nested constrained leaves and omitted, explicit UTF8_BINARY, and 
non-binary collations retain their intended state.



##########
sql/api/src/main/scala/org/apache/spark/sql/types/StructField.scala:
##########
@@ -137,6 +137,8 @@ case class StructField(
   }
 
   private def isCollatedString(dt: DataType): Boolean = dt match {
+    case c: CharType => c.collation.isDefined

Review Comment:
   **Blocking (P1):** Adding `CharType`/`VarcharType` to this metadata path 
emits, for example, type `char(4)` plus a `__COLLATIONS` entry. The immediately 
preceding JVM `DataType.fromJson` sees that reserved entry before parsing the 
type and rejects anything other than literal `string` with 
`INVALID_JSON_DATA_TYPE_FOR_COLLATIONS`. As a result, schema JSON written by 
this version cannot be read during a JVM downgrade or mixed-version exchange, 
despite the nearby backward-compatibility contract. Please use a 
constrained-type restoration representation that the older JVM reader treats as 
ignorable metadata, while current Scala and Python readers still restore the 
exact length and optional collation.
   
   **Recommended change:** Encode CHAR/VARCHAR restoration data in a dedicated 
reserved metadata form that older readers treat as unknown metadata while 
keeping plain char(n)/varchar(n) type text. Teach current Scala and Python 
readers and writers to consume and strip that form, retain the existing 
__COLLATIONS behavior for StringType, and add cross-version compatibility 
fixtures plus current round-trip and invalid-encoding coverage.
   
   **Why this works:** An older JVM reader ignores an unknown metadata key and 
parses the uncollated constrained type, while current readers recognize the 
dedicated key and restore optional collation and length. Separating namespaces 
avoids triggering the released __COLLATIONS validator without weakening its 
type checks.
   
   **Scope:** sql/api/src/main/scala/org/apache/spark/sql/types, 
sql/catalyst/src/test/scala/org/apache/spark/sql/types, python/pyspark/sql
   
   **Compatibility:** Current StringType compatibility serialization and 
same-version constrained-type collation semantics remain intact while older 
readers degrade only the newly unsupported collation attribute.
   
   **Risks:** Older readers will retain the unknown reserved key as ordinary 
field metadata unless the compatibility contract deliberately accounts for 
that. Scala and Python could diverge on nested path syntax, provider 
qualification, or omission versus explicit UTF8_BINARY. Accepting both old and 
new restoration forms without duplicate validation could reintroduce ambiguous 
dual encodings.
   
   **Constraints:** Do not change the existing StringType __COLLATIONS JSON 
shape or behavior. Preserve CHAR/VARCHAR length, nested paths, provider 
validation, and the distinction between absent and explicit UTF8_BINARY 
collation in current readers. Reject ambiguous inline-plus-metadata and 
duplicate restoration encodings. Ensure the immediately preceding JVM parser 
can consume the emitted document as uncollated char(n)/varchar(n).
   
   **Success:** The immediately preceding JVM Spark reader parses newly emitted 
collated CHAR/VARCHAR schema JSON without error and obtains the historical 
uncollated constrained type. Current Scala and Python readers round-trip direct 
and nested CHAR/VARCHAR lengths and optional collations exactly. Existing 
collated StringType documents remain byte-shape compatible and preserve their 
current behavior. Malformed, conflicting, or dual restoration encodings still 
fail explicitly.



##########
sql/connect/common/src/main/scala/org/apache/spark/sql/connect/SparkSession.scala:
##########
@@ -223,7 +223,9 @@ class SparkSession private[sql] (
 
   /** @inheritdoc */
   def createDataFrame(rows: java.util.List[Row], schema: StructType): 
DataFrame = {
-    createDataset(RowEncoder.encoderFor(schema), 
rows.iterator().asScala).toDF()
+    // The client cannot observe the server's CHAR/VARCHAR configuration. 
Encode an explicitly
+    // provided schema independently of the client's local configuration, as 
for result schemas.
+    createDataset(RowEncoder.encoderForResultSchema(schema), 
rows.iterator().asScala).toDF()

Review Comment:
   Confirmed. Empty and populated inputs now share server-side reconciliation 
across standard, legacy-as-string, and default modes, including unpadded STRING 
output in legacy mode. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3960045571","thread_id":"inline:3960045571","verdict_sha256":"ae8dcffe4c707be0bb2e2c3138a8182a2db8f6643f48323831dc0886612431ac"}
 -->



##########
sql/connect/common/src/main/scala/org/apache/spark/sql/connect/SparkSession.scala:
##########
@@ -223,7 +223,9 @@ class SparkSession private[sql] (
 
   /** @inheritdoc */
   def createDataFrame(rows: java.util.List[Row], schema: StructType): 
DataFrame = {
-    createDataset(RowEncoder.encoderFor(schema), 
rows.iterator().asScala).toDF()
+    // The client cannot observe the server's CHAR/VARCHAR configuration. 
Encode an explicitly

Review Comment:
   Confirmed. The comment now limits the client-local configuration distinction 
to CHAR/VARCHAR semantics and no longer implies that other RowEncoder settings 
are overridden. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3962398470","thread_id":"inline:3962398470","verdict_sha256":"ae8dcffe4c707be0bb2e2c3138a8182a2db8f6643f48323831dc0886612431ac"}
 -->



##########
sql/connect/common/src/main/scala/org/apache/spark/sql/connect/SparkSession.scala:
##########
@@ -223,7 +223,10 @@ class SparkSession private[sql] (
 
   /** @inheritdoc */
   def createDataFrame(rows: java.util.List[Row], schema: StructType): 
DataFrame = {
-    createDataset(RowEncoder.encoderFor(schema), 
rows.iterator().asScala).toDF()
+    // RowEncoder consults the client process's local SqlApiConf, which can 
differ from the
+    // server-side configuration visible through SparkSession.conf. Encode an 
explicitly provided
+    // schema independently of that local configuration, as for result schemas.
+    createDataset(RowEncoder.encoderForResultSchema(schema), 
rows.iterator().asScala).toDF()

Review Comment:
   Confirmed. Explicit Row input now stays as raw collation-preserving STRING 
data until server reconciliation, and the final schema and values match 
standard, legacy-as-string, and default modes for empty and populated input. 
Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3992292084","thread_id":"inline:3992292084","verdict_sha256":"ae8dcffe4c707be0bb2e2c3138a8182a2db8f6643f48323831dc0886612431ac"}
 -->



##########
python/pyspark/sql/types.py:
##########
@@ -330,19 +330,31 @@ class CharType(AtomicType):
     ----------
     length : int
         the length limitation.
+    collation : str, optional

Review Comment:
   Confirmed. Both constructor docs now clearly distinguish the default None 
value from an explicit UTF8_BINARY declaration. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3992292095","thread_id":"inline:3992292095","verdict_sha256":"ae8dcffe4c707be0bb2e2c3138a8182a2db8f6643f48323831dc0886612431ac"}
 -->



##########
python/pyspark/sql/types.py:
##########
@@ -2671,7 +2708,12 @@ def _parse_datatype_json_value(  # type: ignore[return]
 def _assert_valid_type_for_collation(
     fieldPath: str, fieldType: Any, collationMap: Dict[str, str]
 ) -> None:
-    if fieldPath in collationMap and fieldType != "string":
+    is_string_type = (
+        fieldType == "string"
+        or (isinstance(fieldType, str) and _LENGTH_CHAR.fullmatch(fieldType) 
is not None)

Review Comment:
   Confirmed. Restoration metadata is now accepted only with uncollated 
char(n)/varchar(n) text, and both matching and conflicting dual encodings are 
rejected by the new tests. Thanks.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:3992292102","thread_id":"inline:3992292102","verdict_sha256":"ae8dcffe4c707be0bb2e2c3138a8182a2db8f6643f48323831dc0886612431ac"}
 -->



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