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


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/trees/SQLQueryContextSuite.scala:
##########
@@ -45,5 +45,31 @@ class SQLQueryContextSuite extends SparkFunSuite with 
SQLHelper {
     ).foreach { case ((sqlText, start, stop), expectedFragment) =>
       assert(getFragment(sqlText, start, stop) === expectedFragment)
     }
+
+    val emoji = new String(Character.toChars(0x1F600))
+    val sql = s"SELECT '$emoji' AS a, DATE 'bad'"
+    val start = sql.codePointCount(0, sql.indexOf("DATE"))
+    val stop = sql.codePointCount(0, sql.length) - 1
+    assert(getFragment(sql, start, stop) === "DATE 'bad'")
+    // A UTF-16 slice of the same indexes would include the comma and drop the 
last quote.
+    assert(sql.substring(start, stop + 1) !== "DATE 'bad'")
+  }
+
+  test("summary caret follows code-point indexes after a supplementary 
character") {

Review Comment:
   **Nit (P3):** This test passes unchanged against the merge-target `summary`. 
For a context on the text's first line, the old code starts the carets one 
UTF-16 unit early but also emits two spaces for the emoji, so the two errors 
cancel: both versions render 17 spaces and then 10 carets for this input, and 
the caret assertion in `QueryContextSuite` matches both as well. The new 
supplementary-text branch of `summary`, including its start/stop conversion and 
per-code-point loop, could therefore regress without any test failing. The 
outputs differ only for a context on a later line than a supplementary 
character, such as `DATE 'bad'` on the line after `SELECT '<emoji>' AS a,` (the 
merge target draws 9 carets, this PR draws 10), or for a start-truncated 
context with a supplementary character before it.
   
   **Verification:**
   - **Regression:** For a context on a later line than a supplementary 
character, such as DATE 'bad' on the line after SELECT '<U+1F600>' AS a, the 
summary caret underlines every code point of the context; the merge-target 
summary drops the last one.
   - **Regression:** For a context that follows other text on a later line than 
a supplementary character, such as DATE 'bad' after '  b, ' on the line after 
SELECT '<U+1F600>' AS a,, the caret starts under the context's first code 
point; the merge-target summary starts it one column to the left.
   - **Behavior:** A context whose fragment contains or ends with a 
supplementary character renders exactly one caret per code point of the 
fragment and none beyond it.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/trees/SQLQueryContextSuite.scala:
##########
@@ -45,5 +45,31 @@ class SQLQueryContextSuite extends SparkFunSuite with 
SQLHelper {
     ).foreach { case ((sqlText, start, stop), expectedFragment) =>
       assert(getFragment(sqlText, start, stop) === expectedFragment)
     }
+
+    val emoji = new String(Character.toChars(0x1F600))
+    val sql = s"SELECT '$emoji' AS a, DATE 'bad'"
+    val start = sql.codePointCount(0, sql.indexOf("DATE"))
+    val stop = sql.codePointCount(0, sql.length) - 1
+    assert(getFragment(sql, start, stop) === "DATE 'bad'")
+    // A UTF-16 slice of the same indexes would include the comma and drop the 
last quote.

Review Comment:
   **Nit (P3):** Here `sql.substring(start, stop + 1)` is `" DATE 'bad"`. 
UTF-16 index 16 is the comma and index 17 is the space before `DATE`, so the 
UTF-16 slice picks up that space, not the comma, and drops the closing quote. 
The assertion still passes, but the comment misleads readers about how the 
shift shows up.
   
   ```suggestion
       // A UTF-16 slice of the same indexes would include a leading space and 
drop the last quote.
   ```
   
   **Verification:**
   - **Inspection:** The comment describes the UTF-16 slice as picking up the 
space before DATE and dropping the closing quote, which matches the asserted 
expression.



##########
sql/api/src/main/scala/org/apache/spark/sql/catalyst/trees/QueryContexts.scala:
##########
@@ -19,6 +19,25 @@ package org.apache.spark.sql.catalyst.trees
 
 import org.apache.spark.{QueryContext, QueryContextType, 
SparkUnsupportedOperationException}
 
+object SQLQueryContext {
+
+  /**
+   * Inclusive Unicode code-point slice `[from, to]` of `text`. Indexes are 
clamped to the text.
+   * Parser origins store ANTLR code-point offsets, so this is the unit every 
SQL-text recovery

Review Comment:
   **Nit (P3):** This scaladoc is now the only place that says query-context 
indexes are code points. The public `QueryContext` interface, which 
`SparkThrowable.getQueryContext` returns to users, still describes 
`startIndex()` and `stopIndex()` only as "The index starts from 0", in line 
comments that the published API docs omit, and the PySpark docstrings say the 
same. After this PR, whole-text contexts and positions mapped past a parameter 
value also count code points. For example, the syntax error in 
`spark.sql("SELECT '<emoji>', :p FRUM t", Map("p" -> 1))` now reports 
`stopIndex()` 20 instead of 21, so a JVM caller that slices its own SQL 
`String` with `substring(startIndex(), stopIndex() + 1)` loses the last 
character. Javadoc on `startIndex()` and `stopIndex()` stating that they are 
0-based code-point offsets with an inclusive stop, plus the same note in the 
PySpark docstrings, would close the gap; the Connect proto field comments can 
follow separately.
   
   **Verification:**
   - **Inspection:** The QueryContext startIndex/stopIndex documentation in 
Java and in PySpark names Unicode code points as the unit and says stopIndex is 
inclusive.



##########
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:
   Thanks, this fixes the main issue: SQLQueryContext, the whole-text origins, 
and PositionMapper now all use code points, and the new QueryContextSuite cases 
cover the regular SQL path and a parameter value that contains an emoji. One 
part is still open. parse_sql still replaces the fragment of every in-range 
statement-level context with its own code-point slice, so the per-context 
re-slicing is still there even though the renderer now produces the same 
fragment. Only the out-of-range widening needs to rewrite the fragment. This 
review also raises that the new tests do not exercise the whole-text stopIndex 
changes.
   
   <!-- SPARK_DEV_REVIEW_REPLY 
{"feedback_id":"inline:4407271872","thread_id":"inline:4407271872","verdict_sha256":"0f38303c309a31165f841b91c941a84afa47055a3e530b76a007588847464a98"}
 -->



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