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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -619,25 +622,59 @@ class JacksonParser(
 
   /**
    * Parse an object as a Map, preserving all fields.
+   *
+   * JSON object names used as CHAR/VARCHAR keys are length-checked without 
rewriting:
+   * CHAR keys must already be exactly n characters, and VARCHAR keys must 
already be
+   * at most n characters. Padding, trimming, and mapKeyDedupPolicy are not 
applied.
+   * Repeated names that are binary-equal last-win for CHAR/VARCHAR keys 
(collation is
+   * not consulted, matching how STRING keys are compared here); STRING keys 
keep every
+   * pair. This differs from XML, which pads keys and then applies 
mapKeyDedupPolicy.
    */
   private def convertMap(
       parser: JsonParser,
-      fieldConverter: ValueConverter): MapData = {
+      fieldConverter: ValueConverter,
+      keyType: DataType): MapData = {
     val keys = ArrayBuffer.empty[UTF8String]
     val values = ArrayBuffer.empty[Any]
     var badRecordException: Option[Throwable] = None
+    // CHAR/VARCHAR keys last-win on exact (binary) duplicate names. This maps 
a key to
+    // the index of its value in `values` for O(1) overwrite; it stays null 
(and the
+    // buffers keep every pair, as STRING maps always have) when last-win does 
not apply.
+    // It indexes `values` (not `keys`) so it stays valid even after the 
NonFatal arm
+    // appends a dangling key, leaving the two buffers unbalanced.
+    val lastWinIndex: mutable.HashMap[UTF8String, Int] =
+      if (isLengthCheckedKeyType(keyType)) mutable.HashMap.empty[UTF8String, 
Int] else null
 
     while (nextUntil(parser, JsonToken.END_OBJECT)) {
-      keys += UTF8String.fromString(parser.currentName)
+      val key = convertJsonMapKey(parser.currentName, keyType)

Review Comment:
   Good catch. The key check now runs inside the per-entry loop and is handled 
exactly like a failing value converter: on rejection we advance past the value 
and skip its children, leaving the key unpaired. The loop still drains to 
`END_OBJECT`, so siblings parse normally and streaming row counts are 
unaffected. The unbalanced key/value buffers then trigger the existing 
`keys.length != values.length` rethrow of the real cause, independent of 
`enablePartialResults`. Added tests for a bad key followed by a sibling (both 
partial-result modes) and for the multiLine top-level-array row count.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -107,6 +107,9 @@ class JacksonParser(
 
   private val enablePartialResults = SQLConf.get.jsonEnablePartialResults
 
+  // CHAR/VARCHAR JSON map keys are length-checked without pad or trim under 
this flag.
+  private val charVarcharStandardSemantics = 
SQLConf.get.charVarcharStandardSemantics

Review Comment:
   Fixed. The flag is now captured at analysis time in `JsonToStructs` (default 
arg off `SQLConf.get`), threaded through `JsonToStructsEvaluator`, and passed 
into `JacksonParser` via a new `charVarcharStandardSemanticsOverride` 
constructor arg. The file-based datasource path leaves the override as `None` 
and keeps reading the live conf, which is correct there. A view created with 
the flag on now keeps standard semantics when queried with it off (test added).
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -619,25 +622,59 @@ class JacksonParser(
 
   /**
    * Parse an object as a Map, preserving all fields.
+   *
+   * JSON object names used as CHAR/VARCHAR keys are length-checked without 
rewriting:
+   * CHAR keys must already be exactly n characters, and VARCHAR keys must 
already be
+   * at most n characters. Padding, trimming, and mapKeyDedupPolicy are not 
applied.
+   * Repeated names that are binary-equal last-win for CHAR/VARCHAR keys 
(collation is
+   * not consulted, matching how STRING keys are compared here); STRING keys 
keep every
+   * pair. This differs from XML, which pads keys and then applies 
mapKeyDedupPolicy.

Review Comment:
   Agreed - switched to keep-all, identical to STRING keys. Dropped the 
last-win dedup and the HashMap entirely; `spark.sql.mapKeyDedupPolicy` is not 
consulted. Updated the scaladoc and migration guide, and added tests that pin 
duplicate retention via `size` and `map_entries`.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -619,25 +622,59 @@ class JacksonParser(
 
   /**
    * Parse an object as a Map, preserving all fields.
+   *
+   * JSON object names used as CHAR/VARCHAR keys are length-checked without 
rewriting:
+   * CHAR keys must already be exactly n characters, and VARCHAR keys must 
already be
+   * at most n characters. Padding, trimming, and mapKeyDedupPolicy are not 
applied.
+   * Repeated names that are binary-equal last-win for CHAR/VARCHAR keys 
(collation is
+   * not consulted, matching how STRING keys are compared here); STRING keys 
keep every
+   * pair. This differs from XML, which pads keys and then applies 
mapKeyDedupPolicy.
    */
   private def convertMap(
       parser: JsonParser,
-      fieldConverter: ValueConverter): MapData = {
+      fieldConverter: ValueConverter,
+      keyType: DataType): MapData = {
     val keys = ArrayBuffer.empty[UTF8String]
     val values = ArrayBuffer.empty[Any]
     var badRecordException: Option[Throwable] = None
+    // CHAR/VARCHAR keys last-win on exact (binary) duplicate names. This maps 
a key to
+    // the index of its value in `values` for O(1) overwrite; it stays null 
(and the
+    // buffers keep every pair, as STRING maps always have) when last-win does 
not apply.
+    // It indexes `values` (not `keys`) so it stays valid even after the 
NonFatal arm
+    // appends a dangling key, leaving the two buffers unbalanced.
+    val lastWinIndex: mutable.HashMap[UTF8String, Int] =
+      if (isLengthCheckedKeyType(keyType)) mutable.HashMap.empty[UTF8String, 
Int] else null

Review Comment:
   Moot now - the HashMap is gone with the switch to keep-all (see above), so 
there is no longer any key-hashing of untrusted input.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -659,6 +696,33 @@ class JacksonParser(
     }
   }
 
+  /**
+   * A CHAR/VARCHAR map key under standard semantics is length-checked (not 
padded or
+   * trimmed) and deduplicated last-win on exact names. This is the single 
predicate both
+   * `convertMap` and `convertJsonMapKey` consult so the two cannot drift 
apart.
+   */
+  private def isLengthCheckedKeyType(keyType: DataType): Boolean =
+    charVarcharStandardSemantics && (keyType match {
+      case _: CharType | _: VarcharType => true
+      case _ => false
+    })
+
+  private def convertJsonMapKey(rawName: String, keyType: DataType): 
UTF8String = {
+    val key = UTF8String.fromString(rawName)
+    if (!isLengthCheckedKeyType(keyType)) {
+      key
+    } else {
+      keyType match {
+        case c: CharType =>
+          CharVarcharCodegenUtils.charTypeJsonMapKeyCheck(key, c.length)
+        case v: VarcharType =>
+          CharVarcharCodegenUtils.varcharTypeJsonMapKeyCheck(key, v.length)
+        case _ =>
+          key

Review Comment:
   Fixed. The key checker is now built once up front via `makeMapKeyChecker` 
(returns `Option[UTF8String => Unit]`), so there is no per-key re-matching and 
the unreachable branch is gone.
   



##########
sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/util/CharVarcharCodegenUtils.java:
##########
@@ -111,4 +113,28 @@ public static UTF8String charTypeReadSideCheck(UTF8String 
inputStr, int limit) {
   public static UTF8String varcharTypeReadSideCheck(UTF8String inputStr, int 
limit) {
     return varcharTypeWriteSideCheck(inputStr, limit);
   }
+
+  /**
+   * JSON object names used as MAP<CHAR(n), _> keys cannot be padded or 
trimmed.
+   * The name must already be exactly n characters.
+   */
+  public static UTF8String charTypeJsonMapKeyCheck(UTF8String inputStr, int 
limit) {
+    if (inputStr.numChars() != limit) {
+      throw QueryExecutionErrors.unsupportedJsonCharVarcharMapKey(
+          inputStr, CharType.apply(limit));

Review Comment:
   Fixed. The helpers moved out of `CharVarcharCodegenUtils` into 
`JacksonParser` and now receive the actual `CharType`/`VarcharType` key type, 
so collation is preserved. Added a test asserting the error message shows 
`VARCHAR(2) COLLATE UTF8_LCASE` and that collation is not consulted for the 
length check.
   



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