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

peter-toth pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-3.5 by this push:
     new 03d8e7407030 [SPARK-57960][SQL] Apply escapeSql to table/schema 
identifiers in JDBC index metadata queries
03d8e7407030 is described below

commit 03d8e74070301b2c3720385f09c7f87a70a963a2
Author: Peter Toth <[email protected]>
AuthorDate: Tue Jul 7 10:07:18 2026 +0200

    [SPARK-57960][SQL] Apply escapeSql to table/schema identifiers in JDBC 
index metadata queries
    
    ### What changes were proposed in this pull request?
    
    `PostgresDialect.indexExists` and `H2Dialect.indexExists`/`listIndexes` 
embed the table (and, for H2, schema) name into a SQL string literal in the 
`pg_indexes` / `INFORMATION_SCHEMA` lookup query without escaping. A table or 
schema name containing a single quote therefore produces a malformed query that 
fails with a syntax error. This is inconsistent with the index name in the same 
methods, which is already escaped via `escapeSql` (SPARK-57446 / SPARK-57447).
    
    This change applies `escapeSql` to the table/schema identifiers at these 
sites so quote-containing identifiers work correctly. 
`MySQLDialect.indexExists` places the table in identifier position via 
`quoteIdentifier` and is already correct.
    
    ### Why are the changes needed?
    
    To correctly handle table and schema names that contain a single quote, 
consistent with the existing index-name handling.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New unit test in `JDBCSuite` asserting the generated lookup SQL escapes 
single quotes in the table/schema name for the H2 and Postgres dialects.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #57038 from 
peter-toth/SPARK-57960-jdbc-dialect-escape-identifier-literals.
    
    Authored-by: Peter Toth <[email protected]>
    Signed-off-by: Peter Toth <[email protected]>
    (cherry picked from commit e58ade647be7a6eab13aa1ef72517a7d4b9abbd2)
    Signed-off-by: Peter Toth <[email protected]>
---
 .../org/apache/spark/sql/jdbc/H2Dialect.scala      |  8 +++----
 .../apache/spark/sql/jdbc/PostgresDialect.scala    |  2 +-
 .../org/apache/spark/sql/jdbc/JDBCSuite.scala      | 27 ++++++++++++++++++++++
 3 files changed, 32 insertions(+), 5 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 d9a90766937c..d95bdc3e5970 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
@@ -123,8 +123,8 @@ private[sql] object H2Dialect extends JdbcDialect {
       tableIdent: Identifier,
       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 = 
'${escapeSql(indexName)}'"
+      s"TABLE_SCHEMA = '${escapeSql(tableIdent.namespace().last)}' AND " +
+      s"TABLE_NAME = '${escapeSql(tableIdent.name())}' AND INDEX_NAME = 
'${escapeSql(indexName)}'"
     JdbcUtils.checkIfIndexExists(conn, sql, options)
   }
 
@@ -151,8 +151,8 @@ private[sql] object H2Dialect extends JdbcDialect {
          | AND i.INDEX_CATALOG = ic.INDEX_CATALOG
          | AND i.INDEX_SCHEMA = ic.INDEX_SCHEMA
          | AND i.INDEX_NAME = ic.INDEX_NAME
-         | AND i.TABLE_NAME = '${tableIdent.name()}'
-         | AND i.INDEX_SCHEMA = '${tableIdent.namespace().last}'
+         | AND i.TABLE_NAME = '${escapeSql(tableIdent.name())}'
+         | AND i.INDEX_SCHEMA = '${escapeSql(tableIdent.namespace().last)}'
          |""".stripMargin
     }
     var indexMap: Map[String, TableIndex] = Map()
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 1d5ac1f552bf..cf3159d91eda 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
@@ -212,7 +212,7 @@ private object PostgresDialect extends JdbcDialect with 
SQLConfHelper {
       indexName: String,
       tableIdent: Identifier,
       options: JDBCOptions): Boolean = {
-    val sql = s"SELECT * FROM pg_indexes WHERE tablename = 
'${tableIdent.name()}' AND" +
+    val sql = s"SELECT * FROM pg_indexes WHERE tablename = 
'${escapeSql(tableIdent.name())}' AND" +
       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 0d96517a712f..f74aff8efb41 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
@@ -806,6 +806,33 @@ class JDBCSuite extends QueryTest with SharedSparkSession {
     }
   }
 
+  test("SPARK-57960: (H2|Postgres)Dialect escape a single quote in indexExists 
table/schema name") {
+    // indexExists also embeds the table (and, for H2, schema) name as a SQL 
string literal, so a
+    // single quote in the identifier must be escaped too, consistent with the 
index-name escaping.
+    val ident = Identifier.of(Array("sch'ema"), "ta'ble")
+    Seq(
+      "jdbc:h2:mem:testdb0" -> Seq("TABLE_SCHEMA = 'sch''ema'", "TABLE_NAME = 
'ta''ble'"),
+      "jdbc:postgresql://127.0.0.1/db" -> Seq("tablename = 'ta''ble'")
+    ).foreach { case (jdbcUrl, expectedClauses) =>
+      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, "idx", ident, options)
+
+      val sqlCaptor = ArgumentCaptor.forClass(classOf[String])
+      verify(stmt).executeQuery(sqlCaptor.capture())
+      expectedClauses.foreach { expectedClause =>
+        assert(sqlCaptor.getValue.contains(expectedClause),
+          s"Unexpected lookup SQL for $jdbcUrl: ${sqlCaptor.getValue}")
+      }
+    }
+  }
+
   test("quote column names by jdbc dialect") {
     val MySQL = JdbcDialects.get("jdbc:mysql://127.0.0.1/db")
     val Postgres = 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