Repository navigation
Escape the closing quote in SQL identifiers, table and schema names - #1940
ItsDeadlyProgrammer wants to merge 6 commits into
Conversation
PR Summary
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1940 +/- ##
============================================
+ Coverage 92.40% 92.50% +0.10%
- Complexity 3565 3572 +7
============================================
Files 348 348
Lines 7055 7059 +4
Branches 692 687 -5
============================================
+ Hits 6519 6530 +11
+ Misses 370 369 -1
+ Partials 166 160 -6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
MySQL, MariaDB, BigQuery, Spark SQL and the other dialects that escape a quote with a backslash also treat a lone backslash as an escape character. A value such as C:\temp\ was written as 'C:\temp\', where the last backslash escapes the closing quote and the statement is broken. Double the backslash in these dialects (and without a dialect, which uses the same backslash escaping). Dialects that double quotes are unchanged.
The backslash rule is now a constructor parameter of SqlDialect instead of a switch, and SqlTransformer asks the dialect to escape the value.
…ct behaviour, SNOWFLAKE backslash
8643bcd to
b13df93
Compare
Field names were escaped by putting the opening identifier quote before both the opening and the closing character, which is right for "..." and `...` but gives [a[]b[[c] for SQL Server, where only ] is escaped (as ]]). Schema and table names were not escaped at all, so a quote in them closed the identifier early. Double the closing character in all three.
SqlDialect.escapeIdentifier doubles the closing quote character, using the same escape helper as string literals.
b13df93 to
b1cc478
Compare
| * Escapes a name placed inside a quoted identifier. Only the closing character needs escaping, | ||
| * by doubling it ({@code "a""b"}, {@code `a``b`}, {@code [a]]b]}). | ||
| */ | ||
| public String escapeIdentifier(String name) { |
There was a problem hiding this comment.
BigQuery does not accept a doubled backtick inside a quoted identifier. Its quoted identifiers use the same escape sequences as string literals, so a backtick is written as \`` and a backslash as \. With BIGQUERY, a table t2 now becomes `t2``, which BigQuery reads as ``t`` followed by ``2 `` and rejects. A name with a backslash (a\b) is also left as is, so BigQuery treats it as an escape sequence. Consider letting dialects with backslash identifier escaping (at least BIGQUERY; ClickHouse may need checking too) override escapeIdentifier, for example with escape(name, close, '\')`.
| MYSQL("`", Casing.UNCHANGED), | ||
| MYSQL("`", Casing.UNCHANGED, BACKSLASH), | ||
| NETEZZA("\""), | ||
| ORACLE("\"", Casing.TO_UPPER, |
There was a problem hiding this comment.
Doubling the quote does not help for Oracle: a quoted identifier there cannot contain " at all. So a field a"b with ORACLE still produces "a""b", which fails with ORA-03001/ORA-00904. Throwing an IllegalArgumentException for such names under ORACLE would be better than generating SQL that silently fails.
| @@ -287,21 +281,7 @@ private String appendTableInfo(Field<?, ? extends CharSequence>[] fields) { | |||
| appendNameToQuery(result, tableName); | |||
There was a problem hiding this comment.
Behaviour change for schema and table names: they used to be appended unchanged and are now escaped. A caller who worked around the old bug by escaping the name themselves (e.g. tableName("t\"\"2")) now gets "t""""2", a different identifier. This is probably acceptable, but it should go in the release notes as a breaking change.
| } | ||
| sb.append(name); | ||
| sb.append(dialect == null | ||
| ? SqlDialect.escape(name, closeSqlIdentifier, closeSqlIdentifier) |
There was a problem hiding this comment.
Simplification: closeSqlIdentifier is already the dialect's closing character, because build() passes dialect.getSqlQuoteIdentifier() as sqlIdentifier. So the dialect == null branch and SqlDialect.escapeIdentifier do the same job, and they find the closing character in two different ways (charAt(1) here, charAt(length - 1) there). sb.append(SqlDialect.escape(name, closeSqlIdentifier, closeSqlIdentifier)) would be enough, and the new public escapeIdentifier method could be dropped (or kept only as an override point for BigQuery, see the other comment).
| assertThat(transformer.generate(schema, 1)).isEqualTo(expected); | ||
| } | ||
|
|
||
| private static Stream<Arguments> identifierEscapeDialects() { |
There was a problem hiding this comment.
These cases only cover the dialect branch. Nothing tests the dialect == null path in appendNameToQuery, i.e. builder().sqlQuoteIdentifier("[]") or the default "" with a quote in the schema, table or field name. A case such as sqlQuoteIdentifier("[]").tableName("t]2") would cover it.
Another escaping fix in
SqlTransformer, this time for identifiers. It's independent of #1939 (string values).Inside a quoted identifier, only the closing character needs escaping, and it's escaped by doubling:
"a""b",`ab``,[a]]b]`."or`), but for SQL Server a fielda]b[ccame out as[a[]b[[c], which is invalid. It should be[a]]b[c].t"2gave"t"2"and broke the statement.Both now go through
appendNameToQuery, which doubles the closing character. Deciding when a name gets quoted (isSqlQuoteIdentifierRequiredFor) is unchanged.Test:
escapesClosingQuoteInIdentifiersfor SQL Server, PostgreSQL and MySQL, with the quote character in the schema, table and field name. Without the change all 3 cases fail:With it they give
[s]]1].[t]]2] ([a]]b[c]),"s""1"."t""2" ("a""b")and`s1.t2` (`ab) ``.SqlTest` passes (105 tests).