diff --git a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/JdbcDialects.scala b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/JdbcDialects.scala index bea31a6ecd88b..ca5d9a8d922b0 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/JdbcDialects.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/JdbcDialects.scala @@ -170,7 +170,9 @@ abstract class JdbcDialect extends Serializable with Logging { * name is a reserved keyword, or in case it contains characters that require quotes (e.g. space). */ def quoteIdentifier(colName: String): String = { - s""""$colName"""" + // By ANSI standard, quotes are escaped with another quotes. + val escapedColName = colName.replace("\"", "\"\"") + s""""$escapedColName"""" } /** 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 5cdba5f1b91a0..a40ed80130cb5 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 @@ -132,7 +132,13 @@ private case object MySQLDialect extends JdbcDialect with SQLConfHelper { } override def quoteIdentifier(colName: String): String = { - s"`$colName`" + // Per MySQL documentation: https://dev.mysql.com/doc/refman/8.4/en/identifiers.html + // + // Identifier quote characters can be included within an identifier if you quote the + // identifier. If the character to be included within the identifier is the same as + // that used to quote the identifier itself, then you need to double the character. + val escapedColName = colName.replace("`", "``") + s"`$escapedColName`" } override def schemasExists(conn: Connection, options: JDBCOptions, schema: String): Boolean = { 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 f74aff8efb41c..3ec7d4d99430c 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 @@ -834,17 +834,20 @@ class JDBCSuite extends QueryTest with SharedSparkSession { } 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") - val Derby = JdbcDialects.get("jdbc:derby:db") - - val columns = Seq("abc", "key") - val MySQLColumns = columns.map(MySQL.quoteIdentifier(_)) - val PostgresColumns = columns.map(Postgres.quoteIdentifier(_)) - val DerbyColumns = columns.map(Derby.quoteIdentifier(_)) - assert(MySQLColumns === Seq("`abc`", "`key`")) - assert(PostgresColumns === Seq(""""abc"""", """"key"""")) - assert(DerbyColumns === Seq(""""abc"""", """"key"""")) + val mySQLDialect = JdbcDialects.get("jdbc:mysql://127.0.0.1/db") + val postgresDialect = JdbcDialects.get("jdbc:postgresql://127.0.0.1/db") + val derbyDialect = JdbcDialects.get("jdbc:derby:db") + val oracleDialect = JdbcDialects.get("jdbc:oracle:thin:@//localhost:1521/orcl") + + val columns = Seq("abc", "key", "double_quote\"", "back`") + val mySQLColumns = columns.map(mySQLDialect.quoteIdentifier) + val postgresColumns = columns.map(postgresDialect.quoteIdentifier) + val derbyColumns = columns.map(derbyDialect.quoteIdentifier) + val oracleColumns = columns.map(oracleDialect.quoteIdentifier) + assertResult(Seq("`abc`", "`key`", "`double_quote\"`", "`back```"))(mySQLColumns) + assertResult(Seq("\"abc\"", "\"key\"", "\"double_quote\"\"\"", "\"back`\""))(postgresColumns) + assertResult(Seq("\"abc\"", "\"key\"", "\"double_quote\"\"\"", "\"back`\""))(derbyColumns) + assertResult(Seq("\"abc\"", "\"key\"", "\"double_quote\"\"\"", "\"back`\""))(oracleColumns) } test("compile filters") {