Skip to content

Commit 8ff4f64

Browse files
authored
Support PostgreSQL tagged dollar strings and preserve literal bodies (#2572)
* Support PostgreSQL tagged dollar strings and preserve literal bodies * fix(parser): enable tagged dollar strings through PostgreSQL dialect
1 parent 5c5744e commit 8ff4f64

7 files changed

Lines changed: 287 additions & 35 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ Beyond statement shapes, the grammar handles nested sub-selects, bind parameters
149149
array-literal ambiguity. The complete reference is on the
150150
[syntax page](https://jsqlparser.github.io/JSqlParser/syntax.html).
151151

152+
PostgreSQL dollar-quoted strings, including `$tag$…$tag$`, retain their delimiter and
153+
literal body in `StringValue`. Tagged quotes are disabled by default to preserve
154+
identifier parsing. Enable them with `parser.withDialect(Dialect.POSTGRESQL)` or
155+
`parser.withDollarQuotedStringTags(true)`. Untagged `$$…$$` literals remain enabled.
156+
152157
## Statement classification
153158

154159
Any parsed statement can say what it actually does — no second parse, no visitor to write:

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ public StringValue(String escapedValue) {
4242
value = escapedValue.substring(1, escapedValue.length() - 1);
4343
quoteStr = "\"";
4444
return;
45-
} else if (escapedValue.length() >= 4 && escapedValue.startsWith("$$")
46-
&& escapedValue.endsWith("$$")) {
47-
value = escapedValue.substring(2, escapedValue.length() - 2);
48-
quoteStr = "$$";
45+
}
46+
47+
String delimiter = getDollarQuoteDelimiter(escapedValue);
48+
if (delimiter != null && escapedValue.length() >= 2 * delimiter.length()
49+
&& escapedValue.endsWith(delimiter)) {
50+
quoteStr = delimiter;
51+
value = escapedValue.substring(delimiter.length(),
52+
escapedValue.length() - delimiter.length());
4953
return;
5054
}
5155

@@ -64,6 +68,32 @@ public StringValue(String escapedValue) {
6468
value = escapedValue;
6569
}
6670

71+
/**
72+
* Returns the opening PostgreSQL dollar-quote delimiter, or null if there is none. A tag
73+
* follows unquoted identifier rules, excluding dollar signs. This method does not require the
74+
* closing delimiter or inspect the body.
75+
*/
76+
public static String getDollarQuoteDelimiter(String text) {
77+
if (text == null || text.length() < 2 || text.charAt(0) != '$') {
78+
return null;
79+
}
80+
int end = text.indexOf('$', 1);
81+
if (end < 0) {
82+
return null;
83+
}
84+
for (int i = 1; i < end;) {
85+
int character = text.codePointAt(i);
86+
boolean valid =
87+
i == 1 ? Character.isUnicodeIdentifierStart(character) || character == '_'
88+
: Character.isUnicodeIdentifierPart(character);
89+
if (!valid) {
90+
return null;
91+
}
92+
i += Character.charCount(character);
93+
}
94+
return text.substring(0, end + 1);
95+
}
96+
6797
public String getValue() {
6898
return value;
6999
}
@@ -90,6 +120,9 @@ public StringValue setQuoteStr(String quoteStr) {
90120
}
91121

92122
public String getNotExcapedValue() {
123+
if (quoteStr != null && quoteStr.startsWith("$")) {
124+
return value;
125+
}
93126
StringBuilder buffer = new StringBuilder(value);
94127
int index = 0;
95128
int deletesNum = 0;

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

Lines changed: 10 additions & 1 deletion
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,6 +144,14 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) {
143144
return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter);
144145
}
145146

147+
/**
148+
* Controls tagged dollar quotes; disabled by default, enabled by the PostgreSQL dialect preset.
149+
* False preserves dollar-containing identifier spellings.
150+
*/
151+
public P withDollarQuotedStringTags(boolean allowDollarQuotedStringTags) {
152+
return withFeature(Feature.allowDollarQuotedStringTags, allowDollarQuotedStringTags);
153+
}
154+
146155
public P withDoubleQuotedStrings() {
147156
return withFeature(Feature.allowDoubleQuotedStrings, true);
148157
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,12 @@ public enum Feature {
811811
*/
812812
allowDoubleQuotedStrings(false),
813813

814+
/**
815+
* Recognizes PostgreSQL $tag$...$tag$ literals; disabled by default to preserve unquoted
816+
* identifiers, enabled by the PostgreSQL dialect preset. Untagged $$ literals are unaffected.
817+
*/
818+
allowDollarQuotedStringTags(false),
819+
814820
/**
815821
* concatenates adjacent String Literals: NEWLINE when separated by whitespace with at least one
816822
* newline (the SQL standard and PostgreSQL), WHITESPACE across any whitespace (GoogleSQL,

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

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,40 +1685,44 @@ TOKEN_MGR_DECLS : {
16851685
return -1;
16861686
}
16871687

1688-
private static boolean endsWithDelimiter(Deque<Character> windowQueue, String delimiter) {
1689-
if (windowQueue.size() != delimiter.length()) {
1690-
return false;
1691-
}
1692-
1693-
int i = 0;
1694-
for (char ch : windowQueue) {
1695-
if (ch != delimiter.charAt(i++)) {
1696-
return false;
1688+
/** Scans a literal in linear time without tokenizing or rebuilding its whitespace. */
1689+
public void consumeDollarQuotedString(String closingQuote) {
1690+
int[] prefix = new int[closingQuote.length()];
1691+
for (int i = 1, matched = 0; i < closingQuote.length(); i++) {
1692+
while (matched > 0 && closingQuote.charAt(i) != closingQuote.charAt(matched)) {
1693+
matched = prefix[matched - 1];
16971694
}
1695+
if (closingQuote.charAt(i) == closingQuote.charAt(matched)) {
1696+
matched++;
1697+
}
1698+
prefix[i] = matched;
16981699
}
1699-
return true;
1700-
}
1701-
1702-
public void consumeDollarQuotedString(String closingQuote) {
1703-
Deque<Character> windowQueue = new ArrayDeque<Character>();
1704-
int delimiterLength = closingQuote.length();
1705-
17061700
try {
1707-
while (true) {
1701+
int matched = 0;
1702+
while (matched < closingQuote.length()) {
17081703
char ch = input_stream.readChar();
1709-
windowQueue.addLast(ch);
1710-
if (windowQueue.size() > delimiterLength) {
1711-
windowQueue.removeFirst();
1704+
while (matched > 0 && ch != closingQuote.charAt(matched)) {
1705+
matched = prefix[matched - 1];
17121706
}
1713-
if (endsWithDelimiter(windowQueue, closingQuote)) {
1714-
return;
1707+
if (ch == closingQuote.charAt(matched)) {
1708+
matched++;
17151709
}
17161710
}
17171711
} catch (java.io.IOException e) {
17181712
reportError(Math.max(closingQuote.length(), input_stream.GetImage().length()));
17191713
}
17201714
}
17211715

1716+
/** Rewinds any identifier suffix consumed by longest-match lexing before scanning the body. */
1717+
private void consumeDollarQuotedToken(Token token, String delimiter) {
1718+
input_stream.backup(token.image.length() - delimiter.length());
1719+
consumeDollarQuotedString(delimiter);
1720+
token.image = input_stream.GetImage();
1721+
token.kind = charLiteralIndex;
1722+
token.endLine = input_stream.getEndLine();
1723+
token.endColumn = input_stream.getEndColumn();
1724+
}
1725+
17221726
/**
17231727
* Consumes the body of a block comment after the opening delimiter has been matched,
17241728
* honouring nesting, up to and including the outermost closing delimiter. Then backs
@@ -2428,9 +2432,7 @@ TOKEN:
24282432
|
24292433
<S_DOLLAR_QUOTED_STRING: "$$">
24302434
{
2431-
consumeDollarQuotedString(matchedToken.image);
2432-
matchedToken.image = input_stream.GetImage();
2433-
matchedToken.kind = charLiteralIndex;
2435+
consumeDollarQuotedToken(matchedToken, matchedToken.image);
24342436
}
24352437
|
24362438
// Bare `#` as a binary operator (PostgreSQL bitwise XOR / geometric
@@ -2441,6 +2443,13 @@ TOKEN:
24412443
|
24422444
<S_IDENTIFIER: (<LETTER> (<PART_LETTER>)*) | "$" | ("$" <PART_LETTER_NO_DOLLAR> (<PART_LETTER>)*)>
24432445
{
2446+
if (matchedToken.image.charAt(0) == '$'
2447+
&& Boolean.TRUE.equals(configuration.getValue(Feature.allowDollarQuotedStringTags))) {
2448+
String delimiter = StringValue.getDollarQuoteDelimiter(matchedToken.image);
2449+
if (delimiter != null) {
2450+
consumeDollarQuotedToken(matchedToken, delimiter);
2451+
}
2452+
}
24442453
// MySQL `#` line comments (#2499): under the flag an unquoted identifier
24452454
// ends at its first `#`, the rest of the line becomes a comment via the
24462455
// stream-level substitution (real MySQL reads `42#24` as `42` plus
@@ -2449,7 +2458,7 @@ TOKEN:
24492458
// that never opted in (the stream is only wired through the feature
24502459
// consumers / withConfiguration); getValue avoids the String-based
24512460
// getAsBoolean roundtrip
2452-
if (input_stream.featureConfiguration != null
2461+
if (matchedToken.kind == S_IDENTIFIER && input_stream.featureConfiguration != null
24532462
&& Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) {
24542463
int hashIndex = matchedToken.image.indexOf('#');
24552464
if (hashIndex > 0) {
@@ -16822,7 +16831,7 @@ List<String> captureFunctionBody() {
1682216831
tokens.add(tok.image);
1682316832
}
1682416833
foundEnd |= (tok.kind == K_END)
16825-
|| ( tok.image.trim().startsWith("$$") && tok.image.trim().endsWith("$$")) ;
16834+
|| (tok.kind == S_CHAR_LITERAL && StringValue.getDollarQuoteDelimiter(tok.image) != null);
1682616835

1682716836
tok = getNextToken();
1682816837
}

0 commit comments

Comments
 (0)