Skip to content

Commit 27fc8f1

Browse files
committed
feat(parser): support double quoted string literals behind allowDoubleQuotedStrings
One lexeme, two readings: ANSI/Postgres/Oracle and SQL Server quote identifiers with double quotes, while BigQuery, Spark/Databricks and MySQL default sql_mode read them as string literals. The switch rewrites the token kind in the S_QUOTED_IDENTIFIER action (the square bracket machinery below), StringValue keeps the double quote on round-trip via its quoteStr. MYSQL and MARIADB presets carry the switch. Implements item 1 of #2512.
1 parent 94c4508 commit 27fc8f1

6 files changed

Lines changed: 75 additions & 4 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/StringValue.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public StringValue(String escapedValue) {
3636
&& escapedValue.endsWith("'")) {
3737
value = escapedValue.substring(1, escapedValue.length() - 1);
3838
return;
39+
} else if (escapedValue.length() >= 2 && escapedValue.startsWith("\"")
40+
&& escapedValue.endsWith("\"")) {
41+
// double quoted String Literals (Feature.allowDoubleQuotedStrings)
42+
value = escapedValue.substring(1, escapedValue.length() - 1);
43+
quoteStr = "\"";
44+
return;
3945
} else if (escapedValue.length() >= 4 && escapedValue.startsWith("$$")
4046
&& escapedValue.endsWith("$$")) {
4147
value = escapedValue.substring(2, escapedValue.length() - 2);

src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ public abstract class AbstractJSqlParser<P> {
2626

2727
public enum Dialect {
2828
ANSI_SQL, ORACLE, MYSQL(Feature.allowBackslashEscapeCharacter,
29-
Feature.allowHashLineComments), MARIADB(Feature.allowBackslashEscapeCharacter,
30-
Feature.allowHashLineComments), SQLSERVER(
29+
Feature.allowHashLineComments,
30+
Feature.allowDoubleQuotedStrings), MARIADB(Feature.allowBackslashEscapeCharacter,
31+
Feature.allowHashLineComments,
32+
Feature.allowDoubleQuotedStrings), SQLSERVER(
3133
Feature.allowSquareBracketQuotation), POSTGRESQL, H2, EXASOL;
3234

3335
private final Set<Feature> lexerFeatures;
@@ -98,6 +100,14 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) {
98100
return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter);
99101
}
100102

103+
public P withDoubleQuotedStrings() {
104+
return withFeature(Feature.allowDoubleQuotedStrings, true);
105+
}
106+
107+
public P withDoubleQuotedStrings(boolean allowDoubleQuotedStrings) {
108+
return withFeature(Feature.allowDoubleQuotedStrings, allowDoubleQuotedStrings);
109+
}
110+
101111
public P withHashLineComments() {
102112
return withFeature(Feature.allowHashLineComments, true);
103113
}

src/main/java/net/sf/jsqlparser/parser/feature/Feature.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,12 @@ public enum Feature {
795795
*/
796796
allowBackslashEscapeCharacter(false),
797797

798+
/**
799+
* allows double quoted String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode);
800+
* disabled by default, where double quotes stay quoted identifiers (ANSI SQL)
801+
*/
802+
allowDoubleQuotedStrings(false),
803+
798804
/**
799805
* allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary
800806
* operator (#2507: PostgreSQL bitwise XOR / geometric intersection)

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,11 @@ TOKEN:
20772077
matchedToken.kind = squaredBracketOpenIndex;
20782078
input_stream.backup(image.length() - 1);
20792079
}
2080+
if ( configuration.getAsBoolean(Feature.allowDoubleQuotedStrings)
2081+
&& matchedToken.image.charAt(0) == '"' ) {
2082+
// `charLiteralIndex` defined in TokenManagerDeclaration above
2083+
matchedToken.kind = charLiteralIndex;
2084+
}
20802085
}
20812086
}
20822087

src/site/sphinx/usage.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ Define the Parser Features
279279

280280
JSQLParser interprets Squared Brackets ``[..]`` as Arrays, which does not work with MS SQL Server and T-SQL. Please use the Parser Features to instruct JSQLParser to read Squared Brackets as Quotes instead.
281281

282-
JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature.
282+
JSQLParser allows for standard compliant Single Quote ``'..`` Escaping. Additional Back-slash ``\..`` Escaping needs to be activated by setting the ``BackSlashEscapeCharacter`` parser feature. JSQLParser reads Double Quotes ``".."`` as quoted identifiers (ANSI SQL); reading them as String Literals (BigQuery, Spark/Databricks, MySQL default sql_mode) needs the ``DoubleQuotedStrings`` parser feature.
283283

284284
Additionally there are Features to control the Parser's effort at the cost of the performance.
285285

@@ -319,7 +319,7 @@ Additionally there are Features to control the Parser's effort at the cost of th
319319
.withBackslashEscapeCharacter(true)
320320
);
321321
322-
Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter`` and ``withHashLineComments`` (both MySQL and MariaDB syntax), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset.
322+
Instead of turning the individual Parser Features on one by one, a ``Dialect`` preset selects the features of that database dialect: ``withDialect(Dialect.MYSQL)`` turns on ``withBackslashEscapeCharacter``, ``withHashLineComments`` and ``withDoubleQuotedStrings`` (MySQL and MariaDB syntax, the latter for the default sql_mode), ``withDialect(Dialect.SQLSERVER)`` turns on ``withSquareBracketQuotation``. Features set explicitly after the dialect preset win over the preset.
323323

324324
.. code-block:: java
325325

src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import net.sf.jsqlparser.JSQLParserException;
1313
import net.sf.jsqlparser.expression.Expression;
1414
import net.sf.jsqlparser.expression.LongValue;
15+
import net.sf.jsqlparser.expression.StringValue;
1516
import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
1617
import net.sf.jsqlparser.expression.operators.arithmetic.Multiplication;
1718
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
@@ -20,6 +21,7 @@
2021
import net.sf.jsqlparser.statement.Statements;
2122
import net.sf.jsqlparser.statement.UnsupportedStatement;
2223
import net.sf.jsqlparser.statement.select.PlainSelect;
24+
import net.sf.jsqlparser.statement.select.Select;
2325
import net.sf.jsqlparser.statement.select.TableStatement;
2426
import net.sf.jsqlparser.test.MemoryLeakVerifier;
2527
import net.sf.jsqlparser.test.TestUtils;
@@ -44,6 +46,7 @@
4446
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4547
import static org.junit.jupiter.api.Assertions.assertNull;
4648
import static org.junit.jupiter.api.Assertions.assertThrows;
49+
import static org.junit.jupiter.api.Assertions.assertTrue;
4750

4851
public class CCJSqlParserUtilTest {
4952

@@ -611,4 +614,45 @@ public void testDialectPresetSwitchOverrides() throws Exception {
611614
.withDialect(AbstractJSqlParser.Dialect.MYSQL))
612615
.toString());
613616
}
617+
618+
@Test
619+
public void testDoubleQuotedStringsFeature() throws Exception {
620+
// default (flag off): double quotes are quoted identifiers, unchanged
621+
PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil
622+
.parse("SELECT \"not an identifier\"")).getSelectBody();
623+
assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column);
624+
// on: the BigQuery/Spark/MySQL-default reading, double quotes are
625+
// string literals, the quote kept on round-trip
626+
select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"not an identifier\"",
627+
p -> p.withDoubleQuotedStrings(true))).getSelectBody();
628+
Expression expression = select.getSelectItems().get(0).getExpression();
629+
assertTrue(expression instanceof StringValue);
630+
assertEquals("not an identifier", ((StringValue) expression).getValue());
631+
assertEquals("SELECT \"not an identifier\"", select.toString());
632+
// empty string and doubled quotes take the single-quote treatment
633+
assertEquals("SELECT \"\"", CCJSqlParserUtil
634+
.parse("SELECT \"\"", p -> p.withDoubleQuotedStrings(true)).toString());
635+
assertEquals("SELECT \"a\"\"b\"", CCJSqlParserUtil
636+
.parse("SELECT \"a\"\"b\"", p -> p.withDoubleQuotedStrings(true)).toString());
637+
// identifier positions: a `"..."` token follows the existing
638+
// string-as-table branch (`FROM 'file.csv'`), the same leniency
639+
// single quotes already have; MySQL itself would error here
640+
assertEquals("SELECT * FROM \"t\"", CCJSqlParserUtil
641+
.parse("SELECT * FROM \"t\"", p -> p.withDoubleQuotedStrings(true)).toString());
642+
}
643+
644+
@Test
645+
public void testDoubleQuotedStringsPreset() throws Exception {
646+
// MYSQL and MARIADB presets carry the switch (default sql_mode reading)
647+
for (AbstractJSqlParser.Dialect dialect : new AbstractJSqlParser.Dialect[] {
648+
AbstractJSqlParser.Dialect.MYSQL, AbstractJSqlParser.Dialect.MARIADB}) {
649+
PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil
650+
.parse("SELECT \"abc\"", p -> p.withDialect(dialect))).getSelectBody();
651+
assertTrue(select.getSelectItems().get(0).getExpression() instanceof StringValue);
652+
}
653+
// the identifier-default dialects keep the quoted identifier reading
654+
PlainSelect select = (PlainSelect) ((Select) CCJSqlParserUtil.parse("SELECT \"abc\"",
655+
p -> p.withDialect(AbstractJSqlParser.Dialect.SQLSERVER))).getSelectBody();
656+
assertTrue(select.getSelectItems().get(0).getExpression() instanceof Column);
657+
}
614658
}

0 commit comments

Comments
 (0)