From d95bd8fc0208f662e626d93dc608c0b63e1a5dfa Mon Sep 17 00:00:00 2001 From: minleejae Date: Mon, 21 Sep 2026 23:19:08 +0900 Subject: [PATCH] fix: preserve visitor context in expression deparsing Signed-off-by: minleejae --- .../util/deparser/ExpressionDeParser.java | 91 ++++++++++--------- .../util/deparser/ExpressionListDeParser.java | 6 +- .../util/deparser/LimitDeparser.java | 10 +- .../util/deparser/ExpressionContextTest.java | 89 ++++++++++++++++++ 4 files changed, 150 insertions(+), 46 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java 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 3ad46ae729..ec78778722 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -181,14 +181,13 @@ public StringBuilder visit(AliasedExpression expression, S context) { @Override public StringBuilder visit(Addition addition, S context) { - deparse(addition, " + ", null); + deparse(addition, " + ", context); return builder; } @Override public StringBuilder visit(AndExpression andExpression, S context) { - deparse(andExpression, andExpression.isUseOperator() ? " && " : " AND ", - null); + deparse(andExpression, andExpression.isUseOperator() ? " && " : " AND ", context); return builder; } @@ -225,19 +224,19 @@ public StringBuilder visit(OverlapsCondition overlapsCondition, S context) { @Override public StringBuilder visit(EqualsTo equalsTo, S context) { - deparse(equalsTo, " = ", null); + deparse(equalsTo, " = ", context); return builder; } @Override public StringBuilder visit(Division division, S context) { - deparse(division, " / ", null); + deparse(division, " / ", context); return builder; } @Override public StringBuilder visit(IntegerDivision division, S context) { - deparse(division, " DIV ", null); + deparse(division, " DIV ", context); return builder; } @@ -266,13 +265,13 @@ public StringBuilder visit(NotExpression notExpr, S context) { @Override public StringBuilder visit(BitwiseRightShift expr, S context) { - deparse(expr, " >> ", null); + deparse(expr, " >> ", context); return builder; } @Override public StringBuilder visit(BitwiseLeftShift expr, S context) { - deparse(expr, " << ", null); + deparse(expr, " << ", context); return builder; } @@ -303,13 +302,13 @@ public StringBuilder deparse( @Override public StringBuilder visit(GreaterThan greaterThan, S context) { - deparse(greaterThan, " > ", null); + deparse(greaterThan, " > ", context); return builder; } @Override public StringBuilder visit(GreaterThanEquals greaterThanEquals, S context) { - deparse(greaterThanEquals, " >= ", null); + deparse(greaterThanEquals, " >= ", context); return builder; } @@ -616,21 +615,21 @@ public StringBuilder visit(LongValue longValue, S context) { @Override public StringBuilder visit(MinorThan minorThan, S context) { - deparse(minorThan, " < ", null); + deparse(minorThan, " < ", context); return builder; } @Override public StringBuilder visit(MinorThanEquals minorThanEquals, S context) { - deparse(minorThanEquals, " <= ", null); + deparse(minorThanEquals, " <= ", context); return builder; } @Override public StringBuilder visit(Multiplication multiplication, S context) { - deparse(multiplication, " * ", null); + deparse(multiplication, " * ", context); return builder; } @@ -638,23 +637,21 @@ public StringBuilder visit(Multiplication multiplication, S context) { @Override public StringBuilder visit(NotEqualsTo notEqualsTo, S context) { deparse(notEqualsTo, - " " + notEqualsTo.getStringExpression() + " ", null); + " " + notEqualsTo.getStringExpression() + " ", context); return builder; } @Override public StringBuilder visit(DoubleAnd doubleAnd, S context) { - deparse(doubleAnd, " " + doubleAnd.getStringExpression() + " ", - null); + deparse(doubleAnd, " " + doubleAnd.getStringExpression() + " ", context); return builder; } @Override public StringBuilder visit(Contains contains, S context) { - deparse(contains, " " + contains.getStringExpression() + " ", - null); + deparse(contains, " " + contains.getStringExpression() + " ", context); return builder; } @@ -662,7 +659,7 @@ public StringBuilder visit(Contains contains, S context) { @Override public StringBuilder visit(ContainedBy containedBy, S context) { deparse(containedBy, - " " + containedBy.getStringExpression() + " ", null); + " " + containedBy.getStringExpression() + " ", context); return builder; } @@ -676,14 +673,14 @@ public StringBuilder visit(NullValue nullValue, S context) { @Override public StringBuilder visit(OrExpression orExpression, S context) { - deparse(orExpression, " OR ", null); + deparse(orExpression, " OR ", context); return builder; } @Override public StringBuilder visit(XorExpression xorExpression, S context) { - deparse(xorExpression, " XOR ", null); + deparse(xorExpression, " XOR ", context); return builder; } @@ -708,7 +705,7 @@ public StringBuilder visit(BooleanValue booleanValue, S context) { @Override public StringBuilder visit(Subtraction subtraction, S context) { - deparse(subtraction, " - ", null); + deparse(subtraction, " - ", context); return builder; } @@ -727,7 +724,7 @@ public StringBuilder visit(Select select, S context) { builder.append("WITH "); for (Iterator> iter = select.getWithItemsList().iterator(); iter .hasNext();) { - iter.next().accept(selectVisitor, null); + iter.next().accept(selectVisitor, context); if (iter.hasNext()) { builder.append(", "); } @@ -736,7 +733,7 @@ public StringBuilder visit(Select select, S context) { builder.append(" "); } - select.accept(selectVisitor, null); + select.accept(selectVisitor, context); } return builder; } @@ -908,6 +905,15 @@ public StringBuilder visit(Column tableColumn, S context) { return builder; } + private void deParseOrderByElement(OrderByDeParser deParser, + OrderByElement element, S context) { + if (context == null) { + deParser.deParseElement(element); + } else { + deParser.deParseElement(element, context); + } + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public StringBuilder visit(Function function, S context) { @@ -966,7 +972,7 @@ public StringBuilder visit(Function function, S context) { } else { comma = true; } - orderByDeParser.deParseElement(orderByElement); + deParseOrderByElement(orderByDeParser, orderByElement, context); } } @@ -975,7 +981,7 @@ public StringBuilder visit(Function function, S context) { } if (function.getLimit() != null) { - new LimitDeparser(this, builder).deParse(function.getLimit()); + new LimitDeparser(this, builder).deParse(function.getLimit(), context); } // Generic keyword arguments (e.g. SEPARATOR ',', USING utf8) @@ -1096,7 +1102,7 @@ public StringBuilder visit(AnyComparisonExpression anyComparisonExpression, @Override public StringBuilder visit(Concat concat, S context) { - deparse(concat, " || ", null); + deparse(concat, " || ", context); return builder; } @@ -1151,25 +1157,25 @@ public void visit(Concat concat) { @Override public StringBuilder visit(Matches matches, S context) { - deparse(matches, " @@ ", null); + deparse(matches, " @@ ", context); return builder; } @Override public StringBuilder visit(BitwiseAnd bitwiseAnd, S context) { - deparse(bitwiseAnd, " & ", null); + deparse(bitwiseAnd, " & ", context); return builder; } @Override public StringBuilder visit(BitwiseOr bitwiseOr, S context) { - deparse(bitwiseOr, " | ", null); + deparse(bitwiseOr, " | ", context); return builder; } @Override public StringBuilder visit(BitwiseXor bitwiseXor, S context) { - deparse(bitwiseXor, " ^ ", null); + deparse(bitwiseXor, " ^ ", context); return builder; } @@ -1202,7 +1208,7 @@ public StringBuilder visit(CastExpression cast, S context) { @Override public StringBuilder visit(Modulo modulo, S context) { - deparse(modulo, " % ", null); + deparse(modulo, " % ", context); return builder; } @@ -1269,7 +1275,7 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) } if (analyticExpression.getLimit() != null) { - new LimitDeparser(this, builder).deParse(analyticExpression.getLimit()); + new LimitDeparser(this, builder).deParse(analyticExpression.getLimit(), context); } builder.append(") "); @@ -1347,7 +1353,7 @@ public StringBuilder visit(AnalyticExpression analyticExpression, S context) if (i > 0) { builder.append(", "); } - orderByDeParser.deParseElement(orderByElements.get(i)); + deParseOrderByElement(orderByDeParser, orderByElements.get(i), context); } } @@ -1446,7 +1452,8 @@ public StringBuilder visit(OracleHierarchicalExpression hierarchicalExpressi @Override public StringBuilder visit(RegExpMatchOperator regExpMatchOperator, S context) { - deparse(regExpMatchOperator, " " + regExpMatchOperator.getStringExpression() + " ", null); + deparse(regExpMatchOperator, " " + regExpMatchOperator.getStringExpression() + " ", + context); return builder; } @@ -1459,7 +1466,7 @@ public StringBuilder visit(JsonExpression jsonExpr, S context) { @Override public StringBuilder visit(JsonOperator jsonExpr, S context) { - deparse(jsonExpr, " " + jsonExpr.getStringExpression() + " ", null); + deparse(jsonExpr, " " + jsonExpr.getStringExpression() + " ", context); return builder; } @@ -1497,7 +1504,7 @@ public StringBuilder visit(MySQLGroupConcat groupConcat, S context) { public StringBuilder visit(ExpressionList expressionList, S context) { ExpressionListDeParser expressionListDeParser = new ExpressionListDeParser<>(this, builder); - expressionListDeParser.deParse(expressionList); + expressionListDeParser.deParse(expressionList, context); return builder; } @@ -1508,7 +1515,7 @@ public StringBuilder visit(RowConstructor rowConstructor, S context) { } ExpressionListDeParser expressionListDeParser = new ExpressionListDeParser<>(this, builder); - expressionListDeParser.deParse(rowConstructor); + expressionListDeParser.deParse(rowConstructor, context); return builder; } @@ -1827,25 +1834,25 @@ public StringBuilder visit(IsDistinctExpression isDistinctExpression, S cont @Override public StringBuilder visit(GeometryDistance geometryDistance, S context) { deparse(geometryDistance, - " " + geometryDistance.getStringExpression() + " ", null); + " " + geometryDistance.getStringExpression() + " ", context); return builder; } @Override public StringBuilder visit(Intersects intersects, S context) { - deparse(intersects, " # ", null); + deparse(intersects, " # ", context); return builder; } @Override public StringBuilder visit(TSQLLeftJoin tsqlLeftJoin, S context) { - this.deparse(tsqlLeftJoin, " *= ", null); + this.deparse(tsqlLeftJoin, " *= ", context); return builder; } @Override public StringBuilder visit(TSQLRightJoin tsqlRightJoin, S context) { - this.deparse(tsqlRightJoin, " =* ", null); + this.deparse(tsqlRightJoin, " =* ", context); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java index 62b4c829bf..ee32957246 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionListDeParser.java @@ -31,6 +31,10 @@ public ExpressionListDeParser(ExpressionVisitor expressionVisitor @Override public void deParse(ExpressionList expressionList) { + deParse(expressionList, null); + } + + public void deParse(ExpressionList expressionList, S context) { // @todo: remove this NameExpressionList related part String comma = expressionList instanceof NamedExpressionList ? " " @@ -55,7 +59,7 @@ public void deParse(ExpressionList expressionList) { builder.append(name); builder.append(" "); } - expression.accept(expressionVisitor, null); + expression.accept(expressionVisitor, context); i++; } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java b/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java index ff6b6dbf0e..30d4b7ff5e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/LimitDeparser.java @@ -22,6 +22,10 @@ public LimitDeparser(ExpressionVisitor expressionVisitor, StringB @Override public void deParse(Limit limit) { + deParse(limit, null); + } + + public void deParse(Limit limit, S context) { builder.append(" LIMIT "); if (limit.isLimitNull()) { builder.append("NULL"); @@ -30,19 +34,19 @@ public void deParse(Limit limit) { builder.append("ALL"); } else { if (null != limit.getOffset()) { - limit.getOffset().accept(expressionVisitor, null); + limit.getOffset().accept(expressionVisitor, context); builder.append(", "); } if (null != limit.getRowCount()) { - limit.getRowCount().accept(expressionVisitor, null); + limit.getRowCount().accept(expressionVisitor, context); } } } if (limit.getByExpressions() != null) { builder.append(" BY "); - limit.getByExpressions().accept(expressionVisitor, null); + limit.getByExpressions().accept(expressionVisitor, context); } } diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java new file mode 100644 index 0000000000..7ebc9af17f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/ExpressionContextTest.java @@ -0,0 +1,89 @@ +/*- + * #%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.util.deparser; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.select.Limit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class ExpressionContextTest { + @ParameterizedTest + @ValueSource(strings = {"7 + 8 * 9", "COALESCE(7, ABS(8 + 9))", + "CASE WHEN 7 < 8 AND 9 <> 10 THEN 11 ELSE 12 END", + "(7, 8, 9)", "7 IN (8, 9)", "7 BETWEEN 8 AND 9", + "7 / 8 - 9 % 10", "7 || 8", "7 & 8 | 9 ^ 10", + "sum(7) FILTER (WHERE 8 > 9) OVER (PARTITION BY 10 ORDER BY 11)", + "array_agg(7 ORDER BY 8 LIMIT 9)"}) + void preservesCallerContextThroughNestedExpressions(String sql) throws Exception { + Object context = new Object(); + List seen = new ArrayList<>(); + StringBuilder output = new StringBuilder(); + ExpressionDeParser visitor = visitor(output, seen, context); + Expression expression = CCJSqlParserUtil.parseExpression(sql); + expression.accept(visitor, context); + assertFalse(seen.isEmpty()); + assertEquals(expression.toString(), output.toString()); + assertEquals(output.toString(), + CCJSqlParserUtil.parseExpression(output.toString()).toString()); + } + + @Test + void helpersRetainLegacyNullContextAndForwardExplicitContext() { + Object context = new Object(); + List seen = new ArrayList<>(); + StringBuilder out = new StringBuilder(); + ParenthesedExpressionList values = + new ParenthesedExpressionList<>(new LongValue(7), new LongValue(8)); + new ExpressionListDeParser<>(visitor(out, seen, context), out).deParse(values, context); + assertEquals(List.of(7L, 8L), seen); + out.setLength(0); + seen.clear(); + new ExpressionListDeParser<>(visitor(out, seen, null), out).deParse(values); + assertEquals("(7, 8)", out.toString()); + Limit limit = new Limit().withOffset(new LongValue(2)).withRowCount(new LongValue(3)); + out.setLength(0); + seen.clear(); + new LimitDeparser(visitor(out, seen, context), out).deParse(limit, context); + assertEquals(List.of(2L, 3L), seen); + out.setLength(0); + new LimitDeparser(visitor(out, new ArrayList<>(), null), out).deParse(limit); + assertEquals(" LIMIT 2, 3", out.toString()); + } + + @Test + void legacyExpressionEntryPointStillUsesNullContext() throws Exception { + StringBuilder out = new StringBuilder(); + List seen = new ArrayList<>(); + CCJSqlParserUtil.parseExpression("COALESCE(7 + 8, 9)") + .accept(visitor(out, seen, null)); + assertEquals(List.of(7L, 8L, 9L), seen); + } + + private static ExpressionDeParser visitor(StringBuilder out, List seen, Object expected) { + ExpressionDeParser visitor = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + assertSame(expected, context); + seen.add(value.getValue()); + return getBuilder().append(value.getValue()); + } + }; + visitor.setBuilder(out); + return visitor; + } +}