diff --git a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/DatabricksDialect.scala b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/DatabricksDialect.scala index f4fc670470328..9124c1b889098 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/jdbc/DatabricksDialect.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/jdbc/DatabricksDialect.scala @@ -59,7 +59,12 @@ private case class DatabricksDialect() extends JdbcDialect with NoLegacyJDBCErro } override def quoteIdentifier(colName: String): String = { - s"`$colName`" + // Per Databricks documentation: + // https://docs.databricks.com/aws/en/sql/language-manual/sql-ref-identifiers + // + // "Any character from the Unicode character set. Use ` to escape ` itself." + val escapedColName = colName.replace("`", "``") + s"`$escapedColName`" } override def supportsLimit: Boolean = true 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 8afec9cead07d..6a78830b4e4b8 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 @@ -244,7 +244,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 19377057844e5..f984bebfe2d6c 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 @@ -196,7 +196,14 @@ private case class MySQLDialect() extends JdbcDialect with SQLConfHelper with No } 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. + // The following statement creates a table named a`b that contains a column named c"d: + 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 6896f6993fb33..09c2e82c45f1c 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 @@ -803,17 +803,23 @@ 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 databricksDialect = JdbcDialects.get("jdbc:databricks://host/db") + + 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) + val databricksColumns = columns.map(databricksDialect.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) + assertResult(Seq("`abc`", "`key`", "`double_quote\"`", "`back```"))(databricksColumns) } test("compile filters") {