Skip to content

Commit a946e77

Browse files
authored
Split PostgreSQL hash operators from adjacent identifiers (#2588)
1 parent a206b86 commit a946e77

3 files changed

Lines changed: 126 additions & 4 deletions

File tree

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1769,6 +1769,14 @@ TOKEN_MGR_DECLS : {
17691769
token.endColumn = input_stream.getEndColumn();
17701770
}
17711771

1772+
/** Splits a longest-match identifier without losing token positions. */
1773+
private void truncateIdentifierToken(Token token, int length) {
1774+
input_stream.backup(token.image.length() - length);
1775+
token.image = token.image.substring(0, length);
1776+
token.endLine = input_stream.getEndLine();
1777+
token.endColumn = input_stream.getEndColumn();
1778+
}
1779+
17721780
/**
17731781
* Consumes the body of a block comment after the opening delimiter has been matched,
17741782
* honouring nesting, up to and including the outermost closing delimiter. Then backs
@@ -2510,8 +2518,19 @@ TOKEN:
25102518
&& Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) {
25112519
int hashIndex = matchedToken.image.indexOf('#');
25122520
if (hashIndex > 0) {
2513-
input_stream.backup(matchedToken.image.length() - hashIndex);
2514-
matchedToken.image = matchedToken.image.substring(0, hashIndex);
2521+
truncateIdentifierToken(matchedToken, hashIndex);
2522+
}
2523+
}
2524+
// PostgreSQL does not allow # in unquoted identifiers. Re-lex it as an
2525+
// operator, including #> and #>>, even when it touches the left operand.
2526+
if (matchedToken.kind == S_IDENTIFIER
2527+
&& AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect))) {
2528+
int hashIndex = matchedToken.image.indexOf('#');
2529+
if (hashIndex > 0) {
2530+
truncateIdentifierToken(matchedToken, hashIndex);
2531+
} else if (hashIndex == 0) {
2532+
truncateIdentifierToken(matchedToken, 1);
2533+
matchedToken.kind = S_HASH_OPERATOR;
25152534
}
25162535
}
25172536
}
@@ -2532,8 +2551,7 @@ TOKEN:
25322551
&& Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) {
25332552
int hashIndex = matchedToken.image.indexOf('#');
25342553
if (hashIndex > 0) {
2535-
input_stream.backup(matchedToken.image.length() - hashIndex);
2536-
matchedToken.image = matchedToken.image.substring(0, hashIndex);
2554+
truncateIdentifierToken(matchedToken, hashIndex);
25372555
}
25382556
}
25392557
}

src/site/sphinx/usage.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,11 @@ uses the existing ``Update`` model's ``fromItem`` and ``joins`` properties.
766766
Table discovery and metadata validation recognize a target alias declared in
767767
that FROM clause. Other dialects retain the existing FROM-after-SET syntax.
768768

769+
With ``Dialect.POSTGRESQL``, ``#`` terminates an unquoted identifier, so JSON
770+
operators such as ``js#>>'{a}'`` and ``js#>'{a}'`` work without surrounding
771+
spaces. Quote identifiers containing ``#``, for example ``"js#"``. Other
772+
dialects retain their existing identifier and hash-comment rules.
773+
769774
With ``Dialect.SQLSERVER``, ``PRIMARY KEY NONCLUSTERED (id)`` and
770775
``UNIQUE CLUSTERED (id)`` store their clustering option in ``Index.getClustering()``
771776
for both ``CREATE TABLE`` and ``ALTER TABLE``. Without that dialect, these words
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
import net.sf.jsqlparser.expression.JsonExpression;
15+
import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseRightShift;
16+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
17+
import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
18+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
19+
import net.sf.jsqlparser.schema.Column;
20+
import net.sf.jsqlparser.statement.select.PlainSelect;
21+
import net.sf.jsqlparser.util.deparser.StatementDeParser;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.ValueSource;
25+
26+
class PostgreSqlHashOperatorTest {
27+
private static PlainSelect parse(String sql) throws Exception {
28+
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
29+
}
30+
31+
@ParameterizedTest
32+
@ValueSource(strings = {"js#>>'{a,b,1}'", "js #>> '{a,b,1}'", "js#>> '{a,b,1}'",
33+
"t.js#>>'{a,b,1}'", "\"js#\"#>>'{a,b,1}'", "js/*comment*/#>>'{a,b,1}'"})
34+
void preservesJsonPrecedenceWithOrWithoutSpacesIssue2163(String expression) throws Exception {
35+
PlainSelect select = parse("SELECT * FROM t WHERE " + expression + " <> 'bar'");
36+
NotEqualsTo condition = (NotEqualsTo) select.getWhere();
37+
assertInstanceOf(JsonExpression.class, condition.getLeftExpression());
38+
assertTrue(select.toString().contains(" #>> "));
39+
StringBuilder output = new StringBuilder();
40+
select.accept(new StatementDeParser(output));
41+
assertEquals(select.toString(), output.toString());
42+
assertEquals(select.toString(), parse(output.toString()).toString());
43+
}
44+
45+
@ParameterizedTest
46+
@ValueSource(strings = {"js#>'{a}'", "js#>'{a}'#>>'{b}'"})
47+
void recognizesOtherHashJsonOperators(String expression) throws Exception {
48+
PlainSelect select = parse("SELECT " + expression + " FROM t");
49+
assertInstanceOf(JsonExpression.class, select.getSelectItem(0).getExpression());
50+
assertEquals(select.toString(), parse(select.toString()).toString());
51+
}
52+
53+
@Test
54+
void splitsHashXorAndPreservesQuotedIdentifiersAndLiterals() throws Exception {
55+
PlainSelect select = parse("SELECT a#b, a#2, \"a#b\", '#>>', $$a#b$$ FROM t");
56+
assertInstanceOf(Intersects.class, select.getSelectItem(0).getExpression());
57+
assertInstanceOf(Intersects.class, select.getSelectItem(1).getExpression());
58+
assertEquals("\"a#b\"", ((Column) select.getSelectItem(2).getExpression()).getColumnName());
59+
assertEquals(select.toString(), parse(select.toString()).toString());
60+
}
61+
62+
@Test
63+
void leavesLegacyAndSqlServerNamesAndMysqlCommentsIntact() throws Exception {
64+
for (Dialect dialect : Dialect.values()) {
65+
if (dialect != Dialect.POSTGRESQL && dialect != Dialect.MYSQL
66+
&& dialect != Dialect.MARIADB && dialect != Dialect.BIGQUERY) {
67+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT a#b FROM #temp",
68+
p -> p.withDialect(dialect));
69+
assertEquals("a#b",
70+
((Column) select.getSelectItem(0).getExpression()).getColumnName());
71+
}
72+
}
73+
PlainSelect legacy = (PlainSelect) CCJSqlParserUtil.parse("SELECT js#>>'{a}' FROM t");
74+
assertInstanceOf(BitwiseRightShift.class, legacy.getSelectItem(0).getExpression());
75+
PlainSelect mysql = (PlainSelect) CCJSqlParserUtil.parse("SELECT a#comment\nFROM t",
76+
p -> p.withDialect(Dialect.MYSQL));
77+
assertEquals("SELECT a FROM t", mysql.toString());
78+
}
79+
80+
@Test
81+
void keepsTokenOffsetsAfterSplittingAndAcrossStatements() throws Exception {
82+
CCJSqlParser parser = CCJSqlParserUtil.newParser("js#>>'{}'")
83+
.withDialect(Dialect.POSTGRESQL);
84+
Token name = parser.getNextToken();
85+
Token operator = parser.getNextToken();
86+
assertEquals("js", name.image);
87+
assertEquals(1, name.beginColumn);
88+
assertEquals(2, name.endColumn);
89+
assertEquals(1, name.absoluteBegin);
90+
assertEquals(3, name.absoluteEnd);
91+
assertEquals("#>>", operator.image);
92+
assertEquals(3, operator.beginColumn);
93+
assertEquals(5, operator.endColumn);
94+
assertEquals(3, operator.absoluteBegin);
95+
assertEquals(6, operator.absoluteEnd);
96+
assertEquals(2, CCJSqlParserUtil.parseStatements("SELECT js#>>'{}' FROM t; SELECT 1;",
97+
p -> p.withDialect(Dialect.POSTGRESQL)).size());
98+
}
99+
}

0 commit comments

Comments
 (0)