From afa3655e5b67547bbe529c79a42a02fe7dee7494 Mon Sep 17 00:00:00 2001 From: dngr2 Date: Tue, 18 Aug 2026 02:24:58 +0300 Subject: [PATCH] Separate JSON operators with spaces so #> deparses correctly JsonExpression.toString glued each operator directly onto its operand, so a #> ... deparsed as a#>... . Because # is a legal identifier character, that re-parses as a# > ... -- a GreaterThan, a different tree from the original JsonExpression. Emit a space on each side of the operator, matching the JsonOperator visitor. --- .../sf/jsqlparser/expression/JsonExpression.java | 5 ++++- .../expression/JsonExpressionTest.java | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java index f258e855c..7cd34b53d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java @@ -87,7 +87,10 @@ public String toString() { StringBuilder b = new StringBuilder(); b.append(expr.toString()); for (Map.Entry ident : idents) { - b.append(ident.getValue()).append(ident.getKey()); + // Separate the operator with spaces: without them `#>`/`#>>` glue onto + // the operand (e.g. `a#>'{b}'`), and since `#` is a legal identifier + // character the result re-parses as `a#` `>` `...` -- a different tree. + b.append(' ').append(ident.getValue()).append(' ').append(ident.getKey()); } return b.toString(); } diff --git a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java index 5fc72bea8..b31efae37 100644 --- a/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/JsonExpressionTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.PlainSelect; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -39,6 +40,21 @@ void testIssue1792() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + @Test + void testHashArrowOperatorsRoundTrip() throws JSQLParserException { + // #> and #>> must survive deparse+reparse: without spaces the operator glues + // onto the object (a#>'{b}') and re-parses as the comparison a# > '{b}'. + for (String sqlStr : new String[] {"SELECT a #> '{b}' FROM t", + "SELECT a #>> '{b}' FROM t"}) { + PlainSelect st = (PlainSelect) CCJSqlParserUtil.parse(sqlStr); + Assertions.assertInstanceOf(JsonExpression.class, st.getSelectItem(0).getExpression()); + PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(st.toString()); + Assertions.assertInstanceOf(JsonExpression.class, + reparsed.getSelectItem(0).getExpression(), + sqlStr + " deparsed to " + st); + } + } + @Test void testSnowflakeGetOperator() throws JSQLParserException { // https://docs.snowflake.com/en/user-guide/querying-semistructured