Skip to content

Escape the closing quote in SQL identifiers, table and schema names - #1940

Open
ItsDeadlyProgrammer wants to merge 6 commits into
datafaker-net:mainfrom
ItsDeadlyProgrammer:fix/sql-identifier-escape
Open

ItsDeadlyProgrammer wants to merge 6 commits into
datafaker-net:mainfrom
ItsDeadlyProgrammer:fix/sql-identifier-escape

Conversation

@ItsDeadlyProgrammer

Copy link
Copy Markdown
Contributor

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]`.

  1. Field names were escaped by putting the opening identifier character before both the opening and the closing character. That works when both are the same (" or `), but for SQL Server a field a]b[c came out as [a[]b[[c], which is invalid. It should be [a]]b[c].
  2. Schema and table names weren't escaped at all, so a table named t"2 gave "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: escapesClosingQuoteInIdentifiers for SQL Server, PostgreSQL and MySQL, with the quote character in the schema, table and field name. Without the change all 3 cases fail:

INSERT INTO [s]1].[t]2] ([a[]b[[c]) VALUES ('x');
INSERT INTO "s"1"."t"2" ("a""b") VALUES ('x');
INSERT INTO `s`1`.`t`2` (`a``b`) VALUES ('x');

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).

@what-the-diff

what-the-diff Bot commented Oct 19, 2026

Copy link
Copy Markdown

PR Summary

  • Improves SQL compatibility for names containing quote characters. Table, schema, and field names are now safely escaped for SQL Server, PostgreSQL, and MySQL, reducing the risk of invalid queries when names include the relevant closing quote character.
  • Adds coverage for the change. Tests verify escaping across all three database dialects.

@codecov-commenter

codecov-commenter commented Oct 19, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.50%. Comparing base (471b259) to head (b1cc478).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ItsDeadlyProgrammer

Copy link
Copy Markdown
Contributor Author

Reworked the same way: SqlDialect.escapeIdentifier(name) doubles the closing quote character and shares the escape helper with #1939. This branch includes #1939, so it's easiest to merge that one first.

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.
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.
@asolntsev asolntsev added this to the 3.0.0 milestone Oct 25, 2026
* 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) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants