Skip to content

Commit 3dfacb7

Browse files
committed
feat(parser): support MySQL # line comments behind allowHashLineComments
The second step agreed in #2502: with Feature.allowHashLineComments (default off) a `#` runs to end of line as a comment, unconditional like MySQL itself (no blank needed, `42#24` is a comment too); with the flag off a lone `#` stays the binary operator introduced in #2507, so neither reading silently replaces the other. Mechanics: under the flag SimpleCharStream rewrites a token-start `#` in the buffer to a character no other lexical rule starts with, so the dedicated HASH_LINE_COMMENT production wins the match for every `#` form while identifier and JSON-operator lexing of the default mode stay untouched (rewriting the buffer keeps the matcher's backup / re-read arithmetic intact, and GetImage() restores the `#` in the token image). Unquoted identifiers (and @@variables) end at their first `#` via their token actions, which re-lex the remainder as the comment. Quoted forms ("#", `#`, "a#b") keep their `#` in both modes. Under the flag the statement semantics are MySQL's: `SELECT #temp FROM t` comments out the rest of the line and fails, quoted "#temp" still parses. Closes #2499, supersedes #2502. Signed-off-by: Fu Dian <fudianchn@gmail.com>
1 parent dff722b commit 3dfacb7

6 files changed

Lines changed: 173 additions & 1 deletion

File tree

src/main/java/net/sf/jsqlparser/parser/AbstractJSqlParser.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ public P withBackslashEscapeCharacter(boolean allowBackslashEscapeCharacter) {
7777
return withFeature(Feature.allowBackslashEscapeCharacter, allowBackslashEscapeCharacter);
7878
}
7979

80+
public P withHashLineComments() {
81+
return withFeature(Feature.allowHashLineComments, true);
82+
}
83+
84+
public P withHashLineComments(boolean allowHashLineComments) {
85+
return withFeature(Feature.allowHashLineComments, allowHashLineComments);
86+
}
87+
8088
public P withUnparenthesizedSubSelects() {
8189
return withFeature(Feature.allowUnparenthesizedSubSelects, true);
8290
}

src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
88
* #L%
99
*/
10-
/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 8.0.0 */
10+
/* Generated By:JavaCC: Do not edit this line by hand. SimpleCharStream.java Version 8.0.0 */
1111
package net.sf.jsqlparser.parser;
1212

13+
import net.sf.jsqlparser.parser.feature.Feature;
14+
import net.sf.jsqlparser.parser.feature.FeatureConfiguration;
15+
1316
/**
1417
* An implementation of interface CharStream, where the stream is assumed to contain only ASCII
1518
* characters (without unicode processing).
@@ -42,6 +45,17 @@ public class SimpleCharStream {
4245
int available;
4346
int tokenBegin;
4447

48+
// MySQL `#` line comments (#2499): under the flag a token-start `#` is
49+
// rewritten in the buffer to HASH_SUBSTITUTION, a character no other
50+
// lexical rule starts with, so the dedicated HASH_LINE_COMMENT production
51+
// wins the match for every `#` form (`# c`, `#c`, `#>`, ...). Rewriting
52+
// the buffer itself (instead of synthesizing reads) keeps the matcher's
53+
// backup / re-read arithmetic intact; the production's action restores
54+
// the `#` in the token image. Wired (with the token manager's
55+
// configuration) before parsing, null keeps this inert.
56+
static final char HASH_SUBSTITUTION = '\u0001';
57+
FeatureConfiguration featureConfiguration;
58+
4559
/**
4660
* Constructor.
4761
*/
@@ -159,6 +173,11 @@ public final char BeginToken() throws java.io.IOException {
159173

160174
absoluteTokenBegin = totalCharsRead;
161175

176+
if (c == '#' && featureConfiguration != null
177+
&& featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) {
178+
buffer[bufpos] = HASH_SUBSTITUTION;
179+
c = HASH_SUBSTITUTION;
180+
}
162181
return c;
163182
}
164183

