This is an automated email from the ASF dual-hosted git repository.

dongjoon-hyun pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new c697510102cf [SPARK-57447][SQL] Escape the index name in 
`(H2|MySQL|Postgres)Dialect.indexExists`
c697510102cf is described below

commit c697510102cfe6af3307f2b37f863ae3066569c1
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Sun Jun 14 20:37:14 2026 -0700

    [SPARK-57447][SQL] Escape the index name in 
`(H2|MySQL|Postgres)Dialect.indexExists`
    
    `indexExists` in `H2Dialect`, `MySQLDialect`, and `PostgresDialect` embeds 
the
    index name as a SQL string literal in its lookup query. This PR wraps the 
index
    name with the existing `JdbcDialect.escapeSql` helper so that single quotes 
are
    properly escaped.
    
    Note that the scope of this PR is only index names. For table names or 
TABLE_SCHEMA, we are going to handle independently.
    
    If an index name contains a single quote, the lookup query becomes malformed
    (e.g. `... INDEX_NAME = 'a'b'`). `JdbcUtils.checkIfIndexExists` swallows the
    resulting exception and returns `false`, so an existing index whose name
    contains a single quote is incorrectly reported as not existing.
    
    No.
    
    Pass the CIs.
    
    Generated-by: Claude Code
    
    Closes #56508 from dongjoon-hyun/SPARK-57447.
    
    Authored-by: Dongjoon Hyun <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit 86db727372453d45c712bf4f9506c902589bb18e)
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit ab289d60ed936cfd8aa177790dc4a161051e0205)
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit 85a097243972a864cc31e5f1a3366d645110dab2)
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../org/apache/spark/sql/jdbc/H2Dialect.scala      |  2 +-
 .../org/apache/spark/sql/jdbc/MySQLDialect.scala   |  2 +-
 .../apache/spark/sql/jdbc/PostgresDialect.scala    |  2 +-
 .../org/apache/spark/sql/jdbc/JDBCSuite.scala      | 29 +++++++++++++++++++++-
 4 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/H2Dialect.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/H2Dialect.scala
index 966c5d14bc66..95bb1cbf99ba 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/H2Dialect.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/H2Dialect.scala
@@ -133,7 +133,7 @@ private[sql] case class H2Dialect() extends JdbcDialect 
with NoLegacyJDBCError {
       options: JDBCOptions): Boolean = {
     val sql = "SELECT * FROM INFORMATION_SCHEMA.INDEXES WHERE " +
       s"TABLE_SCHEMA = '${tableIdent.namespace().last}' AND " +
-      s"TABLE_NAME = '${tableIdent.name()}' AND INDEX_NAME = '$indexName'"
+      s"TABLE_NAME = '${tableIdent.name()}' AND INDEX_NAME = 
'${escapeSql(indexName)}'"
     JdbcUtils.checkIfIndexExists(conn, sql, options)
   }
 
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala
index 2dc68027ebbf..4bf4209febcc 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/MySQLDialect.scala
@@ -325,7 +325,7 @@ private case class MySQLDialect() extends JdbcDialect with 
SQLConfHelper with No
       tableIdent: Identifier,
       options: JDBCOptions): Boolean = {
     val sql = s"SHOW INDEXES FROM ${quoteIdentifier(tableIdent.name())} " +
-      s"WHERE key_name = '$indexName'"
+      s"WHERE key_name = '${escapeSql(indexName)}'"
     JdbcUtils.checkIfIndexExists(conn, sql, options)
   }
 
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala
index d7d730ed0cc3..df40fb0b2dea 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/PostgresDialect.scala
@@ -255,7 +255,7 @@ private case class PostgresDialect()
       tableIdent: Identifier,
       options: JDBCOptions): Boolean = {
     val sql = s"SELECT * FROM pg_indexes WHERE tablename = 
'${tableIdent.name()}' AND" +
-      s" indexname = '$indexName'"
+      s" indexname = '${escapeSql(indexName)}'"
     JdbcUtils.checkIfIndexExists(conn, sql, options)
   }
 
