From ec40b895525ef6e65156e0a5091fe7fa80c0595a Mon Sep 17 00:00:00 2001 From: dengliming Date: Sat, 22 Aug 2026 09:11:10 +0800 Subject: [PATCH] feat(parser): support MySQL PROCEDURE ANALYSE clause - 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 --- .../select/MySqlProcedureAnalyse.java | 75 +++++++++++++++++++ .../jsqlparser/statement/select/Select.java | 17 +++++ .../select/SelectVisitorAdapter.java | 6 ++ .../util/deparser/SelectDeParser.java | 3 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 33 +++++++- .../select/MySqlProcedureAnalyseTest.java | 72 ++++++++++++++++++ 6 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java b/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java new file mode 100644 index 000000000..b70f2fdc4 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyse.java @@ -0,0 +1,75 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import java.io.Serializable; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * Models the legacy MySQL {@code PROCEDURE ANALYSE()} clause. + * + * @see MySQL 5.7 Reference + * Manual + */ +public class MySqlProcedureAnalyse extends ASTNodeAccessImpl implements Serializable { + + private LongValue maxElements; + private LongValue maxMemory; + + public MySqlProcedureAnalyse() {} + + public MySqlProcedureAnalyse(LongValue maxElements, LongValue maxMemory) { + this.maxElements = maxElements; + this.maxMemory = maxMemory; + } + + public LongValue getMaxElements() { + return maxElements; + } + + public void setMaxElements(LongValue maxElements) { + this.maxElements = maxElements; + } + + public MySqlProcedureAnalyse withMaxElements(LongValue maxElements) { + setMaxElements(maxElements); + return this; + } + + public LongValue getMaxMemory() { + return maxMemory; + } + + public void setMaxMemory(LongValue maxMemory) { + this.maxMemory = maxMemory; + } + + public MySqlProcedureAnalyse withMaxMemory(LongValue maxMemory) { + setMaxMemory(maxMemory); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(" PROCEDURE ANALYSE("); + if (maxElements != null) { + builder.append(maxElements); + if (maxMemory != null) { + builder.append(", ").append(maxMemory); + } + } + return builder.append(")"); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/select/Select.java b/src/main/java/net/sf/jsqlparser/statement/select/Select.java index 501e21728..d3750c98e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/Select.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/Select.java @@ -31,6 +31,7 @@ public abstract class Select extends ASTNodeAccessImpl implements Statement, Exp Offset offset; Fetch fetch; WithIsolation isolation; + MySqlProcedureAnalyse mySqlProcedureAnalyse; boolean oracleSiblings = false; ForClause forClause = null; @@ -322,6 +323,19 @@ public Select withIsolation(WithIsolation isolation) { return this; } + public MySqlProcedureAnalyse getMySqlProcedureAnalyse() { + return mySqlProcedureAnalyse; + } + + public void setMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) { + this.mySqlProcedureAnalyse = mySqlProcedureAnalyse; + } + + public Select withMySqlProcedureAnalyse(MySqlProcedureAnalyse mySqlProcedureAnalyse) { + setMySqlProcedureAnalyse(mySqlProcedureAnalyse); + return this; + } + public ForMode getForMode() { return this.forMode; } @@ -521,6 +535,9 @@ public StringBuilder appendTo(StringBuilder builder) { if (offset != null) { builder.append(offset); } + if (mySqlProcedureAnalyse != null) { + mySqlProcedureAnalyse.appendTo(builder); + } if (fetch != null) { builder.append(fetch); } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index 9b0eb33a6..962e73719 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -200,6 +200,12 @@ public T visit(PlainSelect plainSelect, S context) { if (plainSelect.getOffset() != null) { expressionVisitor.visitExpression(plainSelect.getOffset().getOffset(), context); } + if (plainSelect.getMySqlProcedureAnalyse() != null) { + expressionVisitor.visitExpression( + plainSelect.getMySqlProcedureAnalyse().getMaxElements(), context); + expressionVisitor.visitExpression( + plainSelect.getMySqlProcedureAnalyse().getMaxMemory(), context); + } if (plainSelect.getFetch() != null) { expressionVisitor.visitExpression(plainSelect.getFetch().getExpression(), context); } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java index 066d40d06..24fea616a 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/SelectDeParser.java @@ -365,6 +365,9 @@ public StringBuilder visit(PlainSelect plainSelect, S context) { if (plainSelect.getOffset() != null) { visit(plainSelect.getOffset()); } + if (plainSelect.getMySqlProcedureAnalyse() != null) { + plainSelect.getMySqlProcedureAnalyse().appendTo(builder); + } if (plainSelect.getFetch() != null) { visit(plainSelect.getFetch()); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f6c400a45..4e2092ec0 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -819,11 +819,12 @@ public class CCJSqlParser extends AbstractJSqlParser { switch (kind) { // Safe as implicit identifiers (no clause collision) - case K_TABLES: case K_OPTIMIZE: case K_PROCEDURE: case K_PUBLIC: + case K_TABLES: case K_OPTIMIZE: case K_PUBLIC: case K_CASEWHEN: case K_IIF: return true; // Safe when the follower doesn't form a clause + case K_PROCEDURE: return nextKind != K_ANALYSE; case K_GROUP: return nextKind != K_BY; case K_ORDER: return nextKind != K_BY && nextKind != K_SIBLINGS; case K_CONNECT: return nextKind != K_BY; @@ -1250,6 +1251,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -5451,6 +5453,7 @@ PlainSelect PlainSelect() #PlainSelect: boolean partitionByBrackets = false; List intoTables = null; MySqlSelectIntoClause mySqlSelectIntoClause = null; + MySqlProcedureAnalyse mySqlProcedureAnalyse = null; Table updateTable = null; List
updateTables = new ArrayList
(); Wait wait = null; @@ -5580,6 +5583,10 @@ PlainSelect PlainSelect() #PlainSelect: } ] [ LOOKAHEAD() offset = Offset() { plainSelect.setOffset(offset); } ] [ LOOKAHEAD(, { limit==null }) limit = LimitWithOffset() { plainSelect.setLimit(limit); } ] + [ LOOKAHEAD( ) + mySqlProcedureAnalyse = MySqlProcedureAnalyse() + { plainSelect.setMySqlProcedureAnalyse(mySqlProcedureAnalyse); } + ] [ LOOKAHEAD() fetch = Fetch() { plainSelect.setFetch(fetch); } ] [ LOOKAHEAD( ) withIsolation = WithIsolation() { plainSelect.setIsolation(withIsolation); } ] [ LOOKAHEAD(2) @@ -6226,6 +6233,30 @@ MySqlSelectIntoClause MySqlSelectIntoClause(MySqlSelectIntoClause.Position posit } } +MySqlProcedureAnalyse MySqlProcedureAnalyse(): +{ + MySqlProcedureAnalyse procedureAnalyse = new MySqlProcedureAnalyse(); + Token maxElements = null; + Token maxMemory = null; +} +{ + "(" + [ + maxElements= + [ "," maxMemory= ] + ] + ")" + { + if (maxElements != null) { + procedureAnalyse.setMaxElements(new LongValue(maxElements.image)); + } + if (maxMemory != null) { + procedureAnalyse.setMaxMemory(new LongValue(maxMemory.image)); + } + return procedureAnalyse; + } +} + void MySqlSelectIntoOutfileTail(MySqlSelectIntoClause intoClause): { Token token; diff --git a/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java b/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java new file mode 100644 index 000000000..7d90bdeb9 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/select/MySqlProcedureAnalyseTest.java @@ -0,0 +1,72 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.select; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import org.junit.jupiter.api.Test; + +class MySqlProcedureAnalyseTest { + + @Test + void parseWithoutParameters() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1, col2 FROM heavy_table PROCEDURE ANALYSE()"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertNotNull(procedureAnalyse); + assertNull(procedureAnalyse.getMaxElements()); + assertNull(procedureAnalyse.getMaxMemory()); + } + + @Test + void parseOneParameter() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1 FROM heavy_table PROCEDURE ANALYSE(10)"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertEquals("10", procedureAnalyse.getMaxElements().getStringValue()); + assertNull(procedureAnalyse.getMaxMemory()); + } + + @Test + void parseTwoParametersAfterLimit() throws JSQLParserException { + Select select = (Select) assertSqlCanBeParsedAndDeparsed( + "SELECT col1 FROM heavy_table LIMIT 20 PROCEDURE ANALYSE(10, 256)"); + + MySqlProcedureAnalyse procedureAnalyse = select.getMySqlProcedureAnalyse(); + assertEquals("10", procedureAnalyse.getMaxElements().getStringValue()); + assertEquals("256", procedureAnalyse.getMaxMemory().getStringValue()); + } + + @Test + void keepAnalyseAndProcedureAvailableAsIdentifiers() throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed("SELECT analyse FROM heavy_table"); + assertSqlCanBeParsedAndDeparsed("SELECT * FROM heavy_table procedure"); + } + + @Test + void rejectInvalidForms() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(-1)")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYSE(1, 2, 3)")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("SELECT * FROM t PROCEDURE ANALYZE()")); + } +}