Skip to content

Commit b245090

Browse files
authored
feat: parse MySQL CHECKSUM TABLE and histogram update options (#2883)
CHECKSUM TABLE failed, and so did the MySQL 8.4 forms of ANALYZE TABLE ... UPDATE HISTOGRAM that end in AUTO UPDATE or MANUAL UPDATE or that give the histogram with USING DATA. CHECKSUM TABLE is a TableMaintenanceStatement with the new operation CHECKSUM, which takes no modifier and one of QUICK and EXTENDED. CHECKSUM becomes a non-reserved keyword, so it remains usable as a name and as a table option. Analyze keeps the update mode in getHistogramUpdateMode() and the JSON in getHistogramData(); USING DATA excludes the bucket count and the update mode, and DROP HISTOGRAM takes none of them. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 0a5d312 commit b245090

11 files changed

Lines changed: 194 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
147147
| **DDL** | `CREATE …` · `ALTER …` · `DROP …` |
148148
| **Oracle vector types** | `VECTOR(*, *)` · `VECTOR(*, FLOAT32)` · `VECTOR(3, *)`, including dense and sparse storage formats |
149149
| **PostgreSQL ownership** | `DROP OWNED` · `REASSIGN OWNED` |
150-
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` |
150+
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
151151
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
152152
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
153153
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ public enum Feature {
478478
analyze,
479479

480480
/**
481-
* MySQL/MariaDB "CHECK TABLE", "OPTIMIZE TABLE" and "REPAIR TABLE" statements are allowed
481+
* MySQL/MariaDB "CHECK TABLE", "CHECKSUM TABLE", "OPTIMIZE TABLE" and "REPAIR TABLE" statements
482+
* are allowed
482483
*
483484
* @see TableMaintenanceStatement
484485
*/

‎src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,8 @@ public <S> Void visit(Analyze analyze, S context) {
737737
public <S> Void visit(TableMaintenanceStatement statement, S context) {
738738
analysis.claimTopLevel();
739739
analysis.certain(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET);
740-
if (statement.getOperation() != TableMaintenanceStatement.Operation.CHECK) {
740+
if (statement.getOperation() == TableMaintenanceStatement.Operation.OPTIMIZE
741+
|| statement.getOperation() == TableMaintenanceStatement.Operation.REPAIR) {
741742
// OPTIMIZE rebuilds table storage; REPAIR may also rewrite or discard rows
742743
analysis.certain(StmtFeature.MODIFIES_SCHEMA);
743744
if (statement.getOperation() == TableMaintenanceStatement.Operation.REPAIR) {

‎src/main/java/net/sf/jsqlparser/statement/TableMaintenanceStatement.java‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@
1818
import net.sf.jsqlparser.schema.Table;
1919

2020
/**
21-
* A MySQL/MariaDB {@code CHECK TABLE}, {@code OPTIMIZE TABLE} or {@code REPAIR TABLE} statement.
22-
* {@code ANALYZE TABLE} remains {@link net.sf.jsqlparser.statement.analyze.Analyze}.
21+
* A MySQL/MariaDB {@code CHECK TABLE}, {@code CHECKSUM TABLE}, {@code OPTIMIZE TABLE} or
22+
* {@code REPAIR TABLE} statement. {@code ANALYZE TABLE} remains
23+
* {@link net.sf.jsqlparser.statement.analyze.Analyze}.
2324
*/
2425
public class TableMaintenanceStatement extends ASTNodeAccessImpl implements Statement {
2526

2627
public enum Operation {
27-
CHECK, OPTIMIZE, REPAIR;
28+
CHECK, CHECKSUM, OPTIMIZE, REPAIR;
2829

2930
/** Whether this operation accepts the option after its table list. */
3031
public boolean allows(Option option) {
3132
switch (this) {
3233
case CHECK:
3334
return option != Option.USE_FRM;
35+
case CHECKSUM:
36+
return option == Option.QUICK || option == Option.EXTENDED;
3437
case REPAIR:
3538
return option == Option.QUICK || option == Option.EXTENDED
3639
|| option == Option.USE_FRM;
@@ -41,7 +44,7 @@ public boolean allows(Option option) {
4144

4245
/** Whether this operation accepts {@code NO_WRITE_TO_BINLOG} or {@code LOCAL}. */
4346
public boolean allowsModifier() {
44-
return this != CHECK;
47+
return this == OPTIMIZE || this == REPAIR;
4548
}
4649
}
4750

‎src/main/java/net/sf/jsqlparser/statement/analyze/Analyze.java‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Collections;
1515
import java.util.List;
1616
import java.util.stream.Collectors;
17+
import net.sf.jsqlparser.expression.StringValue;
1718
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1819
import net.sf.jsqlparser.schema.Column;
1920
import net.sf.jsqlparser.schema.Table;
@@ -30,12 +31,19 @@ public enum HistogramAction {
3031
UPDATE, DROP
3132
}
3233

34+
/** Whether MySQL updates a histogram automatically when it updates the table statistics. */
35+
public enum HistogramUpdateMode {
36+
AUTO, MANUAL
37+
}
38+
3339
private final List<Table> tables = new ArrayList<>();
3440
private boolean tableKeyword;
3541
private Modifier modifier;
3642
private HistogramAction histogramAction;
3743
private final List<Column> histogramColumns = new ArrayList<>();
3844
private Integer histogramBucketCount;
45+
private StringValue histogramData;
46+
private HistogramUpdateMode histogramUpdateMode;
3947

4048
@Override
4149
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
@@ -107,6 +115,24 @@ public void setHistogramBucketCount(Integer histogramBucketCount) {
107115
this.histogramBucketCount = histogramBucketCount;
108116
}
109117

118+
/** The JSON histogram of {@code UPDATE HISTOGRAM ... USING DATA}, or null. */
119+
public StringValue getHistogramData() {
120+
return histogramData;
121+
}
122+
123+
public void setHistogramData(StringValue histogramData) {
124+
this.histogramData = histogramData;
125+
}
126+
127+
/** The {@code AUTO UPDATE} or {@code MANUAL UPDATE} of {@code UPDATE HISTOGRAM}, or null. */
128+
public HistogramUpdateMode getHistogramUpdateMode() {
129+
return histogramUpdateMode;
130+
}
131+
132+
public void setHistogramUpdateMode(HistogramUpdateMode histogramUpdateMode) {
133+
this.histogramUpdateMode = histogramUpdateMode;
134+
}
135+
110136
@Override
111137
public String toString() {
112138
StringBuilder builder = new StringBuilder("ANALYZE");
@@ -125,6 +151,12 @@ public String toString() {
125151
if (histogramBucketCount != null) {
126152
builder.append(" WITH ").append(histogramBucketCount).append(" BUCKETS");
127153
}
154+
if (histogramData != null) {
155+
builder.append(" USING DATA ").append(histogramData);
156+
}
157+
if (histogramUpdateMode != null) {
158+
builder.append(" ").append(histogramUpdateMode).append(" UPDATE");
159+
}
128160
}
129161
return builder.toString();
130162
}
@@ -173,4 +205,14 @@ public Analyze withHistogramBucketCount(Integer histogramBucketCount) {
173205
setHistogramBucketCount(histogramBucketCount);
174206
return this;
175207
}
208+
209+
public Analyze withHistogramData(StringValue histogramData) {
210+
setHistogramData(histogramData);
211+
return this;
212+
}
213+
214+
public Analyze withHistogramUpdateMode(HistogramUpdateMode histogramUpdateMode) {
215+
setHistogramUpdateMode(histogramUpdateMode);
216+
return this;
217+
}
176218
}

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

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,17 +2498,18 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
24982498
}
24992499

25002500
/**
2501-
* Whether a MySQL CHECK, OPTIMIZE or REPAIR TABLE statement starts here. TABLE or TABLES must
2502-
* follow, so other uses of these words are unaffected.
2501+
* Whether a MySQL CHECK, CHECKSUM, OPTIMIZE or REPAIR TABLE statement starts here. TABLE or
2502+
* TABLES must follow, so other uses of these words are unaffected.
25032503
*/
25042504
private boolean isTableMaintenanceAhead() {
25052505
int kind = getToken(1).kind;
2506-
if (kind != K_CHECK && kind != K_OPTIMIZE && kind != K_REPAIR) {
2506+
if (kind != K_CHECK && kind != K_CHECKSUM && kind != K_OPTIMIZE && kind != K_REPAIR) {
25072507
return false;
25082508
}
25092509
int offset = 2;
25102510
int modifier = getToken(2).kind;
2511-
if (kind != K_CHECK && (modifier == K_NO_WRITE_TO_BINLOG || modifier == K_LOCAL)) {
2511+
if ((kind == K_OPTIMIZE || kind == K_REPAIR)
2512+
&& (modifier == K_NO_WRITE_TO_BINLOG || modifier == K_LOCAL)) {
25122513
offset = 3;
25132514
}
25142515
int table = getToken(offset).kind;
@@ -4033,6 +4034,7 @@ String NonReservedWordPart1() :
40334034
| tk=<K_CHANGE:"CHANGE">
40344035
| tk=<K_CHANGES:"CHANGES">
40354036
| tk=<K_CHECKPOINT:"CHECKPOINT">
4037+
| tk=<K_CHECKSUM:"CHECKSUM">
40364038
| tk=<K_CHAR:"CHAR">
40374039
| tk=<K_CLOSE:"CLOSE">
40384040
| tk=<K_CLOUD:"CLOUD">
@@ -19086,7 +19088,7 @@ TableOption MySqlAdditionalTableOption():
1908619088
value=MySqlSizeNumber()
1908719089
|
1908819090
LOOKAHEAD({ isKeywordAhead("CHECKSUM") || isKeywordAhead("DELAY_KEY_WRITE") })
19089-
token=<S_IDENTIFIER> { name = token.image.toUpperCase(Locale.ROOT); }
19091+
( token=<K_CHECKSUM> | token=<S_IDENTIFIER> ) { name = token.image.toUpperCase(Locale.ROOT); }
1909019092
[ "=" { useEquals = true; } ]
1909119093
token=<S_LONG> { value = token.image; }
1909219094
|
@@ -19565,8 +19567,8 @@ ColDataType.TypeModifier MySqlTypeModifier():
1956519567
}
1956619568

1956719569
/**
19568-
* Parses MySQL/MariaDB CHECK, OPTIMIZE and REPAIR TABLE statements. Options are accepted only for
19569-
* the operation that defines them, without reserving the option words.
19570+
* Parses MySQL/MariaDB CHECK, CHECKSUM, OPTIMIZE and REPAIR TABLE statements. Options are accepted
19571+
* only for the operation that defines them, without reserving the option words.
1957019572
*/
1957119573
TableMaintenanceStatement TableMaintenanceStatement():
1957219574
{
@@ -19580,6 +19582,7 @@ TableMaintenanceStatement TableMaintenanceStatement():
1958019582
{
1958119583
(
1958219584
<K_CHECK> { operation = TableMaintenanceStatement.Operation.CHECK; }
19585+
| <K_CHECKSUM> { operation = TableMaintenanceStatement.Operation.CHECKSUM; }
1958319586
| <K_OPTIMIZE> { operation = TableMaintenanceStatement.Operation.OPTIMIZE; }
1958419587
| <K_REPAIR> { operation = TableMaintenanceStatement.Operation.REPAIR; }
1958519588
)
@@ -19607,7 +19610,11 @@ TableMaintenanceStatement TableMaintenanceStatement():
1960719610
)
1960819611
{ statement.addOptions(option); }
1960919612
)*
19610-
{ return statement; }
19613+
{
19614+
requireDdlSyntax(operation != TableMaintenanceStatement.Operation.CHECKSUM
19615+
|| statement.getOptions().size() <= 1, "CHECKSUM TABLE takes QUICK or EXTENDED, not both");
19616+
return statement;
19617+
}
1961119618
}
1961219619

1961319620
Analyze Analyze():
@@ -19618,6 +19625,7 @@ Analyze Analyze():
1961819625
Token modifier = null;
1961919626
Token histogramAction = null;
1962019627
Token bucketCount = null;
19628+
Token data;
1962119629
}
1962219630
{
1962319631
<K_ANALYZE>
@@ -19639,11 +19647,28 @@ Analyze Analyze():
1963919647
<K_HISTOGRAM> <K_ON>
1964019648
column=Column() { analyze.addHistogramColumns(column); }
1964119649
( "," column=Column() { analyze.addHistogramColumns(column); } )*
19642-
[
19643-
LOOKAHEAD(3)
19644-
<K_WITH> bucketCount=<S_LONG> <K_BUCKETS>
19645-
{ analyze.setHistogramBucketCount(Integer.valueOf(bucketCount.image)); }
19646-
]
19650+
(
19651+
// MySQL 8.4 sets a histogram from JSON instead of sampling the column
19652+
LOOKAHEAD(2) <K_USING> <K_DATA> data=<S_CHAR_LITERAL> {
19653+
requireDdlSyntax(histogramAction.kind == K_UPDATE, "USING DATA requires UPDATE HISTOGRAM");
19654+
analyze.setHistogramData(stringValue(data));
19655+
}
19656+
|
19657+
[
19658+
LOOKAHEAD(3)
19659+
<K_WITH> bucketCount=<S_LONG> <K_BUCKETS>
19660+
{ analyze.setHistogramBucketCount(Integer.valueOf(bucketCount.image)); }
19661+
]
19662+
[
19663+
LOOKAHEAD({ (getToken(1).kind == K_AUTO || isKeywordAhead("MANUAL"))
19664+
&& getToken(2).kind == K_UPDATE })
19665+
( <K_AUTO> { analyze.setHistogramUpdateMode(Analyze.HistogramUpdateMode.AUTO); }
19666+
| ContextualKeyword("MANUAL") { analyze.setHistogramUpdateMode(Analyze.HistogramUpdateMode.MANUAL); } )
19667+
<K_UPDATE> {
19668+
requireDdlSyntax(histogramAction.kind == K_UPDATE, "AUTO UPDATE and MANUAL UPDATE require UPDATE HISTOGRAM");
19669+
}
19670+
]
19671+
)
1964719672
]
1964819673

1964919674
{

‎src/site/sphinx/index.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad
159159
* - **PostgreSQL ownership**
160160
- ``DROP OWNED`` · ``REASSIGN OWNED``
161161
* - **MySQL table maintenance**
162-
- ``ANALYZE`` · ``CHECK`` · ``OPTIMIZE`` · ``REPAIR TABLE``
162+
- ``ANALYZE`` · ``CHECK`` · ``CHECKSUM`` · ``OPTIMIZE`` · ``REPAIR TABLE``
163163
* - **MySQL account management**
164164
- ``CREATE``/``ALTER``/``RENAME USER`` · ``CREATE ROLE`` · ``SET ROLE`` · ``SET DEFAULT ROLE`` · ``SET PASSWORD``
165165
* - **Tablespaces**

‎src/site/sphinx/usage.rst‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,14 +2458,15 @@ MySQL `ALTER FUNCTION <https://dev.mysql.com/doc/refman/8.4/en/alter-function.ht
24582458
MySQL table maintenance statements
24592459
----------------------------------
24602460

2461-
``CHECK TABLE``, ``OPTIMIZE TABLE`` and ``REPAIR TABLE`` parse to
2461+
``CHECK TABLE``, ``CHECKSUM TABLE``, ``OPTIMIZE TABLE`` and ``REPAIR TABLE`` parse to
24622462
``TableMaintenanceStatement`` in every dialect; ``ANALYZE TABLE`` remains ``Analyze``.
24632463
``getOperation()`` identifies the statement, ``getModifier()`` returns
24642464
``NO_WRITE_TO_BINLOG`` or ``LOCAL`` for OPTIMIZE and REPAIR, and ``isTablesKeyword()``
24652465
records the ``TABLES`` spelling. ``getOptions()`` keeps the trailing options in source
24662466
order. CHECK accepts ``FOR UPGRADE``, ``QUICK``, ``FAST``, ``MEDIUM``, ``EXTENDED`` and
2467-
``CHANGED``; REPAIR accepts ``QUICK``, ``EXTENDED`` and ``USE_FRM``. Other combinations
2468-
are rejected, and the option words remain ordinary identifiers elsewhere.
2467+
``CHANGED``; CHECKSUM accepts one of ``QUICK`` and ``EXTENDED``; REPAIR accepts ``QUICK``,
2468+
``EXTENDED`` and ``USE_FRM``. Other combinations are rejected, and the option words and
2469+
``CHECKSUM`` remain ordinary identifiers elsewhere.
24692470

24702471
.. code-block:: java
24712472
@@ -2479,6 +2480,12 @@ Table visitors and metadata validation use the table list. Validators report the
24792480
analysis reports ``READS_DATA`` and ``RETURNS_RESULT_SET``; OPTIMIZE and REPAIR also
24802481
report ``MODIFIES_SCHEMA``, and REPAIR may report ``MODIFIES_DATA``.
24812482

2483+
``ANALYZE TABLE ... UPDATE HISTOGRAM ON`` keeps ``WITH n BUCKETS``
2484+
(``getHistogramBucketCount()``), MySQL 8.4's ``AUTO UPDATE`` or ``MANUAL UPDATE``
2485+
(``getHistogramUpdateMode()``) and ``USING DATA 'json'`` (``getHistogramData()``, a
2486+
``StringValue``). ``USING DATA`` excludes the other two options, and ``DROP HISTOGRAM``
2487+
takes none of them.
2488+
24822489
MySQL account management statements
24832490
-----------------------------------
24842491

‎src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,27 @@ void checkOptionsKeepSourceOrder() throws JSQLParserException {
9494
assertEquals(List.of("t1", "t2"), tableNames(statement));
9595
}
9696

97+
@Test
98+
void checksumTableWithOption() throws JSQLParserException {
99+
TableMaintenanceStatement statement = parse("CHECKSUM TABLE t1, db.t2 QUICK");
100+
101+
assertEquals(Operation.CHECKSUM, statement.getOperation());
102+
assertNull(statement.getModifier());
103+
assertEquals(List.of("t1", "db.t2"), tableNames(statement));
104+
assertEquals(List.of(Option.QUICK), statement.getOptions());
105+
}
106+
107+
@ParameterizedTest
108+
@ValueSource(strings = {"CHECKSUM TABLE t", "CHECKSUM TABLE t EXTENDED",
109+
"CHECKSUM TABLES t1, t2"})
110+
void checksumTableForms(String sql) throws JSQLParserException {
111+
assertEquals(Operation.CHECKSUM, parse(sql).getOperation());
112+
for (Dialect dialect : List.of(Dialect.MYSQL, Dialect.MARIADB)) {
113+
assertEquals(sql, CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect))
114+
.toString());
115+
}
116+
}
117+
97118
@Test
98119
void keywordsAreCaseInsensitive() throws JSQLParserException {
99120
Statement statement = CCJSqlParserUtil.parse("check tables t medium for upgrade");
@@ -110,7 +131,13 @@ void keywordsAreCaseInsensitive() throws JSQLParserException {
110131
"REPAIR TABLE t FAST",
111132
"REPAIR TABLE t FOR UPGRADE",
112133
"OPTIMIZE t",
113-
"REPAIR TABLE"
134+
"REPAIR TABLE",
135+
"CHECKSUM NO_WRITE_TO_BINLOG TABLE t",
136+
"CHECKSUM LOCAL TABLE t",
137+
"CHECKSUM TABLE t FAST",
138+
"CHECKSUM TABLE t FOR UPGRADE",
139+
"CHECKSUM TABLE t QUICK EXTENDED",
140+
"CHECKSUM t"
114141
})
115142
void rejectsSyntaxOutsideTheStatementDefinition(String sql) {
116143
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
@@ -149,6 +176,10 @@ void wordsRemainUsableOutsideTheStatement() throws JSQLParserException {
149176
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t OPTIMIZE PARTITION p1");
150177
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t CHECK PARTITION p1");
151178
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t REPAIR PARTITION p1");
179+
assertSqlCanBeParsedAndDeparsed(
180+
"SELECT checksum, CHECKSUM(*), CHECKSUM(a, b) FROM checksum");
181+
assertSqlCanBeParsedAndDeparsed("CREATE TABLE t (checksum INT) CHECKSUM = 1");
182+
assertSqlCanBeParsedAndDeparsed("ALTER TABLE t CHECKSUM = 0, ADD COLUMN checksum INT");
152183
}
153184

154185
@Test
@@ -169,6 +200,11 @@ void reportsStatementFeatures() throws JSQLParserException {
169200
assertThat(check.getCertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA,
170201
StmtFeature.RETURNS_RESULT_SET);
171202

203+
StatementFeatures checksum = CCJSqlParserUtil.parse("CHECKSUM TABLE t").getFeatures();
204+
assertThat(checksum.getCertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA,
205+
StmtFeature.RETURNS_RESULT_SET);
206+
assertFalse(checksum.may(StmtFeature.MODIFIES_SCHEMA));
207+
172208
StatementFeatures optimize = CCJSqlParserUtil.parse("OPTIMIZE TABLE t").getFeatures();
173209
assertThat(optimize.getCertain()).contains(StmtFeature.MODIFIES_SCHEMA);
174210
assertFalse(optimize.may(StmtFeature.MODIFIES_DATA));

0 commit comments

Comments
 (0)