diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 2eb909a98..adbe17e8d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -40,6 +40,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -725,6 +726,12 @@ default void visit(GeometryDistance geometryDistance) { this.visit(geometryDistance, null); } + T visit(Intersects intersects, S context); + + default void visit(Intersects intersects) { + this.visit(intersects, null); + } + T visit(Select select, S context); T visit(TranscodingFunction transcodingFunction, S context); diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 274d4e00a..853ffbfe9 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -44,6 +44,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -804,6 +805,11 @@ public T visit(GeometryDistance geometryDistance, S context) { return visitBinaryExpression(geometryDistance, context); } + @Override + public T visit(Intersects intersects, S context) { + return visitBinaryExpression(intersects, context); + } + @Override public T visit(Select select, S context) { if (selectVisitor != null) { diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java new file mode 100644 index 000000000..f85e5b338 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/Intersects.java @@ -0,0 +1,41 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0. + * #L% + */ +package net.sf.jsqlparser.expression.operators.relational; + +import net.sf.jsqlparser.expression.BinaryExpression; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; + +/** + * The PostgreSQL # binary operator: the geometric intersection of lseg / line / box + * (documentation Table 9.36) and the integer bitwise exclusive OR (Table 9.4). + */ +public class Intersects extends BinaryExpression { + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String getStringExpression() { + return "#"; + } + + @Override + public Intersects withLeftExpression(Expression expression) { + return (Intersects) super.withLeftExpression(expression); + } + + @Override + public Intersects withRightExpression(Expression expression) { + return (Intersects) super.withRightExpression(expression); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 471a971a4..0e94b9c9e 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -46,6 +46,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -1127,6 +1128,12 @@ public Void visit(JsonOperator jsonExpr, S context) { return null; } + @Override + public Void visit(Intersects intersects, S context) { + visitBinaryExpression(intersects); + return null; + } + @Override public Void visit(AllColumns allColumns, S context) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 8328dc799..05f251019 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -110,6 +110,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -1773,6 +1774,12 @@ public StringBuilder visit(GeometryDistance geometryDistance, S context) { return builder; } + @Override + public StringBuilder visit(Intersects intersects, S context) { + deparse(intersects, " # ", null); + return builder; + } + @Override public StringBuilder visit(TSQLLeftJoin tsqlLeftJoin, S context) { this.deparse(tsqlLeftJoin, " *= ", null); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index e44fee241..3f35986ee 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -107,6 +107,7 @@ import net.sf.jsqlparser.expression.operators.relational.GreaterThanEquals; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.IncludesExpression; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression; import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; @@ -815,6 +816,12 @@ public Void visit(JsonOperator jsonExpr, S context) { return null; } + @Override + public Void visit(Intersects intersects, S context) { + visitBinaryExpression(intersects, " # "); + return null; + } + @Override public Void visit(UserVariable var, S context) { // nothing to validate @@ -916,6 +923,10 @@ public void visit(JsonOperator jsonExpr) { visit(jsonExpr, null); } + public void visit(Intersects intersects) { + visit(intersects, null); + } + public void visit(UserVariable var) { visit(var, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index de05f830d..9ea69f37b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -487,9 +487,11 @@ public class CCJSqlParser extends AbstractJSqlParser { int prec; // Named tokens: OP_SLASH(/), OP_CARET(^), K_DIV, OP_CONCAT(||), - // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>) + // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>), + // S_HASH_OPERATOR(#) // String-literal tokens: *, +, -, %, & (unnamed in JavaCC grammar) if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6; + else if (op == S_HASH_OPERATOR) prec = 5; else if (op == OP_CONCAT || op == OP_PIPE || op == OP_LSHIFT || op == OP_RSHIFT) prec = 5; else { @@ -515,6 +517,7 @@ public class CCJSqlParser extends AbstractJSqlParser { Expression right = prattArithRest(PrimaryExpression(), prec + 1); if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == S_HASH_OPERATOR) { Intersects r = new Intersects(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } 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; } else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_CONCAT) { Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } @@ -1930,6 +1933,12 @@ TOKEN: matchedToken.kind = charLiteralIndex; } | +// Bare `#` as a binary operator (PostgreSQL bitwise XOR / geometric +// intersection). Declared before to win the length tie on a +// lone `#`; `#temp`-style identifiers and `#>` / `#>>` keep their usual +// lexing by longest match (#1197, #1695). + +| ()*) | "$" | ("$" ()*)> | <#LETTER: | | [ "#", "_" ] // Not SQL:2016 compliant! diff --git a/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java b/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java index 9ded16786..218cb580d 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/PostgresTest.java @@ -13,6 +13,7 @@ import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.JsonExpression; import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.Intersects; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; @@ -138,4 +139,44 @@ void testQuotedIdentifier() throws JSQLParserException { Assertions.assertEquals("`This is a Test Table`", table.getName()); } + + @Test + public void testPostgresHashBinaryOperator() throws JSQLParserException { + // PostgreSQL 18, Table 9.4 (Mathematical Operators): + // integral_type # integral_type -> bitwise exclusive OR + assertSqlCanBeParsedAndDeparsed("SELECT 17 # 5"); + // PostgreSQL 18, Table 9.36 (Geometric Operators): + // geometric_type # geometric_type -> point of intersection + Select select = (Select) assertSqlCanBeParsedAndDeparsed("SELECT lseg1 # lseg2"); + Intersects intersects = Assertions.assertInstanceOf(Intersects.class, + select.getPlainSelect().getSelectItem(0).getExpression()); + Assertions.assertEquals("#", intersects.getStringExpression()); + Assertions.assertInstanceOf(Column.class, intersects.getLeftExpression()); + Assertions.assertInstanceOf(Column.class, intersects.getRightExpression()); + + assertSqlCanBeParsedAndDeparsed("SELECT a # (b + 1) FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT a # b AS x FROM t"); + // same tier as `&`: binds tighter than the comparison that follows + assertSqlCanBeParsedAndDeparsed("SELECT * FROM t WHERE a # b = 1"); + } + + @Test + public void testPostgresHashOperatorKeepsIdentifiersAndJsonOperators() + throws JSQLParserException { + // `#` stays an identifier character: SQL Server `#temp`, `##global` + // (#1197), Oracle `#$tab1#` and unquoted names containing `#` + assertSqlCanBeParsedAndDeparsed("SELECT #temp FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT ##global FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT #$tab1# FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT a#b FROM t"); + // `#>` / `#>>` keep their JSON lexing (#1695) + assertSqlCanBeParsedAndDeparsed("SELECT data #> '{a,b}' FROM t"); + assertSqlCanBeParsedAndDeparsed("SELECT data #>> '{0,1}' FROM t"); + // a lone `#` can no longer be an identifier (alias, column or table + // name), all such forms fail loudly now that `#` is an operator + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT 1 #")); + Assertions.assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT # FROM t")); + } }