@@ -325,6 +344,23 @@ public void ReInit(Provider dstream) {
325344
* Get token literal value.
326345
*/
327346
public String GetImage() {
347+
// restore the `#` rewritten to HASH_SUBSTITUTION: only the comment
348+
// token's own window starts with the sentinel (token-start rewrites
349+
// only), so this is exact and covers every consumer of GetImage.
350+
// The wiring check keeps parses that never opted in on the original
351+
// cost (the stream is only wired through the feature consumers /
352+
// withConfiguration)
353+
if (featureConfiguration == null) {
354+
return doGetImage();
355+
}
356+
String image = doGetImage();
357+
if (!image.isEmpty() && image.charAt(0) == HASH_SUBSTITUTION) {
358+
image = "#" + image.substring(1);
359+
}
360+
return image;
361+
}
362+
363+
private String doGetImage() {
328364
if (bufpos >= tokenBegin) {
329365
return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
330366
} else {

src/main/java/net/sf/jsqlparser/parser/feature/Feature.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,12 @@ public enum Feature {
795795
*/
796796
allowBackslashEscapeCharacter(false),
797797

798+
/**
799+
* allows MySQL `#` line comments; disabled by default, where a lone `#` stays the binary
800+
* operator (#2507: PostgreSQL bitwise XOR / geometric intersection)
801+
*/
802+
allowHashLineComments(false),
803+
798804
/**
799805
* allows sub selects without parentheses, e.g. `select * from dual where 1 = select 1`
800806
*/

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,17 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
9191

9292
public CCJSqlParser withConfiguration(FeatureConfiguration configuration) {
9393
token_source.configuration = configuration;
94+
jj_input_stream.featureConfiguration = configuration;
9495
return this;
9596
}
9697

9798
public FeatureConfiguration getConfiguration() {
99+
// wire the stream before parsing starts: every feature mutation goes
100+
// through here (the consumers run pre-parse), default-off parsing
101+
// never wires the stream and stays on the zero-cost path
102+
if (jj_input_stream.featureConfiguration != token_source.configuration) {
103+
jj_input_stream.featureConfiguration = token_source.configuration;
104+
}
98105
return token_source.configuration;
99106
}
100107

@@ -1883,6 +1890,19 @@ SPECIAL_TOKEN:
18831890
< LINE_COMMENT: ("--" | "//") (~["\r","\n"])*>
18841891
}
18851892

1893+
// MySQL `#` line comment (#2499), flag-gated. Under
1894+
// Feature.allowHashLineComments the stream rewrites a token-start `#` to
1895+
// SimpleCharStream.HASH_SUBSTITUTION, a character no other rule starts with,
1896+
// so this production wins the match for every `#` form (`# c`, `#c`, `#>`,
1897+
// ...) without touching the identifier and JSON-operator lexing of the
1898+
// default mode. The action restores the `#` in the token image; unquoted
1899+
// identifiers end at their first `#` via the S_IDENTIFIER action, which
1900+
// re-lexes the remainder as this comment.
1901+
SPECIAL_TOKEN:
1902+
{
1903+
< HASH_LINE_COMMENT: "\u0001" (~["\r","\n"])*>
1904+
}
1905+
18861906
// Nested block comments: /* ... /* ... */ ... */
18871907
//
18881908
// Uses a nesting counter (commentNesting in TOKEN_MGR_DECLS) and
@@ -1940,12 +1960,46 @@ TOKEN:
19401960
<S_HASH_OPERATOR: "#">
19411961
|
19421962
<S_IDENTIFIER: (<LETTER> (<PART_LETTER>)*) | "$" | ("$" <PART_LETTER_NO_DOLLAR> (<PART_LETTER>)*)>
1963+
{
1964+
// MySQL `#` line comments (#2499): under the flag an unquoted identifier
1965+
// ends at its first `#`, the rest of the line becomes a comment via the
1966+
// stream-level substitution (real MySQL reads `42#24` as `42` plus
1967+
// comment too). Quoted identifiers and strings keep their `#`.
1968+
// the wiring check keeps the flag lookup off the hot path of parses
1969+
// that never opted in (the stream is only wired through the feature
1970+
// consumers / withConfiguration); getValue avoids the String-based
1971+
// getAsBoolean roundtrip
1972+
if (input_stream.featureConfiguration != null
1973+
&& Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) {
1974+
int hashIndex = matchedToken.image.indexOf('#');
1975+
if (hashIndex > 0) {
1976+
input_stream.backup(matchedToken.image.length() - hashIndex);
1977+
matchedToken.image = matchedToken.image.substring(0, hashIndex);
1978+
}
1979+
}
1980+
}
19431981
| <#LETTER: <UnicodeIdentifierStart>
19441982
| <Nd> | [ "#", "_" ] // Not SQL:2016 compliant!
19451983
>
19461984
| <#PART_LETTER_NO_DOLLAR: <UnicodeIdentifierStart> | <UnicodeIdentifierExtend> | [ "#", "_" , "@" ] >
19471985
| <#PART_LETTER: <UnicodeIdentifierStart> | <UnicodeIdentifierExtend> | [ "$" , "#", "_" , "@" ] >
19481986
| <S_AT_IDENTIFIER: <K_AT_SIGN> (<K_AT_SIGN>)? <S_IDENTIFIER> >
1987+
{
1988+
// same truncation as S_IDENTIFIER: `@@#name` ends at the `#` under
1989+
// allowHashLineComments, the rest of the line becomes the comment
1990+
// the wiring check keeps the flag lookup off the hot path of parses
1991+
// that never opted in (the stream is only wired through the feature
1992+
// consumers / withConfiguration); getValue avoids the String-based
1993+
// getAsBoolean roundtrip
1994+
if (input_stream.featureConfiguration != null
1995+
&& Boolean.TRUE.equals(configuration.getValue(Feature.allowHashLineComments))) {
1996+
int hashIndex = matchedToken.image.indexOf('#');
1997+
if (hashIndex > 0) {
1998+
input_stream.backup(matchedToken.image.length() - hashIndex);
1999+
matchedToken.image = matchedToken.image.substring(0, hashIndex);
2000+
}
2001+
}
2002+
}
19492003

19502004
// Unicode characters and categories are defined here: https://www.unicode.org/Public/UNIDATA/UnicodeData.txt
19512005
// SQL:2016 states:

src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,54 @@ void testSingleStatementWithEmptyLines() throws JSQLParserException {
514514
+ "def'\n"
515515
+ "where id=?", true);
516516
}
517+
518+
@Test
519+
public void testHashLineCommentsFeature() throws Exception {
520+
// with the flag: MySQL line comments, `#` to end of line,
521+
// unconditional like MySQL itself (no blank needed, `42#24` is a
522+
// comment there too); the statement continues on the next line
523+
assertEquals("SELECT 42", CCJSqlParserUtil
524+
.parse("SELECT 42 # 24", p -> p.withHashLineComments(true)).toString());
525+
assertEquals("SELECT 1", CCJSqlParserUtil
526+
.parse("SELECT 1 # comment, 2", p -> p.withHashLineComments(true)).toString());
527+
assertEquals("SELECT 1", CCJSqlParserUtil
528+
.parse("SELECT 1 #comment", p -> p.withHashLineComments(true)).toString());
529+
assertEquals("SELECT 1", CCJSqlParserUtil
530+
.parse("SELECT 1 #!bang", p -> p.withHashLineComments(true)).toString());
531+
assertEquals("SELECT 42", CCJSqlParserUtil
532+
.parse("SELECT 42#24", p -> p.withHashLineComments(true)).toString());
533+
assertEquals("SELECT a", CCJSqlParserUtil
534+
.parse("SELECT a#b FROM t", p -> p.withHashLineComments(true)).toString());
535+
assertEquals("SELECT 1 FROM t", CCJSqlParserUtil
536+
.parse("SELECT 1 # c\nFROM t", p -> p.withHashLineComments(true)).toString());
537+
assertEquals("SELECT 1", CCJSqlParserUtil
538+
.parse("# leading\nSELECT 1", p -> p.withHashLineComments(true)).toString());
539+
assertEquals("SELECT 1", CCJSqlParserUtil
540+
.parse("SELECT 1 #", p -> p.withHashLineComments(true)).toString());
541+
assertEquals("SELECT 1", CCJSqlParserUtil
542+
.parse("SELECT 1 #\n", p -> p.withHashLineComments(true)).toString());
543+
// the `#>` family has no meaning in MySQL and comments out like any
544+
// other `#`; quoted forms keep their `#`
545+
assertEquals("SELECT data", CCJSqlParserUtil
546+
.parse("SELECT data #> '{a}' FROM t", p -> p.withHashLineComments(true))
547+
.toString());
548+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT '#' FROM t", true,
549+
p -> p.withHashLineComments(true));
550+
TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT \"a#b\" FROM t", true,
551+
p -> p.withHashLineComments(true));
552+
// default (flag off): the #2507 binary operator, unchanged
553+
assertEquals("SELECT 42 # 24", CCJSqlParserUtil.parse("SELECT 42 # 24").toString());
554+
assertEquals("SELECT 42#24", CCJSqlParserUtil.parse("SELECT 42#24").toString());
555+
}
556+
557+
@Test
558+
public void testHashLineCommentsMySQLStatementSemantics() throws Exception {
559+
// `#temp` is a comment under the flag, so `SELECT #temp FROM t`
560+
// loses the rest of the line and fails, exactly like MySQL; with the
561+
// flag off the same input parses as the #2507 operator expression
562+
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil
563+
.parse("SELECT #temp FROM t", p -> p.withHashLineComments(true)));
564+
assertEquals("SELECT 1 # comment",
565+
CCJSqlParserUtil.parse("SELECT 1 # comment").toString());
566+
}
517567
}

