Skip to content

Commit fb1e42a

Browse files
authored
Preserve MySQL arithmetic and executable comment semantics (#2728)
Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent f0d12b3 commit fb1e42a

6 files changed

Lines changed: 447 additions & 1 deletion

File tree

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ public P withLegacyMySqlGroupBy(boolean enabled) {
109109
return withFeature(Feature.allowLegacyMySqlGroupBy, enabled);
110110
}
111111

112+
/**
113+
* Resolves MySQL executable comments against this server version (for example 90702 for 9.7.2).
114+
* Requires the MYSQL dialect. Rendered SQL reflects this version, without conditional wrappers.
115+
* Without an explicit version, conditional executable comments raise a lexical error.
116+
*
117+
* @param version major * 10000 + minor * 100 + patch, from 0 through 999999
118+
* @return this parser
119+
*/
120+
public P withMySqlServerVersion(int version) {
121+
if (version < 0 || version > 999999) {
122+
throw new IllegalArgumentException("MySQL server version must be between 0 and 999999");
123+
}
124+
return withFeature(Feature.mySqlServerVersion, (long) version);
125+
}
126+
112127
public P withTimeOut(long timeOutMillSeconds) {
113128
return withFeature(Feature.timeOut, timeOutMillSeconds);
114129
}

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

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ public class SimpleCharStream {
6060
// the `#` in the token image. Wired (with the token manager's
6161
// configuration) before parsing, null keeps this inert.
6262
static final char HASH_SUBSTITUTION = '\u0001';
63+
static final char MYSQL_MINUS_SUBSTITUTION = '\u0002';
64+
static final char MYSQL_COMMENT_ERROR = '\u0003';
6365
FeatureConfiguration featureConfiguration;
66+
private boolean mySqlExecutableComment;
67+
String mySqlCommentError;
68+
boolean mySqlMinusSubstitution;
6469

6570
/**
6671
* Constructor.
@@ -173,8 +178,24 @@ protected void FillBuff() throws java.io.IOException {
173178
* Start.
174179
*/
175180
public final char BeginToken() throws java.io.IOException {
181+
mySqlMinusSubstitution = false;
182+
if (mySqlCommentError != null) {
183+
// Parser lookahead may catch lexical errors. Keep this failure sticky until ReInit
184+
// rather than allowing a later token request to continue after the rejected marker.
185+
return MYSQL_COMMENT_ERROR;
186+
}
176187
tokenBegin = -1;
177-
char c = readChar();
188+
char c;
189+
try {
190+
c = readChar();
191+
} catch (java.io.IOException e) {
192+
if (!mySqlExecutableComment) {
193+
throw e;
194+
}
195+
// JavaCC treats exceptions from BeginToken as EOF. Emit a real token whose lexical
196+
// action reports the error, so an unclosed executable comment cannot succeed as SQL.
197+
c = mySqlCommentError("Unterminated MySQL executable comment");
198+
}
178199
tokenBegin = bufpos;
179200

180201
absoluteTokenBegin = totalCharsRead;
@@ -183,10 +204,103 @@ public final char BeginToken() throws java.io.IOException {
183204
&& featureConfiguration.getAsBoolean(Feature.allowHashLineComments)) {
184205
buffer[bufpos] = HASH_SUBSTITUTION;
185206
c = HASH_SUBSTITUTION;
207+
} else if ((c == '-' || c == '/' || c == '*') && featureConfiguration != null
208+
&& AbstractJSqlParser.Dialect.MYSQL.name()
209+
.equals(featureConfiguration.getValue(Feature.dialect))) {
210+
c = mySqlCommentBoundary(c);
186211
}
187212
return c;
188213
}
189214

215+
/** Changes only token boundaries, never characters being scanned inside strings or names. */
216+
private char mySqlCommentBoundary(char c) throws java.io.IOException {
217+
if (c == '-') {
218+
String following = peekCharacters(2);
219+
if (following.length() == 2 && following.charAt(0) == '-'
220+
&& !isMySqlCommentSpace(following.charAt(1))) {
221+
mySqlMinusSubstitution = true;
222+
buffer[bufpos] = MYSQL_MINUS_SUBSTITUTION;
223+
return MYSQL_MINUS_SUBSTITUTION;
224+
}
225+
} else if (c == '*' && mySqlExecutableComment && peekCharacters(1).equals("/")) {
226+
eraseCommentMarker(2);
227+
mySqlExecutableComment = false;
228+
return ' ';
229+
} else if (c == '/') {
230+
String following = peekCharacters(9);
231+
if (following.startsWith("*!")) {
232+
if (mySqlExecutableComment) {
233+
return mySqlCommentError("Nested MySQL executable comments are not supported");
234+
}
235+
int digits = mySqlCommentVersionLength(following);
236+
if (digits > 0) {
237+
long target = featureConfiguration.getAsLong(Feature.mySqlServerVersion);
238+
if (target < 0 || target > 999999) {
239+
return mySqlCommentError("MySQL conditional executable comments require "
240+
+ "withMySqlServerVersion(major * 10000 + minor * 100 + patch)");
241+
}
242+
int minimum = Integer.parseInt(following.substring(2, 2 + digits));
243+
if (minimum > target) {
244+
return c;
245+
}
246+
}
247+
eraseCommentMarker(3 + digits);
248+
mySqlExecutableComment = true;
249+
return ' ';
250+
}
251+
}
252+
return c;
253+
}
254+
255+
private static boolean isMySqlCommentSpace(char c) {
256+
return c <= ' ' || c == '\u007f';
257+
}
258+
259+
private static int mySqlCommentVersionLength(String following) {
260+
if (following.length() < 7) {
261+
return 0;
262+
}
263+
for (int i = 2; i < 7; i++) {
264+
if (following.charAt(i) < '0' || following.charAt(i) > '9') {
265+
return 0;
266+
}
267+
}
268+
// MySQL recognizes a sixth digit only when followed by whitespace. Otherwise the
269+
// five-digit prefix is the version and the remaining characters belong to the SQL body.
270+
return following.length() >= 9 && following.charAt(7) >= '0'
271+
&& following.charAt(7) <= '9' && (following.charAt(8) == ' '
272+
|| following.charAt(8) >= '\t' && following.charAt(8) <= '\r')
273+
? 6
274+
: 5;
275+
}
276+
277+
private String peekCharacters(int length) {
278+
StringBuilder result = new StringBuilder(length);
279+
try {
280+
for (int i = 0; i < length; i++) {
281+
result.append(readChar());
282+
}
283+
} catch (java.io.IOException ignored) {
284+
// Only the successfully read characters need to be rewound, including at EOF.
285+
}
286+
backup(result.length());
287+
return result.toString();
288+
}
289+
290+
private void eraseCommentMarker(int length) throws java.io.IOException {
291+
buffer[bufpos] = ' ';
292+
for (int i = 1; i < length; i++) {
293+
readChar();
294+
buffer[bufpos] = ' ';
295+
}
296+
backup(length - 1);
297+
}
298+
299+
private char mySqlCommentError(String message) {
300+
mySqlCommentError = message;
301+
return MYSQL_COMMENT_ERROR;
302+
}
303+
190304
protected final void UpdateLineColumn(char c) {
191305
column++;
192306

@@ -330,6 +444,8 @@ public void ReInit(Provider dstream, int startline, int startcolumn, int buffers
330444
prevCharIsLF = prevCharIsCR = false;
331445
tokenBegin = inBuf = maxNextCharInd = 0;
332446
bufpos = -1;
447+
mySqlExecutableComment = false;
448+
mySqlCommentError = null;
333449
}
334450

335451
/**
@@ -362,6 +478,8 @@ public String GetImage() {
362478
String image = doGetImage();
363479
if (!image.isEmpty() && image.charAt(0) == HASH_SUBSTITUTION) {
364480
image = "#" + image.substring(1);
481+
} else if (!image.isEmpty() && image.charAt(0) == MYSQL_MINUS_SUBSTITUTION) {
482+
image = "-" + image.substring(1);
365483
}
366484
return image;
367485
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,14 @@ public enum Feature {
820820
/** Enables legacy GROUP BY ordering with the MYSQL dialect; disabled by default. */
821821
allowLegacyMySqlGroupBy(false),
822822

823+
/**
824+
* Target MySQL version for executable comments, encoded as major * 10000 + minor * 100 + patch.
825+
* A negative value leaves the version unspecified: conditional comments then fail instead of
826+
* silently discarding SQL. Only used with the MYSQL dialect; unversioned comments need no
827+
* value.
828+
*/
829+
mySqlServerVersion(-1),
830+
823831
// PERFORMANCE
824832

825833
/**

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2227,6 +2227,12 @@ TOKEN_MGR_DECLS : {
22272227

22282228
private RowPatternTokenContext rowPatternContext;
22292229

2230+
private void reportMySqlCommentError() {
2231+
throw new TokenMgrException(input_stream.mySqlCommentError == null
2232+
? "Unexpected control character" : input_stream.mySqlCommentError,
2233+
TokenMgrException.LEXICAL_ERROR);
2234+
}
2235+
22302236
public void CommonTokenAction(Token t)
22312237
{
22322238
t.absoluteBegin = getCurrentTokenAbsolutePosition();
@@ -2917,6 +2923,18 @@ TOKEN : /* Operators */
29172923
| <OP_RSHIFT: ">>">
29182924
| <OP_JSON_PATH_DELETE: "#-">
29192925
| <OP_JSON_PATH_EXISTS: "@?">
2926+
| <OP_MINUS: "-">
2927+
| <MYSQL_MINUS: "\u0002"> {
2928+
if (!input_stream.mySqlMinusSubstitution) {
2929+
input_stream.mySqlCommentError = "Unexpected control character";
2930+
reportMySqlCommentError();
2931+
}
2932+
matchedToken.kind = OP_MINUS;
2933+
matchedToken.image = "-";
2934+
}
2935+
| <MYSQL_EXECUTABLE_COMMENT_ERROR: "\u0003"> {
2936+
reportMySqlCommentError();
2937+
}
29202938
| <OP_DOUBLEAND: "&&">
29212939
| <OP_CONTAINS: "&>">
29222940
| <OP_CONTAINEDBY: "<&">

‎src/site/sphinx/usage.rst‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,36 @@ One grammar covers every supported RDBMS, but a few pieces of syntax mean differ
863863

864864
Features set explicitly *after* the preset win over it.
865865

866+
MySQL comments
867+
~~~~~~~~~~~~~~
868+
869+
With ``Dialect.MYSQL``, ``--`` starts a line comment only when followed by
870+
whitespace or a control character. Thus ``SELECT 1--2`` contains subtraction
871+
and a negative operand. SQL inside an unversioned executable comment is parsed
872+
normally: ``SELECT 1 /*! + 2 */`` becomes ``SELECT 1 + 2``. Optimizer hints
873+
(``/*+ ... */``) remain separate from executable comments.
874+
875+
Conditional executable comments require an explicit target server version:
876+
877+
.. code-block:: java
878+
879+
Statement stmt = CCJSqlParserUtil.parse(
880+
"SELECT 1 /*!90702 + 2 */",
881+
parser -> parser.withDialect(Dialect.MYSQL).withMySqlServerVersion(90702));
882+
883+
The value is ``major * 10000 + minor * 100 + patch``; for example, ``90702``
884+
means 9.7.2 and ``260700`` means 26.7.0. Five-digit and six-digit comment version
885+
prefixes follow MySQL's whitespace rules. SQL for a newer server is ignored;
886+
SQL for the configured version or an older one participates in the AST.
887+
Both SQL renderers produce the resolved SQL for that target, without the
888+
conditional wrappers. Without a target, a conditional executable comment
889+
raises an error instead of silently discarding its SQL. This replaces the
890+
previous treatment of all executable comments as ordinary discarded comments.
891+
The same configuration applies to parsers constructed with a ``StreamProvider``
892+
or ``FeatureConfiguration``. Other dialects retain their comment behavior.
893+
Nested executable comments are rejected; nested-comment acceptance in other
894+
dialects is not a guarantee that the same SQL is valid on MySQL.
895+
866896
ALTER column names
867897
~~~~~~~~~~~~~~~~~~
868898

0 commit comments

Comments
 (0)