diff --git a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
index 2633a688bdd6..4848575cd08d 100644
--- a/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
+++ b/sql/core/src/test/scala/org/apache/spark/sql/jdbc/JDBCSuite.scala
@@ -18,7 +18,7 @@
 package org.apache.spark.sql.jdbc
 
 import java.math.BigDecimal
-import java.sql.{Date, DriverManager, Timestamp}
+import java.sql.{Connection, Date, DriverManager, ResultSet, Statement, 
Timestamp}
 import java.time.{Instant, LocalDate, LocalDateTime}
 import java.time.format.DateTimeFormatter
 import java.util.{Calendar, GregorianCalendar, Properties, TimeZone}
@@ -26,6 +26,7 @@ import java.util.{Calendar, GregorianCalendar, Properties, 
TimeZone}
 import scala.jdk.CollectionConverters._
 import scala.util.Random
 
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers._
 import org.mockito.Mockito._
 
@@ -35,6 +36,7 @@ import org.apache.spark.sql.catalyst.{analysis, 
TableIdentifier}
 import org.apache.spark.sql.catalyst.parser.CatalystSqlParser
 import org.apache.spark.sql.catalyst.plans.logical.ShowCreateTable
 import org.apache.spark.sql.catalyst.util.{CaseInsensitiveMap, 
CharVarcharUtils, DateTimeTestUtils}
+import org.apache.spark.sql.connector.catalog.Identifier
 import org.apache.spark.sql.execution.{DataSourceScanExec, ExtendedMode, 
ProjectExec}
 import org.apache.spark.sql.execution.command.{ExplainCommand, 
ShowCreateTableCommand}
 import org.apache.spark.sql.execution.datasources.{LogicalRelation, 
LogicalRelationWithTable}
@@ -779,6 +781,31 @@ class JDBCSuite extends QueryTest with SharedSparkSession {
     assert(JdbcDialects.get("test.invalid") === NoopDialect)
   }
 
+  test("SPARK-57447: (H2|MySQL|Postgres)Dialect escape a single quote in 
indexExists") {
+    // indexExists builds a lookup query with the index name as a SQL string 
literal, so a single
+    // quote in the name must be escaped to keep the WHERE clause well-formed.
+    Seq(
+      "jdbc:h2:mem:testdb0" -> "INDEX_NAME = 'i''1'",
+      "jdbc:mysql://127.0.0.1/db" -> "key_name = 'i''1'",
+      "jdbc:postgresql://127.0.0.1/db" -> "indexname = 'i''1'"
+    ).foreach { case (jdbcUrl, expectedClause) =>
+      val dialect = JdbcDialects.get(jdbcUrl)
+      val conn = mock(classOf[Connection])
+      val stmt = mock(classOf[Statement])
+      val rs = mock(classOf[ResultSet])
+      when(conn.createStatement()).thenReturn(stmt)
+      when(stmt.executeQuery(anyString())).thenReturn(rs)
+
+      val options = new JDBCOptions(jdbcUrl, "test.people", Map.empty[String, 
String])
+      dialect.indexExists(conn, "i'1", Identifier.of(Array("test"), "people"), 
options)
+
+      val sqlCaptor = ArgumentCaptor.forClass(classOf[String])
+      verify(stmt).executeQuery(sqlCaptor.capture())
+      assert(sqlCaptor.getValue.contains(expectedClause),
+        s"Unexpected lookup SQL for $jdbcUrl: ${sqlCaptor.getValue}")
+    }
+  }
+
   test("quote column names by jdbc dialect") {
     val mySQLDialect = JdbcDialects.get("jdbc:mysql://127.0.0.1/db")
     val postgresDialect = JdbcDialects.get("jdbc:postgresql://127.0.0.1/db")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to