This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 85a097243972 [SPARK-57447][SQL] Escape the index name in
`(H2|MySQL|Postgres)Dialect.indexExists`
85a097243972 is described below
commit 85a097243972a864cc31e5f1a3366d645110dab2
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`
### What changes were proposed in this pull request?
`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.
### Why are the changes needed?
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.
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Pass the CIs.
### Was this patch authored or co-authored using generative AI tooling?
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]>
---
.../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 fb7a1c6e617e..b301c0c0bd5b 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
@@ -318,7 +318,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 dd57c129179e..8941767ec357 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
@@ -271,7 +271,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 9fcbc8b52d79..01f0d2619c22 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.connector.expressions.{Expression => V2Expression,
FieldReference}
import org.apache.spark.sql.connector.expressions.filter.{AlwaysFalse,
AlwaysTrue, Predicate}
import org.apache.spark.sql.execution.{DataSourceScanExec, ExtendedMode,
ProjectExec}
@@ -837,6 +839,31 @@ class JDBCSuite extends 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]