From 5796b903d4ba4ee329acf9faa88f2f98e22c4e11 Mon Sep 17 00:00:00 2001 From: tanjiro Kamado Date: Mon, 2 Nov 2026 22:37:10 +0530 Subject: [PATCH] fix: end block comments at the first */ for dialects that do not nest consumeBlockCommentBody honoured nested block comments under every dialect, so a /* inside a comment opened another level and the comment ran on to a later */. MySQL, MariaDB, SQLite, Oracle, BigQuery and Spanner end a block comment at the first */ and run what follows it, so with Dialect.MYSQL the parser returned SELECT 1 for SELECT 1 /* a /* b */ + 2 -- */, which the server evaluates as SELECT 1 + 2. Dialect gains isNestedBlockComments(), false for those six dialects, and the token manager nests only when the configured dialect does. Without a dialect, and for PostgreSQL, SQL Server, H2 and the other dialects, nesting is unchanged. --- .../jsqlparser/parser/AbstractJSqlParser.java | 14 ++++++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 21 ++++++++--- .../statement/select/NestedCommentTest.java | 36 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java index 7cee29a5e8..c1671eadd3 100644 --- a/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java +++ b/src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java @@ -51,6 +51,11 @@ public enum Dialect { Feature.allowBackslashEscapeCharacter), SNOWFLAKE( Feature.allowBackslashEscapeCharacter), INFORMIX, SPANNER, DORIS, COCKROACHDB, TERADATA, SQLITE; + // MySQL, MariaDB, SQLite, Oracle, BigQuery and Spanner document that block comments do + // not nest: the first closing delimiter ends the comment. + private static final Set FLAT_BLOCK_COMMENT_DIALECTS = + EnumSet.of(MYSQL, MARIADB, ORACLE, SQLITE, BIGQUERY, SPANNER); + private final Set lexerFeatures; private final AdjacentStringLiterals adjacentStringLiterals; @@ -72,6 +77,15 @@ public Set getLexerFeatures() { public AdjacentStringLiterals getAdjacentStringLiterals() { return adjacentStringLiterals; } + + /** + * Whether a {@code /*} inside a block comment opens a nested comment that needs its own + * closing {@code *}{@code /}, as in the SQL standard, PostgreSQL, SQL Server or H2. The + * other dialects end the comment at the first closing delimiter. + */ + public boolean isNestedBlockComments() { + return !FLAT_BLOCK_COMMENT_DIALECTS.contains(this); + } } public P withSquareBracketQuotation() { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 087c530b43..2e939653ef 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3918,17 +3918,18 @@ TOKEN_MGR_DECLS : { /** * Consumes the body of a block comment after the opening delimiter has been matched, - * honouring nesting, up to and including the outermost closing delimiter. Then backs - * up 2 characters so the MULTI_LINE_COMMENT rule matches the closing delimiter and - * terminates the MORE chain. + * honouring nesting where the dialect nests, up to and including the outermost closing + * delimiter. Then backs up 2 characters so the MULTI_LINE_COMMENT rule matches the + * closing delimiter and terminates the MORE chain. */ void consumeBlockCommentBody() { + boolean nested = isNestedBlockCommentDialect(); int nesting = 1; char prev = 0; try { while (nesting > 0) { char c = input_stream.readChar(); - if (prev == '/' && c == '*') { + if (nested && prev == '/' && c == '*') { nesting++; prev = 0; } else if (prev == '*' && c == '/') { @@ -3944,6 +3945,18 @@ TOKEN_MGR_DECLS : { // manager raise the lexical error from IN_BLOCK_COMMENT. } } + + // Without a dialect block comments nest as in the SQL standard. A dialect whose server ends + // the comment at the first closing delimiter must not hide the SQL that follows it. + private boolean isNestedBlockCommentDialect() { + Object dialect = configuration.getValue(Feature.dialect); + for (AbstractJSqlParser.Dialect candidate : AbstractJSqlParser.Dialect.values()) { + if (candidate.name().equals(dialect)) { + return candidate.isNestedBlockComments(); + } + } + return true; + } } SKIP: diff --git a/src/test/java/net/sf/jsqlparser/statement/select/NestedCommentTest.java b/src/test/java/net/sf/jsqlparser/statement/select/NestedCommentTest.java index a470257d2b..d2223b6982 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/NestedCommentTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/NestedCommentTest.java @@ -9,9 +9,13 @@ */ package net.sf.jsqlparser.statement.select; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import static org.junit.jupiter.api.Assertions.*; @@ -102,4 +106,36 @@ void testMultilineNestedComment() { void testOracleHintPreserved() { assertParses("SELECT /*+ FULL(t) */ * FROM t"); } + + private static String parse(String sql, Dialect dialect) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)).toString(); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, + names = {"MYSQL", "MARIADB", "ORACLE", "SQLITE", "BIGQUERY", "SPANNER"}) + void testBlockCommentEndsAtFirstDelimiterWhereTheServerDoes(Dialect dialect) + throws JSQLParserException { + assertFalse(dialect.isNestedBlockComments()); + assertEquals("SELECT 1 + 2", parse("SELECT 1 /* a /* b */ + 2 -- */", dialect)); + assertEquals("SELECT 1 + 2", parse("SELECT 1 /* a /* b */ + 2", dialect)); + assertEquals("SELECT 1", parse("SELECT 1 /* a /* b */ /* c */", dialect)); + assertThrows(JSQLParserException.class, () -> parse("SELECT 1 /* a /* b */ */", dialect)); + assertThrows(JSQLParserException.class, () -> parse("SELECT 1 /* a /* b", dialect)); + } + + @ParameterizedTest + @EnumSource(value = Dialect.class, names = {"ANSI_SQL", "POSTGRESQL", "SQLSERVER", "H2"}) + void testBlockCommentNestsWhereTheServerDoes(Dialect dialect) throws JSQLParserException { + assertTrue(dialect.isNestedBlockComments()); + assertEquals("SELECT 1", parse("SELECT 1 /* a /* b */ + 2 -- */", dialect)); + assertEquals("SELECT 1 + 2", parse("SELECT 1 /* a /* b */ c */ + 2", dialect)); + assertThrows(JSQLParserException.class, () -> parse("SELECT 1 /* a /* b */", dialect)); + } + + @Test + void testBlockCommentNestsWithoutDialect() throws JSQLParserException { + assertEquals("SELECT 1", + CCJSqlParserUtil.parse("SELECT 1 /* a /* b */ + 2 -- */").toString()); + } }