Skip to content

Commit 98bd01d

Browse files
authored
feat(parser): support MySQL PROCEDURE ANALYSE clause (#2492)
- add ANALYSE as a non-reserved keyword - parse zero, one, or two integer parameters - prevent PROCEDURE ANALYSE from being consumed as an alias - add AST, deparser, and visitor support - add parsing and regression tests
1 parent edd5bfc commit 98bd01d

6 files changed

Lines changed: 205 additions & 1 deletion

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import java.io.Serializable;
13+
import net.sf.jsqlparser.expression.LongValue;
14+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
15+
16+
/**
17+
* Models the legacy MySQL {@code PROCEDURE ANALYSE()} clause.
18+
*
19+
* @see <a href="https://dev.mysql.com/doc/refman/5.7/en/procedure-analyse.html">MySQL 5.7 Reference
20+
* Manual</a>
21+
*/
22+
public class MySqlProcedureAnalyse extends ASTNodeAccessImpl implements Serializable {
23+
24+
private LongValue maxElements;
25+
private LongValue maxMemory;
26+
27+
public MySqlProcedureAnalyse() {}
28+
29+
public MySqlProcedureAnalyse(LongValue maxElements, LongValue maxMemory) {
30+
this.maxElements = maxElements;
31+
this.maxMemory = maxMemory;
32+
}
33+
34+
public LongValue getMaxElements() {
35+
return maxElements;
36+
}
37+
38+
public void setMaxElements(LongValue maxElements) {
39+
this.maxElements = maxElements;
40+
}
41+
42+
public MySqlProcedureAnalyse withMaxElements(LongValue maxElements) {
43+
setMaxElements(maxElements);
44+
return this;
45+
}
46+
47+
public LongValue getMaxMemory() {
48+
return maxMemory;
49+
}
50+
51+
public void setMaxMemory(LongValue maxMemory) {
52+
this.maxMemory = maxMemory;
53+
}
54+
55+
public MySqlProcedureAnalyse withMaxMemory(LongValue maxMemory) {
56+
setMaxMemory(maxMemory);
57+
return this;
58+
}
59+
60+
public StringBuilder appendTo(StringBuilder builder) {
61+
builder.append(" PROCEDURE ANALYSE(");
62+
if (maxElements != null) {
63+
builder.append(maxElements);
64+
if (maxMemory != null) {
65+
builder.append(", ").append(maxMemory);
66+
}
67+
}
68+
return builder.append(")");
69+
}
70+
71+
@Override
72+
public String toString() {
73+
return appendTo(new StringBuilder()).toString();
74+
}
75+
}

src/main/java/net/sf/jsqlparser/statement/select/Select.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp
3131
Offset offset;
3232
Fetch fetch;
3333
WithIsolation isolation;
34+
MySqlProcedureAnalyse mySqlProcedureAnalyse;
3435
boolean oracleSiblings = false;
3536

3637
ForClause forClause = null;
@@ -322,6 +323,19 @@ public Select withIsolation(WithIsolation isolation) {
322323
return this;
323324
}
324325

326+
public MySqlProcedureAnalyse getMySqlProcedureAnalyse() {
327+
return mySqlProcedureAnalyse;
328+
}
329+
330+
public void setMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) {
331+
this.mySqlProcedureAnalyse = mySqlProcedureAnalyse;
332+
}
333+
334+
public Select withMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) {
335+
setMySqlProcedureAnalyse(mySqlProcedureAnalyse);
336+
return this;
337+
}
338+
325339
public ForMode getForMode() {
326340
return this.forMode;
327341
}
@@ -521,6 +535,9 @@ public StringBuilder appendTo(StringBuilder builder) {
521535
if (offset != null) {
522536
builder.append(offset);
523537
}
538+
if (mySqlProcedureAnalyse != null) {
539+
mySqlProcedureAnalyse.appendTo(builder);
540+
}
524541
if (fetch != null) {
525542
builder.append(fetch);
526543
}

src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ public <S> T visit(PlainSelect plainSelect, S context) {
200200
if (plainSelect.getOffset() != null) {
201201
expressionVisitor.visitExpression(plainSelect.getOffset().getOffset(), context);
202202
}
203+
if (plainSelect.getMySqlProcedureAnalyse() != null) {
204+
expressionVisitor.visitExpression(
205+
plainSelect.getMySqlProcedureAnalyse().getMaxElements(), context);
206+
expressionVisitor.visitExpression(
207+
plainSelect.getMySqlProcedureAnalyse().getMaxMemory(), context);
208+
}
203209
if (plainSelect.getFetch() != null) {
204210
expressionVisitor.visitExpression(plainSelect.getFetch().getExpression(), context);
205211
}

src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,9 @@ public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
365365
if (plainSelect.getOffset() != null) {
366366
visit(plainSelect.getOffset());
367367
}
368+
if (plainSelect.getMySqlProcedureAnalyse() != null) {
369+
plainSelect.getMySqlProcedureAnalyse().appendTo(builder);
370+
}
368371
if (plainSelect.getFetch() != null) {
369372
visit(plainSelect.getFetch());
370373
}

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,11 +819,12 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
819819

820820
switch (kind) {
821821
// Safe as implicit identifiers (no clause collision)
822-
case K_TABLES: case K_OPTIMIZE: case K_PROCEDURE: case K_PUBLIC:
822+
case K_TABLES: case K_OPTIMIZE: case K_PUBLIC:
823823
case K_CASEWHEN: case K_IIF:
824824
return true;
825825

826826
// Safe when the follower doesn't form a clause
827+
case K_PROCEDURE: return nextKind != K_ANALYSE;
827828
case K_GROUP: return nextKind != K_BY;
828829
case K_ORDER: return nextKind != K_BY && nextKind != K_SIBLINGS;
829830
case K_CONNECT: return nextKind != K_BY;
@@ -1250,6 +1251,7 @@ String NonReservedWord() :
12501251
| tk=<K_ALIGN:"ALIGN">
12511252
| tk=<K_ALTER:"ALTER">
12521253
| tk=<K_ALWAYS:"ALWAYS">
1254+
| tk=<K_ANALYSE:"ANALYSE">
12531255
| tk=<K_ANALYZE:"ANALYZE">
12541256
| tk=<K_APPEND_ONLY:"APPEND_ONLY">
12551257
| tk=<K_APPLY:"APPLY">
@@ -5451,6 +5453,7 @@ PlainSelect PlainSelect() #PlainSelect:
54515453
boolean partitionByBrackets = false;
54525454
List<Table> intoTables = null;
54535455
MySqlSelectIntoClause mySqlSelectIntoClause = null;
5456+
MySqlProcedureAnalyse mySqlProcedureAnalyse = null;
54545457
Table updateTable = null;
54555458
List<Table> updateTables = new ArrayList<Table>();
54565459
Wait wait = null;
@@ -5580,6 +5583,10 @@ PlainSelect PlainSelect() #PlainSelect:
55805583
} ]
55815584
[ LOOKAHEAD(<K_OFFSET>) offset = Offset() { plainSelect.setOffset(offset); } ]
55825585
[ LOOKAHEAD(<K_LIMIT>, { limit==null }) limit = LimitWithOffset() { plainSelect.setLimit(limit); } ]
5586+
[ LOOKAHEAD(<K_PROCEDURE> <K_ANALYSE>)
5587+
mySqlProcedureAnalyse = MySqlProcedureAnalyse()
5588+
{ plainSelect.setMySqlProcedureAnalyse(mySqlProcedureAnalyse); }
5589+
]
55835590
[ LOOKAHEAD(<K_FETCH>) fetch = Fetch() { plainSelect.setFetch(fetch); } ]
55845591
[ LOOKAHEAD(<K_WITH> <K_ISOLATION>) withIsolation = WithIsolation() { plainSelect.setIsolation(withIsolation); } ]
55855592
[ LOOKAHEAD(2)
@@ -6226,6 +6233,30 @@ MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position posit
62266233
}
62276234
}
62286235

