Skip to content

Commit fd3779a

Browse files
committed
fix(parser): enable tagged dollar strings through PostgreSQL dialect
1 parent 08a837a commit fd3779a

5 files changed

Lines changed: 69 additions & 26 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,9 @@ array-literal ambiguity. The complete reference is on the
149149
[syntax page](https://jsqlparser.github.io/JSqlParser/syntax.html).
150150

151151
PostgreSQL dollar-quoted strings, including `$tag$…$tag$`, retain their delimiter and
152-
literal body in `StringValue`. For dialects that use the same spelling as an unquoted
153-
identifier, `parser.withDollarQuotedStringTags(false)` retains identifier parsing.
152+
literal body in `StringValue`. Tagged quotes are disabled by default to preserve
153+
identifier parsing. Enable them with `parser.withDialect(Dialect.POSTGRESQL)` or
154+
`parser.withDollarQuotedStringTags(true)`. Untagged `$$…$$` literals remain enabled.
154155

155156
## Statement classification
156157

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public enum Dialect {
3737
Feature.allowHashLineComments,
3838
Feature.allowDoubleQuotedStrings), SQLSERVER(AdjacentStringLiterals.OFF,
3939
Feature.allowSquareBracketQuotation), POSTGRESQL(
40-
AdjacentStringLiterals.NEWLINE), H2, EXASOL, BIGQUERY(
40+
AdjacentStringLiterals.NEWLINE,
41+
Feature.allowDollarQuotedStringTags), H2, EXASOL, BIGQUERY(
4142
AdjacentStringLiterals.WHITESPACE,
4243
Feature.allowDoubleQuotedStrings,
4344
Feature.allowHashLineComments,
@@ -143,7 +144,10 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) {
143144
return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter);
144145
}
145146

