Skip to content

Commit 39503ef

Browse files
committed
fix(parser): support PostgreSQL # binary operator (bitwise XOR / geometric intersection)
A lone '#' now lexes as a binary operator instead of a one-character identifier. In PostgreSQL '#' is the integer bitwise exclusive OR (docs Table 9.4) and the geometric intersection of lseg / line / box (Table 9.36), see #2499. Identifier lexing of '#' is untouched by longest match: SQL Server #temp / ##global (#1197) and the JSON operators #> / #>> (#1695) lex as before. A lone '#' can no longer be an identifier: bare column / table / alias names previously parsed silently and now fail loudly; quoted forms ("#", `#`) still parse. The AST node is the dedicated Intersects operator (sibling of GeometryDistance), wired into ExpressionVisitor, the adapter, the deparser, TablesNamesFinder and the validator. Signed-off-by: Fu Dian <fudianchn@gmail.com>
1 parent 9fc38bd commit 39503ef

8 files changed

Lines changed: 130 additions & 1 deletion

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
4141
import net.sf.jsqlparser.expression.operators.relational.InExpression;
4242
import net.sf.jsqlparser.expression.operators.relational.IncludesExpression;
43+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
4344
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
4445
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
4546
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
@@ -725,6 +726,12 @@ default void visit(GeometryDistance geometryDistance) {
725726
this.visit(geometryDistance, null);
726727
}
727728

729+
<S> T visit(Intersects intersects, S context);
730+
731+
default void visit(Intersects intersects) {
732+
this.visit(intersects, null);
733+
}
734+
728735
<S> T visit(Select select, S context);
729736

730737
<S> T visit(TranscodingFunction transcodingFunction, S context);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
4545
import net.sf.jsqlparser.expression.operators.relational.InExpression;
4646
import net.sf.jsqlparser.expression.operators.relational.IncludesExpression;
47+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
4748
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
4849
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
4950
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
@@ -804,6 +805,11 @@ public <S> T visit(GeometryDistance geometryDistance, S context) {
804805
return visitBinaryExpression(geometryDistance, context);
805806
}
806807

808+
@Override
809+
public <S> T visit(Intersects intersects, S context) {
810+
return visitBinaryExpression(intersects, context);
811+
}
812+
807813
@Override
808814
public <S> T visit(Select select, S context) {
809815
if (selectVisitor != null) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.expression.operators.relational;
11+
12+
import net.sf.jsqlparser.expression.BinaryExpression;
13+
import net.sf.jsqlparser.expression.Expression;
14+
import net.sf.jsqlparser.expression.ExpressionVisitor;
15+
16+
/**
17+
* The PostgreSQL <code>#</code> binary operator: the geometric intersection of lseg / line / box
18+
* (documentation Table 9.36) and the integer bitwise exclusive OR (Table 9.4).
19+
*/
20+
public class Intersects extends BinaryExpression {
21+
22+
@Override
23+
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
24+
return expressionVisitor.visit(this, context);
25+
}
26+
27+
@Override
28+
public String getStringExpression() {
29+
return "#";
30+
}
31+
32+
@Override
33+
public Intersects withLeftExpression(Expression expression) {
34+
return (Intersects) super.withLeftExpression(expression);
35+
}
36+
37+
@Override
38+
public Intersects withRightExpression(Expression expression) {
39+
return (Intersects) super.withRightExpression(expression);
40+
}
41+
}

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
4747
import net.sf.jsqlparser.expression.operators.relational.InExpression;
4848
import net.sf.jsqlparser.expression.operators.relational.IncludesExpression;
49+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
4950
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
5051
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
5152
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
@@ -1127,6 +1128,12 @@ public <S> Void visit(JsonOperator jsonExpr, S context) {
11271128
return null;
11281129
}
11291130