6236+
MySqlProcedureAnalyse MySqlProcedureAnalyse():
6237+
{
6238+
MySqlProcedureAnalyse procedureAnalyse = new MySqlProcedureAnalyse();
6239+
Token maxElements = null;
6240+
Token maxMemory = null;
6241+
}
6242+
{
6243+
<K_PROCEDURE> <K_ANALYSE> "("
6244+
[
6245+
maxElements=<S_LONG>
6246+
[ "," maxMemory=<S_LONG> ]
6247+
]
6248+
")"
6249+
{
6250+
if (maxElements != null) {
6251+
procedureAnalyse.setMaxElements(new LongValue(maxElements.image));
6252+
}
6253+
if (maxMemory != null) {
6254+
procedureAnalyse.setMaxMemory(new LongValue(maxMemory.image));
6255+
}
6256+
return procedureAnalyse;
6257+
}
6258+
}
6259+
62296260
void MySqlSelectIntoOutfileTail(MySqlSelectIntoClause intoClause):
62306261
{
62316262
Token token;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.select;
11+
12+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertNotNull;
15+
import static org.junit.jupiter.api.Assertions.assertNull;
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
18+
import net.sf.jsqlparser.JSQLParserException;
19+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
20+
import org.junit.jupiter.api.Test;
21+
22+
class MySqlProcedureAnalyseTest {
23+
24+
@Test
25+
void parseWithoutParameters() throws JSQLParserException {
26+
Select select = (Select) assertSqlCanBeParsedAndDeparsed(
27+
"SELECT col1, col2 FROM heavy_table PROCEDURE ANALYSE()");
28+
29+
MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse();
30+
assertNotNull(procedureAnalyse);
31+
assertNull(procedureAnalyse.getMaxElements());
32+
assertNull(procedureAnalyse.getMaxMemory());
33+
}
34+
35+
@Test
36+
void parseOneParameter() throws JSQLParserException {
37+
Select select = (Select) assertSqlCanBeParsedAndDeparsed(
38+
"SELECT col1 FROM heavy_table PROCEDURE ANALYSE(10)");
39+
40+
MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse();
41+
assertEquals("10", procedureAnalyse.getMaxElements().getStringValue());
42+
assertNull(procedureAnalyse.getMaxMemory());
43+
}
44+
45+
@Test
46+
void parseTwoParametersAfterLimit() throws JSQLParserException {
47+
Select select = (Select) assertSqlCanBeParsedAndDeparsed(
48+
"SELECT col1 FROM heavy_table LIMIT 20 PROCEDURE ANALYSE(10, 256)");
49+
50+
MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse();
51+
assertEquals("10", procedureAnalyse.getMaxElements().getStringValue());
52+
assertEquals("256", procedureAnalyse.getMaxMemory().getStringValue());
53+
}
54+
55+
@Test
56+
void keepAnalyseAndProcedureAvailableAsIdentifiers() throws JSQLParserException {
57+
assertSqlCanBeParsedAndDeparsed("SELECT analyse FROM heavy_table");
58+
assertSqlCanBeParsedAndDeparsed("SELECT * FROM heavy_table procedure");
59+
}
60+
61+
@Test
62+
void rejectInvalidForms() {
63+
assertThrows(JSQLParserException.class,
64+
() -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE"));
65+
assertThrows(JSQLParserException.class,
66+
() -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(-1)"));
67+
assertThrows(JSQLParserException.class,
68+
() -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(1, 2, 3)"));
69+
assertThrows(JSQLParserException.class,
70+
() -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYZE()"));
71+
}
72+
}

0 commit comments

Comments
 (0)