src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import net.sf.jsqlparser.JSQLParserException;
1515
import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
1616
import net.sf.jsqlparser.parser.CCJSqlParserTreeConstants;
17+
import net.sf.jsqlparser.parser.CCJSqlParser;
1718
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1819
import net.sf.jsqlparser.parser.Node;
1920
import net.sf.jsqlparser.parser.Token;
@@ -212,4 +213,21 @@ public void testSelectASTExtractWithCommentsIssue1580_2() throws JSQLParserExcep
212213
assertThat(root.jjtGetFirstToken().specialToken.image)
213214
.isEqualTo("/* I want this comment */\n");
214215
}
216+
217+
@Test
218+
public void testSelectASTHashLineCommentImage() throws Exception {
219+
// a `#` comment under allowHashLineComments is a normal special
220+
// token carrying the original `# ...` text
221+
CCJSqlParser parser = CCJSqlParserUtil.newParser("SELECT 1 # note\nFROM t");
222+
parser.withHashLineComments(true);
223+
parser.Statement();
224+
List<Token> comments = new ArrayList<>();
225+
for (Token t = parser.getASTRoot().jjtGetFirstToken(); t.next != null; t = t.next) {
226+
for (Token sp = t.specialToken; sp != null; sp = sp.specialToken) {
227+
comments.add(sp);
228+
}
229+
}
230+
231+
assertThat(comments).extracting(token -> token.image).containsExactly("# note");
232+
}
215233
}

0 commit comments

Comments
 (0)