diff --git a/README.md b/README.md index 32f7867416..d4b0e2848a 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **DDL** | `CREATE …` · `ALTER …` · `DROP …` | | **Oracle vector types** | `VECTOR(*, *)` · `VECTOR(*, FLOAT32)` · `VECTOR(3, *)`, including dense and sparse storage formats | | **PostgreSQL ownership** | `DROP OWNED` · `REASSIGN OWNED` | -| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` | +| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` | | **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` | | **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 11812c9f44..3ce49efd6a 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -478,7 +478,8 @@ public enum Feature { analyze, /** - * MySQL/MariaDB "CHECK TABLE", "OPTIMIZE TABLE" and "REPAIR TABLE" statements are allowed + * MySQL/MariaDB "CHECK TABLE", "CHECKSUM TABLE", "OPTIMIZE TABLE" and "REPAIR TABLE" statements + * are allowed * * @see TableMaintenanceStatement */ diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index fea4497b29..8f4e7aa8f7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -737,7 +737,8 @@ public Void visit(Analyze analyze, S context) { public Void visit(TableMaintenanceStatement statement, S context) { analysis.claimTopLevel(); analysis.certain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); - if (statement.getOperation() != TableMaintenanceStatement.Operation.CHECK) { + if (statement.getOperation() == TableMaintenanceStatement.Operation.OPTIMIZE + || statement.getOperation() == TableMaintenanceStatement.Operation.REPAIR) { // OPTIMIZE rebuilds table storage; REPAIR may also rewrite or discard rows analysis.certain(StmtFeature.MODIFIES_SCHEMA); if (statement.getOperation() == TableMaintenanceStatement.Operation.REPAIR) { diff --git a/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java b/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java index 0593a85f82..4c22dad751 100644 --- a/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java @@ -18,19 +18,22 @@ import net.sf.jsqlparser.schema.Table; /** - * A MySQL/MariaDB {@code CHECK TABLE}, {@code OPTIMIZE TABLE} or {@code REPAIR TABLE} statement. - * {@code ANALYZE TABLE} remains {@link net.sf.jsqlparser.statement.analyze.Analyze}. + * A MySQL/MariaDB {@code CHECK TABLE}, {@code CHECKSUM TABLE}, {@code OPTIMIZE TABLE} or + * {@code REPAIR TABLE} statement. {@code ANALYZE TABLE} remains + * {@link net.sf.jsqlparser.statement.analyze.Analyze}. */ public class TableMaintenanceStatement extends ASTNodeAccessImpl implements Statement { public enum Operation { - CHECK, OPTIMIZE, REPAIR; + CHECK, CHECKSUM, OPTIMIZE, REPAIR; /** Whether this operation accepts the option after its table list. */ public boolean allows(Option option) { switch (this) { case CHECK: return option != Option.USE_FRM; + case CHECKSUM: + return option == Option.QUICK || option == Option.EXTENDED; case REPAIR: return option == Option.QUICK || option == Option.EXTENDED || option == Option.USE_FRM; @@ -41,7 +44,7 @@ public boolean allows(Option option) { /** Whether this operation accepts {@code NO_WRITE_TO_BINLOG} or {@code LOCAL}. */ public boolean allowsModifier() { - return this != CHECK; + return this == OPTIMIZE || this == REPAIR; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java b/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java index 4acd7e435c..d8d0e01397 100644 --- a/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java +++ b/src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; @@ -30,12 +31,19 @@ public enum HistogramAction { UPDATE, DROP } + /** Whether MySQL updates a histogram automatically when it updates the table statistics. */ + public enum HistogramUpdateMode { + AUTO, MANUAL + } + private final List tables = new ArrayList<>(); private boolean tableKeyword; private Modifier modifier; private HistogramAction histogramAction; private final List histogramColumns = new ArrayList<>(); private Integer histogramBucketCount; + private StringValue histogramData; + private HistogramUpdateMode histogramUpdateMode; @Override public T accept(StatementVisitor statementVisitor, S context) { @@ -107,6 +115,24 @@ public void setHistogramBucketCount(Integer histogramBucketCount) { this.histogramBucketCount = histogramBucketCount; } + /** The JSON histogram of {@code UPDATE HISTOGRAM ... USING DATA}, or null. */ + public StringValue getHistogramData() { + return histogramData; + } + + public void setHistogramData(StringValue histogramData) { + this.histogramData = histogramData; + } + + /** The {@code AUTO UPDATE} or {@code MANUAL UPDATE} of {@code UPDATE HISTOGRAM}, or null. */ + public HistogramUpdateMode getHistogramUpdateMode() { + return histogramUpdateMode; + } + + public void setHistogramUpdateMode(HistogramUpdateMode histogramUpdateMode) { + this.histogramUpdateMode = histogramUpdateMode; + } + @Override public String toString() { StringBuilder builder = new StringBuilder("ANALYZE"); @@ -125,6 +151,12 @@ public String toString() { if (histogramBucketCount != null) { builder.append(" WITH ").append(histogramBucketCount).append(" BUCKETS"); } + if (histogramData != null) { + builder.append(" USING DATA ").append(histogramData); + } + if (histogramUpdateMode != null) { + builder.append(" ").append(histogramUpdateMode).append(" UPDATE"); + } } return builder.toString(); } @@ -173,4 +205,14 @@ public Analyze withHistogramBucketCount(Integer histogramBucketCount) { setHistogramBucketCount(histogramBucketCount); return this; } + + public Analyze withHistogramData(StringValue histogramData) { + setHistogramData(histogramData); + return this; + } + + public Analyze withHistogramUpdateMode(HistogramUpdateMode histogramUpdateMode) { + setHistogramUpdateMode(histogramUpdateMode); + return this; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index a361fa20f4..34a79f798b 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2498,17 +2498,18 @@ public class CCJSqlParser extends AbstractJSqlParser { } /** - * Whether a MySQL CHECK, OPTIMIZE or REPAIR TABLE statement starts here. TABLE or TABLES must - * follow, so other uses of these words are unaffected. + * Whether a MySQL CHECK, CHECKSUM, OPTIMIZE or REPAIR TABLE statement starts here. TABLE or + * TABLES must follow, so other uses of these words are unaffected. */ private boolean isTableMaintenanceAhead() { int kind = getToken(1).kind; - if (kind != K_CHECK && kind != K_OPTIMIZE && kind != K_REPAIR) { + if (kind != K_CHECK && kind != K_CHECKSUM && kind != K_OPTIMIZE && kind != K_REPAIR) { return false; } int offset = 2; int modifier = getToken(2).kind; - if (kind != K_CHECK && (modifier == K_NO_WRITE_TO_BINLOG || modifier == K_LOCAL)) { + if ((kind == K_OPTIMIZE || kind == K_REPAIR) + && (modifier == K_NO_WRITE_TO_BINLOG || modifier == K_LOCAL)) { offset = 3; } int table = getToken(offset).kind; @@ -4033,6 +4034,7 @@ String NonReservedWordPart1() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -19086,7 +19088,7 @@ TableOption MySqlAdditionalTableOption(): value=MySqlSizeNumber() | LOOKAHEAD({ isKeywordAhead("CHECKSUM") || isKeywordAhead("DELAY_KEY_WRITE") }) - token= { name = token.image.toUpperCase(Locale.ROOT); } + ( token= | token= ) { name = token.image.toUpperCase(Locale.ROOT); } [ "=" { useEquals = true; } ] token= { value = token.image; } | @@ -19556,8 +19558,8 @@ ColDataType.TypeModifier MySqlTypeModifier(): } /** - * Parses MySQL/MariaDB CHECK, OPTIMIZE and REPAIR TABLE statements. Options are accepted only for - * the operation that defines them, without reserving the option words. + * Parses MySQL/MariaDB CHECK, CHECKSUM, OPTIMIZE and REPAIR TABLE statements. Options are accepted + * only for the operation that defines them, without reserving the option words. */ TableMaintenanceStatement TableMaintenanceStatement(): { @@ -19571,6 +19573,7 @@ TableMaintenanceStatement TableMaintenanceStatement(): { ( { operation = TableMaintenanceStatement.Operation.CHECK; } + | { operation = TableMaintenanceStatement.Operation.CHECKSUM; } | { operation = TableMaintenanceStatement.Operation.OPTIMIZE; } | { operation = TableMaintenanceStatement.Operation.REPAIR; } ) @@ -19598,7 +19601,11 @@ TableMaintenanceStatement TableMaintenanceStatement(): ) { statement.addOptions(option); } )* - { return statement; } + { + requireDdlSyntax(operation != TableMaintenanceStatement.Operation.CHECKSUM + || statement.getOptions().size() <= 1, "CHECKSUM TABLE takes QUICK or EXTENDED, not both"); + return statement; + } } Analyze Analyze(): @@ -19609,6 +19616,7 @@ Analyze Analyze(): Token modifier = null; Token histogramAction = null; Token bucketCount = null; + Token data; } { @@ -19630,11 +19638,28 @@ Analyze Analyze(): column=Column() { analyze.addHistogramColumns(column); } ( "," column=Column() { analyze.addHistogramColumns(column); } )* - [ - LOOKAHEAD(3) - bucketCount= - { analyze.setHistogramBucketCount(Integer.valueOf(bucketCount.image)); } - ] + ( + // MySQL 8.4 sets a histogram from JSON instead of sampling the column + LOOKAHEAD(2) data= { + requireDdlSyntax(histogramAction.kind == K_UPDATE, "USING DATA requires UPDATE HISTOGRAM"); + analyze.setHistogramData(stringValue(data)); + } + | + [ + LOOKAHEAD(3) + bucketCount= + { analyze.setHistogramBucketCount(Integer.valueOf(bucketCount.image)); } + ] + [ + LOOKAHEAD({ (getToken(1).kind == K_AUTO || isKeywordAhead("MANUAL")) + && getToken(2).kind == K_UPDATE }) + ( { analyze.setHistogramUpdateMode(Analyze.HistogramUpdateMode.AUTO); } + | ContextualKeyword("MANUAL") { analyze.setHistogramUpdateMode(Analyze.HistogramUpdateMode.MANUAL); } ) + { + requireDdlSyntax(histogramAction.kind == K_UPDATE, "AUTO UPDATE and MANUAL UPDATE require UPDATE HISTOGRAM"); + } + ] + ) ] { diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 9f5b53892b..a5a6969f6a 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -159,7 +159,7 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad * - **PostgreSQL ownership** - ``DROP OWNED`` · ``REASSIGN OWNED`` * - **MySQL table maintenance** - - ``ANALYZE`` · ``CHECK`` · ``OPTIMIZE`` · ``REPAIR TABLE`` + - ``ANALYZE`` · ``CHECK`` · ``CHECKSUM`` · ``OPTIMIZE`` · ``REPAIR TABLE`` * - **MySQL account management** - ``CREATE``/``ALTER``/``RENAME USER`` · ``CREATE ROLE`` · ``SET ROLE`` · ``SET DEFAULT ROLE`` · ``SET PASSWORD`` * - **Tablespaces** diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index c784f81d3a..fb6d4f1d7e 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -2458,14 +2458,15 @@ MySQL `ALTER FUNCTION parser.withDialect(dialect)) + .toString()); + } + } + @Test void keywordsAreCaseInsensitive() throws JSQLParserException { Statement statement = CCJSqlParserUtil.parse("check tables t medium for upgrade"); @@ -110,7 +131,13 @@ void keywordsAreCaseInsensitive() throws JSQLParserException { "REPAIR TABLE t FAST", "REPAIR TABLE t FOR UPGRADE", "OPTIMIZE t", - "REPAIR TABLE" + "REPAIR TABLE", + "CHECKSUM NO_WRITE_TO_BINLOG TABLE t", + "CHECKSUM LOCAL TABLE t", + "CHECKSUM TABLE t FAST", + "CHECKSUM TABLE t FOR UPGRADE", + "CHECKSUM TABLE t QUICK EXTENDED", + "CHECKSUM t" }) void rejectsSyntaxOutsideTheStatementDefinition(String sql) { assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); @@ -149,6 +176,10 @@ void wordsRemainUsableOutsideTheStatement() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("ALTER TABLE t OPTIMIZE PARTITION p1"); assertSqlCanBeParsedAndDeparsed("ALTER TABLE t CHECK PARTITION p1"); assertSqlCanBeParsedAndDeparsed("ALTER TABLE t REPAIR PARTITION p1"); + assertSqlCanBeParsedAndDeparsed( + "SELECT checksum, CHECKSUM(*), CHECKSUM(a, b) FROM checksum"); + assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (checksum INT) CHECKSUM = 1"); + assertSqlCanBeParsedAndDeparsed("ALTER TABLE t CHECKSUM = 0, ADD COLUMN checksum INT"); } @Test @@ -169,6 +200,11 @@ void reportsStatementFeatures() throws JSQLParserException { assertThat(check.getCertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET); + StatementFeatures checksum = CCJSqlParserUtil.parse("CHECKSUM TABLE t").getFeatures(); + assertThat(checksum.getCertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.RETURNS_RESULT_SET); + assertFalse(checksum.may(StmtFeature.MODIFIES_SCHEMA)); + StatementFeatures optimize = CCJSqlParserUtil.parse("OPTIMIZE TABLE t").getFeatures(); assertThat(optimize.getCertain()).contains(StmtFeature.MODIFIES_SCHEMA); assertFalse(optimize.may(StmtFeature.MODIFIES_DATA)); diff --git a/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java b/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java index 578e68fd78..ea00590eae 100644 --- a/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/analyze/AnalyzeTest.java @@ -12,15 +12,22 @@ import static net.sf.jsqlparser.test.TestUtils.assertDeparse; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.StringReader; import java.util.List; import java.util.stream.Collectors; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserManager; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; public class AnalyzeTest { @@ -83,4 +90,50 @@ public void testMySqlAnalyzeNoWriteToBinlog() throws JSQLParserException { assertEquals(Analyze.Modifier.NO_WRITE_TO_BINLOG, analyze.getModifier()); } + + @Test + public void testMySqlAnalyzeHistogramUpdateMode() throws JSQLParserException { + Analyze auto = (Analyze) assertSqlCanBeParsedAndDeparsed( + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1, c2 WITH 32 BUCKETS AUTO UPDATE"); + assertEquals(32, auto.getHistogramBucketCount()); + assertEquals(Analyze.HistogramUpdateMode.AUTO, auto.getHistogramUpdateMode()); + + Analyze manual = (Analyze) assertSqlCanBeParsedAndDeparsed( + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 MANUAL UPDATE"); + assertNull(manual.getHistogramBucketCount()); + assertEquals(Analyze.HistogramUpdateMode.MANUAL, manual.getHistogramUpdateMode()); + + assertDeparse(new Analyze().withTableKeyword(true).withTable(new Table("t1")) + .withHistogramAction(Analyze.HistogramAction.UPDATE) + .addHistogramColumns(new Column("c1")) + .withHistogramUpdateMode(Analyze.HistogramUpdateMode.AUTO), + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 AUTO UPDATE"); + } + + @Test + public void testMySqlAnalyzeHistogramUsingData() throws JSQLParserException { + String json = "{\"buckets\": [[1, 0.5], [2, 1.0]], \"data-type\": \"int\"}"; + String sql = "ANALYZE NO_WRITE_TO_BINLOG TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA '" + + json + "'"; + Analyze analyze = (Analyze) assertSqlCanBeParsedAndDeparsed(sql); + assertEquals(json, analyze.getHistogramData().getValue()); + assertNull(analyze.getHistogramUpdateMode()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.MYSQL)) + .toString()); + + analyze.setHistogramData(new StringValue("{}")); + assertEquals("ANALYZE NO_WRITE_TO_BINLOG TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA '{}'", + analyze.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"ANALYZE TABLE t1 DROP HISTOGRAM ON c1 AUTO UPDATE", + "ANALYZE TABLE t1 DROP HISTOGRAM ON c1 USING DATA '{}'", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA '{}' MANUAL UPDATE", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 MANUAL UPDATE WITH 8 BUCKETS", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 AUTO", + "ANALYZE TABLE t1 UPDATE HISTOGRAM ON c1 USING DATA"}) + public void testMySqlAnalyzeRejectsMisplacedHistogramOptions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } } diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidatorTest.java index 4bc650f30d..be7b10dd3c 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/TableMaintenanceValidatorTest.java @@ -24,7 +24,7 @@ public class TableMaintenanceValidatorTest extends ValidationTestAsserts { @Test public void testAllowedForMySqlAndMariaDb() throws JSQLParserException { for (String sql : new String[] {"OPTIMIZE TABLE t1, t2", "REPAIR TABLE t QUICK", - "CHECK TABLE t FOR UPGRADE"}) { + "CHECK TABLE t FOR UPGRADE", "CHECKSUM TABLE t1, t2 EXTENDED"}) { validateNoErrors(sql, 1, MySqlVersion.V8_0, MariaDbVersion.V10_5_4, DatabaseType.MYSQL, DatabaseType.MARIADB); }