1131+
@Override
1132+
public <S> Void visit(Intersects intersects, S context) {
1133+
visitBinaryExpression(intersects);
1134+
return null;
1135+
}
1136+
11301137
@Override
11311138
public <S> Void visit(AllColumns allColumns, S context) {
11321139

src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
111111
import net.sf.jsqlparser.expression.operators.relational.InExpression;
112112
import net.sf.jsqlparser.expression.operators.relational.IncludesExpression;
113+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
113114
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
114115
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
115116
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
@@ -1773,6 +1774,12 @@ public <S> StringBuilder visit(GeometryDistance geometryDistance, S context) {
17731774
return builder;
17741775
}
17751776

1777+
@Override
1778+
public <S> StringBuilder visit(Intersects intersects, S context) {
1779+
deparse(intersects, " # ", null);
1780+
return builder;
1781+
}
1782+
17761783
@Override
17771784
public <S> StringBuilder visit(TSQLLeftJoin tsqlLeftJoin, S context) {
17781785
this.deparse(tsqlLeftJoin, " *= ", null);

src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals;
108108
import net.sf.jsqlparser.expression.operators.relational.InExpression;
109109
import net.sf.jsqlparser.expression.operators.relational.IncludesExpression;
110+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
110111
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
111112
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
112113
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
@@ -815,6 +816,12 @@ public <S> Void visit(JsonOperator jsonExpr, S context) {
815816
return null;
816817
}
817818

819+
@Override
820+
public <S> Void visit(Intersects intersects, S context) {
821+
visitBinaryExpression(intersects, " # ");
822+
return null;
823+
}
824+
818825
@Override
819826
public <S> Void visit(UserVariable var, S context) {
820827
// nothing to validate
@@ -916,6 +923,10 @@ public void visit(JsonOperator jsonExpr) {
916923
visit(jsonExpr, null);
917924
}
918925

926+
public void visit(Intersects intersects) {
927+
visit(intersects, null);
928+
}
929+
919930
public void visit(UserVariable var) {
920931
visit(var, null);
921932
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,9 +487,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
487487
int prec;
488488

489489
// Named tokens: OP_SLASH(/), OP_CARET(^), K_DIV, OP_CONCAT(||),
490-
// OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>)
490+
// OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>),
491+
// S_HASH_OPERATOR(#)
491492
// String-literal tokens: *, +, -, %, & (unnamed in JavaCC grammar)
492493
if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6;
494+
else if (op == S_HASH_OPERATOR) prec = 5;
493495
else if (op == OP_CONCAT || op == OP_PIPE
494496
|| op == OP_LSHIFT || op == OP_RSHIFT) prec = 5;
495497
else {
@@ -515,6 +517,7 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
515517
Expression right = prattArithRest(PrimaryExpression(), prec + 1);
516518

517519
if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
520+
else if (op == S_HASH_OPERATOR) { Intersects r = new Intersects(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
518521
else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
519522
else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
520523
else if (op == OP_CONCAT) { Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; }
@@ -1930,6 +1933,12 @@ TOKEN:
19301933
matchedToken.kind = charLiteralIndex;
19311934
}
19321935
|
1936+
// Bare `#` as a binary operator (PostgreSQL bitwise XOR / geometric
1937+
// intersection). Declared before <S_IDENTIFIER> to win the length tie on a
1938+
// lone `#`; `#temp`-style identifiers and `#>` / `#>>` keep their usual
1939+
// lexing by longest match (#1197, #1695).
1940+
<S_HASH_OPERATOR: "#">
1941+
|
19331942
<S_IDENTIFIER: (<LETTER> (<PART_LETTER>)*) | "$" | ("$" <PART_LETTER_NO_DOLLAR> (<PART_LETTER>)*)>
19341943
| <#LETTER: <UnicodeIdentifierStart>
19351944
| <Nd> | [ "#", "_" ] // Not SQL:2016 compliant!

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import net.sf.jsqlparser.expression.Alias;
1414
import net.sf.jsqlparser.expression.JsonExpression;
1515
import net.sf.jsqlparser.expression.StringValue;
16+
import net.sf.jsqlparser.expression.operators.relational.Intersects;
1617
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1718
import net.sf.jsqlparser.schema.Column;
1819
import net.sf.jsqlparser.schema.Table;
@@ -138,4 +139,44 @@ void testQuotedIdentifier() throws JSQLParserException {
138139
Assertions.assertEquals("`This is a Test Table`", table.getName());
139140

140141
}
142+
143+
@Test
144+
public void testPostgresHashBinaryOperator() throws JSQLParserException {
145+
// PostgreSQL 18, Table 9.4 (Mathematical Operators):
146+
// integral_type # integral_type -> bitwise exclusive OR
147+
assertSqlCanBeParsedAndDeparsed("SELECT 17 # 5");
148+
// PostgreSQL 18, Table 9.36 (Geometric Operators):
149+
// geometric_type # geometric_type -> point of intersection
150+
Select select = (Select) assertSqlCanBeParsedAndDeparsed("SELECT lseg1 # lseg2");
151+
Intersects intersects = Assertions.assertInstanceOf(Intersects.class,
152+
select.getPlainSelect().getSelectItem(0).getExpression());
153+
Assertions.assertEquals("#", intersects.getStringExpression());
154+
Assertions.assertInstanceOf(Column.class, intersects.getLeftExpression());
155+
Assertions.assertInstanceOf(Column.class, intersects.getRightExpression());
156+
157+
assertSqlCanBeParsedAndDeparsed("SELECT a # (b + 1) FROM t");
158+
assertSqlCanBeParsedAndDeparsed("SELECT a # b AS x FROM t");
159+
// same tier as `&`: binds tighter than the comparison that follows
160+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a # b = 1");
161+
}
162+
163+
@Test
164+
public void testPostgresHashOperatorKeepsIdentifiersAndJsonOperators()
165+
throws JSQLParserException {
166+
// `#` stays an identifier character: SQL Server `#temp`, `##global`
167+
// (#1197), Oracle `#$tab1#` and unquoted names containing `#`
168+
assertSqlCanBeParsedAndDeparsed("SELECT #temp FROM t");
169+
assertSqlCanBeParsedAndDeparsed("SELECT ##global FROM t");
170+
assertSqlCanBeParsedAndDeparsed("SELECT #$tab1# FROM t");
171+
assertSqlCanBeParsedAndDeparsed("SELECT a#b FROM t");
172+
// `#>` / `#>>` keep their JSON lexing (#1695)
173+
assertSqlCanBeParsedAndDeparsed("SELECT data #> '{a,b}' FROM t");
174+
assertSqlCanBeParsedAndDeparsed("SELECT data #>> '{0,1}' FROM t");
175+
// a lone `#` can no longer be an identifier (alias, column or table
176+
// name), all such forms fail loudly now that `#` is an operator
177+
Assertions.assertThrows(JSQLParserException.class,
178+
() -> CCJSqlParserUtil.parse("SELECT 1 #"));
179+
Assertions.assertThrows(JSQLParserException.class,
180+
() -> CCJSqlParserUtil.parse("SELECT # FROM t"));
181+
}
141182
}

0 commit comments

Comments
 (0)