146-
/** Controls tagged dollar quotes; false preserves dollar-containing identifier spellings. */
147+
/**
148+
* Controls tagged dollar quotes; disabled by default, enabled by the PostgreSQL dialect preset.
149+
* False preserves dollar-containing identifier spellings.
150+
*/
147151
public P withDollarQuotedStringTags(boolean allowDollarQuotedStringTags) {
148152
return withFeature(Feature.allowDollarQuotedStringTags, allowDollarQuotedStringTags);
149153
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -809,10 +809,10 @@ public enum Feature {
809809
allowDoubleQuotedStrings(false),
810810

811811
/**
812-
* Recognizes PostgreSQL $tag$...$tag$ literals. Disable for dialects where these spellings are
813-
* unquoted identifiers. Untagged $$ literals are unaffected.
812+
* Recognizes PostgreSQL $tag$...$tag$ literals; disabled by default to preserve unquoted
813+
* identifiers, enabled by the PostgreSQL dialect preset. Untagged $$ literals are unaffected.
814814
*/
815-
allowDollarQuotedStringTags(true),
815+
allowDollarQuotedStringTags(false),
816816

817817
/**
818818
* concatenates adjacent String Literals: NEWLINE when separated by whitespace with at least one

src/test/java/net/sf/jsqlparser/expression/TaggedDollarStringTest.java

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
import java.io.StringReader;
1616
import java.nio.charset.StandardCharsets;
1717
import java.util.List;
18+
import java.util.function.Consumer;
1819
import java.util.stream.Stream;
1920
import net.sf.jsqlparser.JSQLParserException;
21+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
2022
import net.sf.jsqlparser.parser.CCJSqlParser;
2123
import net.sf.jsqlparser.parser.CCJSqlParserConstants;
2224
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
25+
import net.sf.jsqlparser.parser.StreamProvider;
2326
import net.sf.jsqlparser.parser.Token;
2427
import net.sf.jsqlparser.schema.Column;
2528
import net.sf.jsqlparser.statement.Statements;
@@ -46,7 +49,8 @@ void preservesLiteralBodiesAndDelimiters(String tag) throws Exception {
4649
"$1 $other$ こんにちは")) {
4750
String literal = delimiter + body + delimiter;
4851
String sql = "SELECT " + literal + " AS value, 2 FROM t";
49-
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql);
52+
PlainSelect select = (PlainSelect) TestUtils.assertSqlCanBeParsedAndDeparsed(sql, true,
53+
parser -> parser.withDialect(Dialect.POSTGRESQL));
5054
StringValue value =
5155
assertInstanceOf(StringValue.class, select.getSelectItem(0).getExpression());
5256
assertEquals(body, value.getValue());
@@ -55,7 +59,8 @@ void preservesLiteralBodiesAndDelimiters(String tag) throws Exception {
5559
assertEquals(literal, value.toString());
5660
StringBuilder builder = new StringBuilder();
5761
select.accept(new StatementDeParser(builder), null);
58-
PlainSelect again = (PlainSelect) CCJSqlParserUtil.parse(builder.toString());
62+
PlainSelect again = (PlainSelect) CCJSqlParserUtil.parse(builder.toString(),
63+
parser -> parser.withDialect(Dialect.POSTGRESQL));
5964
assertEquals(body, again.getSelectItem(0).getExpression(StringValue.class).getValue());
6065
assertEquals(select.toString(), builder.toString());
6166
}
@@ -65,45 +70,70 @@ void preservesLiteralBodiesAndDelimiters(String tag) throws Exception {
6570
void keepsDifferentTagsAndDollarSignsInsideBody() throws Exception {
6671
String body = "$other$ text $Tag$ $$ $1 $t";
6772
PlainSelect select =
68-
(PlainSelect) CCJSqlParserUtil.parse("SELECT $tag$" + body + "$tag$::text, $1");
73+
(PlainSelect) CCJSqlParserUtil.parse("SELECT $tag$" + body + "$tag$::text, $1",
74+
parser -> parser.withDialect(Dialect.POSTGRESQL));
6975
CastExpression cast = select.getSelectItem(0).getExpression(CastExpression.class);
7076
assertEquals(body, ((StringValue) cast.getLeftExpression()).getValue());
7177
assertInstanceOf(JdbcParameter.class, select.getSelectItem(1).getExpression());
7278
}
7379

7480
@Test
75-
void retainsIdentifiersAndSupportsOptOut() throws Exception {
81+
void retainsIdentifiersWithPostgreSqlDialect() throws Exception {
7682
PlainSelect select = (PlainSelect) CCJSqlParserUtil
77-
.parse("SELECT $parameter, foo$bar, \"$tag$abc$tag$\", $1 FROM t");
83+
.parse("SELECT $parameter, foo$bar, \"$tag$abc$tag$\", $1 FROM t",
84+
parser -> parser.withDialect(Dialect.POSTGRESQL));
7885
for (int i = 0; i < 3; i++) {
7986
assertInstanceOf(Column.class, select.getSelectItem(i).getExpression());
8087
}
8188
assertInstanceOf(JdbcParameter.class, select.getSelectItem(3).getExpression());
89+
}
90+
91+
static Stream<Consumer<CCJSqlParser>> identifierConfigurations() {
92+
return Stream.concat(
93+
Stream.<Consumer<CCJSqlParser>>of(parser -> {
94+
},
95+
parser -> parser.withDialect(Dialect.POSTGRESQL)
96+
.withDollarQuotedStringTags(false)),
97+
Stream.of(Dialect.values()).filter(dialect -> dialect != Dialect.POSTGRESQL)
98+
.map(dialect -> parser -> parser.withDialect(dialect)));
99+
}
100+
101+
@ParameterizedTest
102+
@MethodSource("identifierConfigurations")
103+
void retainsIdentifiersAndUntaggedLiterals(Consumer<CCJSqlParser> configuration)
104+
throws Exception {
82105
for (String identifier : List.of("$tag$abc$tag$", "$tag$identifier")) {
83-
PlainSelect legacy = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + identifier,
84-
parser -> parser.withDollarQuotedStringTags(false));
106+
PlainSelect select =
107+
(PlainSelect) CCJSqlParserUtil.parse("SELECT " + identifier, configuration);
85108
assertEquals(identifier,
86-
legacy.getSelectItem(0).getExpression(Column.class).getColumnName());
109+
select.getSelectItem(0).getExpression(Column.class).getColumnName());
87110
}
88111
PlainSelect untagged = (PlainSelect) CCJSqlParserUtil.parse("SELECT $$text$$",
89-
parser -> parser.withDollarQuotedStringTags(false));
112+
configuration);
90113
assertEquals("text", untagged.getSelectItem(0).getExpression(StringValue.class).getValue());
91114
}
92115

116+
@Test
117+
void supportsExplicitOptInWithoutDialect() throws Exception {
118+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT $tag$abc$tag$",
119+
parser -> parser.withDollarQuotedStringTags(true));
120+
assertEquals("abc", select.getSelectItem(0).getExpression(StringValue.class).getValue());
121+
}
122+
93123
@Test
94124
void retainsBodyWithOtherLexerOptions() throws Exception {
95125
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
96126
"SELECT $t$#hash\n\\text't$tag$ \"q\"$t$",
97-
parser -> parser
98-
.withDialect(net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect.MYSQL));
127+
parser -> parser.withDialect(Dialect.MYSQL).withDollarQuotedStringTags(true));
99128
assertEquals("#hash\n\\text't$tag$ \"q\"",
100129
select.getSelectItem(0).getExpression(StringValue.class).getValue());
101130
}
102131

103132
@Test
104133
void keepsLineColumnAndAbsoluteTokenPositions() {
105134
String literal = "$tag$a\nb$tag$";
106-
CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT " + literal + ", 2");
135+
CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT " + literal + ", 2")
136+
.withDialect(Dialect.POSTGRESQL);
107137
parser.getNextToken();
108138
Token value = parser.getNextToken();
109139
Token comma = parser.getNextToken();
@@ -124,23 +154,28 @@ void recognizesFunctionBodyAndFollowingStatement() throws Exception {
124154
String body = "SELECT 'a;''b'::text;\n";
125155
String sql =
126156
"CREATE FUNCTION f() RETURNS text AS $fn$" + body + "$fn$ LANGUAGE SQL; SELECT 42;";
127-
Statements statements = CCJSqlParserUtil.parseStatements(sql);
157+
Statements statements = CCJSqlParserUtil.parseStatements(sql,
158+
parser -> parser.withDialect(Dialect.POSTGRESQL));
128159
assertEquals(2, statements.size());
129160
assertEquals("SELECT 42", statements.get(1).toString());
130161
org.junit.jupiter.api.Assertions
131162
.assertTrue(statements.get(0).toString().contains("$fn$" + body + "$fn$"));
132-
assertEquals(2, CCJSqlParserUtil.parseStatements(statements.toString()).size());
163+
assertEquals(2, CCJSqlParserUtil.parseStatements(statements.toString(),
164+
parser -> parser.withDialect(Dialect.POSTGRESQL)).size());
133165
}
134166

135167
@Test
136168
@Timeout(10)
137169
void handlesLongBodiesAndOverlappingDelimiterPrefixes() throws Exception {
138170
String body = "$ta$tagX $tagtagX\n".repeat(12000);
139171
String sql = "SELECT $tagtag$" + body + "$tagtag$";
140-
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(new StringReader(sql));
172+
PlainSelect select =
173+
(PlainSelect) new CCJSqlParser(new StreamProvider(new StringReader(sql)))
174+
.withDialect(Dialect.POSTGRESQL).Statement();
141175
assertEquals(body, select.getSelectItem(0).getExpression(StringValue.class).getValue());
142-
PlainSelect streamed = (PlainSelect) CCJSqlParserUtil.parse(
143-
new java.io.ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8");
176+
PlainSelect streamed = (PlainSelect) CCJSqlParserUtil.newParser(
177+
new java.io.ByteArrayInputStream(sql.getBytes(StandardCharsets.UTF_8)), "UTF-8")
178+
.withDialect(Dialect.POSTGRESQL).Statement();
144179
assertEquals(body, streamed.getSelectItem(0).getExpression(StringValue.class).getValue());
145180
}
146181

@@ -149,6 +184,7 @@ void handlesLongBodiesAndOverlappingDelimiterPrefixes() throws Exception {
149184
"SELECT $a$text$b$", "SELECT $$missing"})
150185
void rejectsUnterminatedOrMismatchedTags(String sql) {
151186
assertThrows(JSQLParserException.class,
152-
() -> CCJSqlParserUtil.parse(sql, parser -> parser.withTimeOut(1000)));
187+
() -> CCJSqlParserUtil.parse(sql,
188+
parser -> parser.withDialect(Dialect.POSTGRESQL).withTimeOut(1000)));
153189
}
154190
}

src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import net.sf.jsqlparser.expression.JsonExpression;
1515
import net.sf.jsqlparser.expression.StringValue;
1616
import net.sf.jsqlparser.expression.operators.relational.Intersects;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
1718
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1819
import net.sf.jsqlparser.schema.Column;
1920
import net.sf.jsqlparser.schema.Table;
@@ -110,7 +111,8 @@ void testNextValueIssue1863() throws JSQLParserException {
110111
@Test
111112
void testDollarQuotedText() throws JSQLParserException {
112113
String sqlStr = "SELECT $tag$This\nis\na\nselect\ntest\n$tag$ from dual where a=b";
113-
PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr);
114+
PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr,
115+
parser -> parser.withDialect(Dialect.POSTGRESQL));
114116

115117
StringValue stringValue = st.getSelectItem(0).getExpression(StringValue.class);
116118

0 commit comments

Comments
 (0)