From 0998415ba604c41ad18f5306c3fb27277d3c7ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Mon, 12 Oct 2026 12:42:01 +0800 Subject: [PATCH 1/3] feat(parser): expose leading comments via getLeadingComments() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downstream SQL rewriters currently recover statement-leading comments from the JavaCC special-token chain via getASTNode(), whose erasure changed within 5.x and broke compiled downstream adapters. Signed-off-by: 付典 --- .../jsqlparser/parser/ASTNodeAccessImpl.java | 23 ++++++ .../parser/LeadingCommentsTest.java | 71 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java index 6ef61996b6..0df927914c 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java +++ b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.parser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -26,6 +29,26 @@ public void setASTNode(Node node) { this.node = node; } + /** + * The comments preceding this node's first token, in source order, with the original token + * images (line comments keep their line terminator). Empty for nodes built programmatically, + * which carry no AST. + */ + public List getLeadingComments() { + Node node = getASTNode(); + if (node == null) { + return new ArrayList<>(); + } + List comments = new ArrayList<>(); + for (Token specialToken = + node.jjtGetFirstToken().specialToken; specialToken != null; specialToken = + specialToken.specialToken) { + comments.add(specialToken.image); + } + Collections.reverse(comments); + return comments; + } + public StringBuilder appendTo(StringBuilder builder) { // don't add spaces around the following punctuation final Set punctuation = new TreeSet<>(Set.of(".", "[", "]")); diff --git a/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java new file mode 100644 index 0000000000..0f86af3f23 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java @@ -0,0 +1,71 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; + +public class LeadingCommentsTest { + + @Test + public void blockCommentsBeforeStatementInSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ /*+ FULL(t) */ SELECT * FROM mytable"); + assertEquals(List.of("/* keep orderby */", "/*+ FULL(t) */"), select.getLeadingComments()); + } + + @Test + public void lineCommentBeforeStatement() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("-- paging hint\nSELECT 1"); + List comments = select.getLeadingComments(); + assertEquals(1, comments.size()); + assertTrue(comments.get(0).startsWith("-- paging hint")); + } + + @Test + public void commentInsideStatementIsNotLeading() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT /* mid comment */ 1"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void statementWithoutComments() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM mytable"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void programmaticStatementHasNoLeadingComments() { + assertEquals(List.of(), new PlainSelect().getLeadingComments()); + } + + @Test + public void toStringDropsLeadingCommentsAsBefore() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ SELECT * FROM mytable"); + assertFalse(select.toString().contains("/*")); + } + + @Test + public void commentsSplitPerStatement() throws JSQLParserException { + Statements statements = CCJSqlParserUtil + .parseStatements("/* first */ SELECT 1; /* second */ SELECT 2"); + assertEquals(List.of("/* first */"), ((Select) statements.get(0)).getLeadingComments()); + assertEquals(List.of("/* second */"), ((Select) statements.get(1)).getLeadingComments()); + } +} From 7b084306944689fc248f067b38ff9339c155eee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Mon, 12 Oct 2026 15:38:19 +0800 Subject: [PATCH 2/3] feat(parser): expose leading comments via getLeadingComments() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the serial UID from 066f581e so existing serialized models remain readable. Return the original comment token images without line terminators. Signed-off-by: 付典 --- .../jsqlparser/parser/ASTNodeAccessImpl.java | 25 ++++++ .../parser/LeadingCommentsTest.java | 80 ++++++++++++++++++ .../statement/SerializationTest.java | 16 ++++ .../parser/plain-select-066f581e.ser | Bin 0 -> 2769 bytes 4 files changed, 121 insertions(+) create mode 100644 src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java create mode 100644 src/test/resources/net/sf/jsqlparser/parser/plain-select-066f581e.ser diff --git a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java index 6ef61996b6..cf8ca97ed6 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java +++ b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java @@ -9,11 +9,16 @@ */ package net.sf.jsqlparser.parser; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.TreeSet; public class ASTNodeAccessImpl implements ASTNodeAccess { + private static final long serialVersionUID = 3920262240331742793L; + private transient Node node; @Override @@ -26,6 +31,26 @@ public void setASTNode(Node node) { this.node = node; } + /** + * The comments preceding this node's first token, in source order, with the original token + * images. Line comments do not include their line terminator; callers must add a line break + * when prepending them to SQL. Empty for nodes built programmatically, which carry no AST. + */ + public List getLeadingComments() { + Node node = getASTNode(); + if (node == null) { + return new ArrayList<>(); + } + List comments = new ArrayList<>(); + for (Token specialToken = + node.jjtGetFirstToken().specialToken; specialToken != null; specialToken = + specialToken.specialToken) { + comments.add(specialToken.image); + } + Collections.reverse(comments); + return comments; + } + public StringBuilder appendTo(StringBuilder builder) { // don't add spaces around the following punctuation final Set punctuation = new TreeSet<>(Set.of(".", "[", "]")); diff --git a/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java new file mode 100644 index 0000000000..6b83c46cb5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/LeadingCommentsTest.java @@ -0,0 +1,80 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class LeadingCommentsTest { + + @Test + public void blockCommentsBeforeStatementInSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ /*+ FULL(t) */ SELECT * FROM mytable"); + assertEquals(List.of("/* keep orderby */", "/*+ FULL(t) */"), select.getLeadingComments()); + } + + @ParameterizedTest + @ValueSource(strings = {"\n", "\r\n", "\r"}) + public void lineCommentsExcludeLineTerminators(String lineTerminator) + throws JSQLParserException { + Select select = + (Select) CCJSqlParserUtil.parse("-- paging hint" + lineTerminator + "SELECT 1"); + assertEquals(List.of("-- paging hint"), select.getLeadingComments()); + } + + @Test + public void mixedCommentsRetainExactImagesAndSourceOrder() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* a */ -- b\r\n// c\r/* d */ SELECT 1"); + assertEquals(List.of("/* a */", "-- b", "// c", "/* d */"), select.getLeadingComments()); + } + + @Test + public void commentInsideStatementIsNotLeading() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT /* mid comment */ 1"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void statementWithoutComments() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM mytable"); + assertEquals(List.of(), select.getLeadingComments()); + } + + @Test + public void programmaticStatementHasNoLeadingComments() { + assertEquals(List.of(), new PlainSelect().getLeadingComments()); + } + + @Test + public void toStringDropsLeadingCommentsAsBefore() throws JSQLParserException { + Select select = (Select) CCJSqlParserUtil + .parse("/* keep orderby */ SELECT * FROM mytable"); + assertFalse(select.toString().contains("/*")); + } + + @Test + public void commentsSplitPerStatement() throws JSQLParserException { + Statements statements = CCJSqlParserUtil + .parseStatements("/* first */ SELECT 1; /* second */ SELECT 2"); + assertEquals(List.of("/* first */"), ((Select) statements.get(0)).getLeadingComments()); + assertEquals(List.of("/* second */"), ((Select) statements.get(1)).getLeadingComments()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java b/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java index 380f8a4e68..692672752f 100644 --- a/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/SerializationTest.java @@ -12,9 +12,11 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.math.BigDecimal; +import java.util.List; import java.util.stream.Stream; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.SpannerInterleaveIn; @@ -34,6 +36,20 @@ import org.junit.jupiter.params.provider.ValueSource; public class SerializationTest { + @Test + void deserializeSelectFromBeforeLeadingCommentsAccessor() throws Exception { + try (InputStream serialized = getClass().getResourceAsStream( + "/net/sf/jsqlparser/parser/plain-select-066f581e.ser")) { + Assertions.assertNotNull(serialized); + try (ObjectInputStream in = new ObjectInputStream(serialized)) { + PlainSelect select = + Assertions.assertInstanceOf(PlainSelect.class, in.readObject()); + Assertions.assertNull(select.getASTNode()); + Assertions.assertEquals(List.of(), select.getLeadingComments()); + } + } + } + @Test void serializeWithItem() throws JSQLParserException, IOException, ClassNotFoundException { String sqlStr = diff --git a/src/test/resources/net/sf/jsqlparser/parser/plain-select-066f581e.ser b/src/test/resources/net/sf/jsqlparser/parser/plain-select-066f581e.ser new file mode 100644 index 0000000000000000000000000000000000000000..51e1d7e0dc8a8f3c6d27465598b670ec3c2ede28 GIT binary patch literal 2769 zcmb7Gy>lBy6n`Tuf5b@~8v+as6e>s{Iv_Jbvv~&z5Fmyo4AHWPVbaXVlcdN4x>t;Bvy1RY9x9{V(zqeoi31zLI8ZlGV zLG?_JMeHffR8L%~Uf4%zs-?jan61?7rtw71}fGchKaGmR8 z&7^ zxd_Lx;W(LaDvf+kH5WDqTpJ$w#=yNIULP#Y3Ccol9?1bS@Ieu1HlXGbif+mQmBzr` zB0iEbBIsHm)#FffJTo$|=bm{}&k1X&!od9^ zF@0jKn<8NY;@3ys6oW8uCkM{vv0_?t89B|1SIUS-M)tf@!L)%}HlvB*!f~}j4BRz)au!a85k-8^58pxReCl8$RIVI4i-6Pim(ARZ= zY*(K6jI&wl@^-lBj)i4s3#0#7c*tM_9~SZcC=ZJpy_9i^ppx)MbRE{tpJE zt|Cfi6Nzy$Vv146HeI-d-vLv^=d`kUeal4FxdK3^8{gpX#B(P^%C6f!^o+b202F(}cjbDG8 z_I~=g1SK3gc*Uc8UvClPu-naCKCs_f*Pi0d$XcL=>^{w`Q$)3SJXNq?Alb}>Ob1~$ b9qLA9qS75MbFO{$`5*W0|Kfa^Tl9YbzW3Qo literal 0 HcmV?d00001 From 43c47d51da773b7eeaec51dc9547c6cab6aa480c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Tue, 13 Oct 2026 11:21:45 +0800 Subject: [PATCH 3/3] refactor(parser): remove the fixed serialVersionUID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding getLeadingComments changes the computed UID; previously serialized AST objects may no longer be readable. Signed-off-by: 付典 --- src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java index cf8ca97ed6..983f99711d 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java +++ b/src/main/java/net/sf/jsqlparser/parser/ASTNodeAccessImpl.java @@ -17,8 +17,6 @@ public class ASTNodeAccessImpl implements ASTNodeAccess { - private static final long serialVersionUID = 3920262240331742793L; - private transient Node node; @Override