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


##########
sql/core/src/main/scala/org/apache/spark/sql/catalyst/parser/ParseSqlResult.scala:
##########
@@ -123,9 +134,83 @@ object ParseSqlResult {
     JObject(fields.toList)
   }
 
+  /**
+   * 1-based Unicode code-point starts for `statements` in `batch`. Statements
+   * are in ascending UTF-16 start order, so one left-to-right pass is enough:
+   * each count covers only the gap since the previous start.
+   */
+  private def charStarts(
+      batch: String,
+      statements: Seq[PositionedSqlStatement]): Seq[Int] = {
+    var utf16 = 0
+    var codePoints = 0
+    statements.map { segment =>
+      val startUtf16 = math.max(0, math.min(segment.start, batch.length))
+      if (startUtf16 >= utf16) {
+        codePoints += batch.codePointCount(utf16, startUtf16)
+      } else {
+        codePoints = batch.codePointCount(0, startUtf16)
+      }
+      utf16 = startUtf16
+      codePoints + 1
+    }
+  }
+
+  /** Unicode code-point length of the trimmed statement text. */
+  private def charLength(segment: PositionedSqlStatement): Int = {
+    val text = segment.statement
+    text.codePointCount(0, text.length)
+  }
+
+  /**
+   * Recompute the fragment of each statement `queryContext` by code points.
+   * The indexes are parser (code-point) offsets, but [[SQLQueryContext]]
+   * slices its fragment by UTF-16 units, which is off after a supplementary
+   * character. A context that falls outside the statement covers all of it.
+   * Contexts of another object (a view body) are left as is.
+   */
+  private def rewriteErrorCharIndexes(errorObj: JObject, statement: String): 
JObject = {
+    JObject(errorObj.obj.map {
+      case ("queryContext", JArray(contexts)) =>
+        "queryContext" -> JArray(contexts.map {
+          case ctx: JObject => rewriteQueryContextIndexes(ctx, statement)
+          case other => other
+        }.toList)
+      case other => other
+    })
+  }
+
+  private def rewriteQueryContextIndexes(ctx: JObject, statement: String): 
JObject = {
+    val objectType = ctx.obj.collectFirst { case ("objectType", JString(t)) => 
t }
+    val start = ctx.obj.collectFirst { case ("startIndex", JInt(n)) => n.toInt 
}
+    val stop = ctx.obj.collectFirst { case ("stopIndex", JInt(n)) => n.toInt }
+    (objectType, start, stop) match {
+      case (Some(""), Some(from), Some(to)) =>
+        val length = statement.codePointCount(0, statement.length)
+        val (first, last) =
+          if (from >= 1 && to >= from && to <= length) (from, to) else (1, 
length)
+        JObject(ctx.obj.map {
+          case ("startIndex", _) => "startIndex" -> JInt(first)
+          case ("stopIndex", _) => "stopIndex" -> JInt(last)
+          case ("fragment", _) => "fragment" -> 
JString(codePointSlice(statement, first - 1, last))

Review Comment:
   **Non-blocking (P2):** This rewrite corrects the fragment only in 
parse_sql's copy of the error. The unit mismatch it works around is in 
`SQLQueryContext`, which this PR leaves unchanged: parser origins carry ANTLR 
code-point indexes, but `fragment`, `summary`, and `isValid` treat them as 
UTF-16 offsets. The regular SQL error path therefore still reports the wrong 
text for any parser-origin context after a supplementary character. For `SELECT 
'<emoji>' AS e, DATE 'bad'`, parse_sql now reports the fragment `DATE 'bad'`, 
while `spark.sql` still reports ` DATE 'bad` in the STANDARD error JSON and 
`QueryContext.fragment()`, and its PRETTY caret sits one column to the left. As 
a result, parse_sql's nested error object, which the function docs describe as 
STANDARD format, now disagrees with Spark's STANDARD error for the same 
exception. Only error text is affected; query results are not.
   
   **Recommended change:** Make SQLQueryContext treat startIndex and stopIndex 
as Unicode code-point offsets into its SQL text, computing the fragment, the 
summary caret, and the validity check by code points, with a direct path when 
the text has no supplementary characters. Switch the whole-text origins for 
parameter substitution, EXECUTE IMMEDIATE, and SQL scripting case conditions 
from UTF-16 lengths to code-point counts, and make the parameter position 
mapper measure each substituted value in code points instead of UTF-16 units, 
so every index that reaches the renderer uses the parser's unit. Then remove 
parse_sql's per-context fragment re-slicing, which the shared renderer now 
produces, keeping its out-of-range widening guard and its start/length 
conversion. Add query-context and error-message tests for errors after a 
supplementary character on the regular SQL path, including a parameterized 
query whose parameter value contains one.
   
   **Why this works:** Parser indexes are already code points, so interpreting 
them as code points at the single renderer makes the fragment, caret, and 
validity agree with the indexes for every consumer at once. Converting the 
remaining UTF-16 whole-text producers to code points, and measuring substituted 
parameter values in code points when mapping positions back to the original 
text, gives every index one unit, so no consumer needs its own correction and 
parse_sql's nested error object matches Spark's STANDARD error for the same 
exception.
   
   **Scope:** Fix the unit at the shared query-context renderer, its producers, 
and the parameter position mapping, then simplify parse_sql's error rewrite to 
rely on it.
   
   **Compatibility:** Parser-produced startIndex and stopIndex values, 
parse_sql's start/length output, and every error surface's output for text 
without supplementary characters.
   
   **Risks:** Whole-text contexts from parameter substitution, EXECUTE 
IMMEDIATE, and scripting case conditions report a smaller stopIndex for text 
containing supplementary characters, and mapped positions after a parameter 
value containing one move to their code-point offsets, so an external consumer 
that sliced by the old UTF-16 indexes would see different bounds; text and 
parameter values without supplementary characters are unchanged, and the 
producer and mapper edits can be reverted only together with the renderer 
change. A context whose indexes exceed its text's code-point length, reachable 
only when variable substitution lengthens the parsed text, is dropped by the 
code-point validity check instead of reaching parse_sql's whole-statement 
widening. Rendering a fragment or caret for text with characters above U+00FF 
adds a code-point offset walk, which runs only when an error context is 
rendered.
   
   **Constraints:** Parser-produced startIndex and stopIndex values must not 
change, because clients already receive them and parse_sql documents them as 
code points. After the change every Origin producer that sets startIndex or 
stopIndex, and the parameter position mapper that translates them, must use 
code points; any producer left on UTF-16 lengths would again truncate or 
misplace fragments for text with supplementary characters, and a mapper left on 
UTF-16 value lengths would truncate the whole-text context of a parameterized 
query whose parameter values contain them.
   
   **Exclusions:**
   - DataFrame-API query contexts, which carry call sites rather than SQL 
indexes.
   - How parse_sql computes statement start and length, including whether the 
statement splitter should carry code-point offsets, which is raised separately 
as an author choice.
   
   **Success:** For a parser-origin error after a supplementary character, the 
QueryContext API fragment, the STANDARD error JSON fragment, and parse_sql's 
nested error fragment are identical and equal the text over the context's 
code-point index range. The PRETTY summary caret underlines exactly the 
context's code-point range. Whole-text contexts for parameter substitution, 
EXECUTE IMMEDIATE, scripting case conditions, and the parse_sql fallback cover 
their full text with a code-point stopIndex, including a parameterized query 
whose parameter values contain supplementary characters. In a parameterized 
query, an error location after a parameter value that contains a supplementary 
character maps to the code-point offset of the same location in the original 
text. Errors in text without supplementary characters produce the same indexes, 
fragments, and summaries on every surface as before.
   
   **Verification:**
   - **Regression:** On the regular SQL error path, a statement with a 
supplementary character before an error location, such as a typed-literal error 
after an emoji string literal, reports a fragment equal to the code-point range 
of its indexes through both the STANDARD error JSON and the QueryContext API, 
with an aligned summary caret.
   - **Behavior:** parse_sql still reports the same code-point indexes and 
fragments for syntax, typed-literal, and scripting errors after a supplementary 
character once its local fragment re-slicing is removed.
   - **Behavior:** Whole-text contexts for a parameterized query, EXECUTE 
IMMEDIATE, and a scripting case condition whose text contains a supplementary 
character report the full text as fragment and a code-point stopIndex, and a 
parameterized query whose parameter value contains a supplementary character 
reports both its whole-text context and a parser error after that value at the 
original text's code-point offsets.
   - **Compatibility:** Errors in text without supplementary characters produce 
identical startIndex, stopIndex, fragment, and summary on every surface before 
and after the change.



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