Skip to content

Commit ebac6f9

Browse files
committed
feat: parse MySQL LOCK TABLES, UNLOCK TABLES and the backup lock statements
mysqldump wraps the rows of each table in LOCK TABLES ... WRITE and UNLOCK TABLES, so a dump script failed at its first table. LOCK INSTANCE FOR BACKUP and UNLOCK INSTANCE failed as well. LOCK TABLES extends LockStatement: each target keeps its lock type (TableLockType) and its table keeps the alias, and isTablesKeyword() records the TABLES spelling. PostgreSQL and Oracle LOCK keep their reading, since neither follows a table name with an alias or a lock type. MariaDB's WRITE CONCURRENT is a lock type, and its WAIT and NOWAIT reuse the wait clause of the statement. UNLOCK TABLES and the backup lock name no table, so they are the new UnlockStatement and BackupLockStatement. UNLOCK becomes a non-reserved keyword and remains usable as a name. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent 4a970a3 commit ebac6f9

19 files changed

Lines changed: 1042 additions & 12 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
149149
| **PostgreSQL ownership** | `DROP OWNED` · `REASSIGN OWNED` |
150150
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
151151
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
152+
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
152153
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
153154
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
154155
| **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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
import net.sf.jsqlparser.statement.drop.Drop;
5252
import net.sf.jsqlparser.statement.execute.Execute;
5353
import net.sf.jsqlparser.statement.grant.Grant;
54+
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
55+
import net.sf.jsqlparser.statement.lock.UnlockStatement;
5456
import net.sf.jsqlparser.statement.merge.Merge;
5557
import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement;
5658
import net.sf.jsqlparser.statement.reindex.Reindex;
@@ -742,6 +744,18 @@ public enum Feature {
742744
* @see net.sf.jsqlparser.statement.lock.LockStatement
743745
*/
744746
lock,
747+
/**
748+
* MySQL/MariaDB "UNLOCK TABLES" statement is allowed
749+
*
750+
* @see UnlockStatement
751+
*/
752+
unlockTables,
753+
/**
754+
* MySQL "LOCK INSTANCE FOR BACKUP" and "UNLOCK INSTANCE" statements are allowed
755+
*
756+
* @see BackupLockStatement
757+
*/
758+
backupLock,
745759
/**
746760
* SQL "COMMENT ON" statement is allowed
747761
*

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@
9898
import net.sf.jsqlparser.statement.insert.Insert;
9999
import net.sf.jsqlparser.statement.insert.InsertBulk;
100100
import net.sf.jsqlparser.statement.load.LoadDataStatement;
101+
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
101102
import net.sf.jsqlparser.statement.lock.LockStatement;
103+
import net.sf.jsqlparser.statement.lock.UnlockStatement;
102104
import net.sf.jsqlparser.statement.maintenance.CheckpointStatement;
103105
import net.sf.jsqlparser.statement.maintenance.ClusterStatement;
104106
import net.sf.jsqlparser.statement.maintenance.VacuumStatement;
@@ -923,6 +925,16 @@ public <S> Void visit(LockStatement lock, S context) {
923925
return transactionOnly();
924926
}
925927

928+
@Override
929+
public <S> Void visit(UnlockStatement unlock, S context) {
930+
return transactionOnly();
931+
}
932+
933+
@Override
934+
public <S> Void visit(BackupLockStatement backupLock, S context) {
935+
return transactionOnly();
936+
}
937+
926938
private Void transactionOnly() {
927939
analysis.claimTopLevel();
928940
analysis.certain(StmtFeature.MODIFIES_TRANSACTION);

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@
9090
import net.sf.jsqlparser.statement.insert.Insert;
9191
import net.sf.jsqlparser.statement.insert.InsertBulk;
9292
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
93+
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
9394
import net.sf.jsqlparser.statement.lock.LockStatement;
95+
import net.sf.jsqlparser.statement.lock.UnlockStatement;
9496
import net.sf.jsqlparser.statement.maintenance.CheckpointStatement;
9597
import net.sf.jsqlparser.statement.maintenance.ClusterStatement;
9698
import net.sf.jsqlparser.statement.maintenance.VacuumStatement;
@@ -702,6 +704,22 @@ default void visit(LockStatement lock) {
702704
this.visit(lock, null);
703705
}
704706

707+
default <S> T visit(UnlockStatement unlock, S context) {
708+
return null;
709+
}
710+
711+
default void visit(UnlockStatement unlock) {
712+
visit(unlock, null);
713+
}
714+
715+
default <S> T visit(BackupLockStatement backupLock, S context) {
716+
return null;
717+
}
718+
719+
default void visit(BackupLockStatement backupLock) {
720+
visit(backupLock, null);
721+
}
722+
705723
<S> T visit(CreatePolicy createPolicy, S context);
706724

707725
default void visit(CreatePolicy createPolicy) {

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@
9393
import net.sf.jsqlparser.statement.insert.InsertBulk;
9494
import net.sf.jsqlparser.statement.insert.InsertConflictAction;
9595
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
96+
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
9697
import net.sf.jsqlparser.statement.lock.LockStatement;
98+
import net.sf.jsqlparser.statement.lock.UnlockStatement;
9799
import net.sf.jsqlparser.statement.maintenance.CheckpointStatement;
98100
import net.sf.jsqlparser.statement.maintenance.ClusterStatement;
99101
import net.sf.jsqlparser.statement.maintenance.VacuumStatement;
@@ -547,6 +549,16 @@ public <S> T visit(LockStatement lock, S context) {
547549
return null;
548550
}
549551

552+
@Override
553+
public <S> T visit(UnlockStatement unlock, S context) {
554+
return null;
555+
}
556+
557+
@Override
558+
public <S> T visit(BackupLockStatement backupLock, S context) {
559+
return null;
560+
}
561+
550562
@Override
551563
public <S> T visit(CreatePolicy createPolicy, S context) {
552564
fromItemVisitor.visitFromItem(createPolicy.getTable(), context);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.lock;
11+
12+
import java.util.Objects;
13+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
14+
import net.sf.jsqlparser.statement.Statement;
15+
import net.sf.jsqlparser.statement.StatementVisitor;
16+
17+
/**
18+
* MySQL {@code LOCK INSTANCE FOR BACKUP} and {@code UNLOCK INSTANCE}, which take and release the
19+
* instance-level backup lock. It locks no table, unlike {@link LockStatement}.
20+
*
21+
* @see <a href="https://dev.mysql.com/doc/refman/8.4/en/lock-instance-for-backup.html">MySQL LOCK
22+
* INSTANCE FOR BACKUP and UNLOCK INSTANCE</a>
23+
*/
24+
public class BackupLockStatement extends ASTNodeAccessImpl implements Statement {
25+
private Action action = Action.LOCK;
26+
27+
public enum Action {
28+
/** LOCK INSTANCE FOR BACKUP */
29+
LOCK("LOCK INSTANCE FOR BACKUP"),
30+
/** UNLOCK INSTANCE */
31+
UNLOCK("UNLOCK INSTANCE");
32+
33+
private final String sql;
34+
35+
Action(String sql) {
36+
this.sql = sql;
37+
}
38+
39+
public String getSql() {
40+
return sql;
41+
}
42+
}
43+
44+
public BackupLockStatement() {}
45+
46+
public BackupLockStatement(Action action) {
47+
setAction(action);
48+
}
49+
50+
public Action getAction() {
51+
return action;
52+
}
53+
54+
public void setAction(Action action) {
55+
this.action = Objects.requireNonNull(action);
56+
}
57+
58+
public StringBuilder appendTo(StringBuilder builder) {
59+
return builder.append(action.getSql());
60+
}
61+
62+
@Override
63+
public String toString() {
64+
return appendTo(new StringBuilder()).toString();
65+
}
66+
67+
@Override
68+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
69+
return statementVisitor.visit(this, context);
70+
}
71+
}

‎src/main/java/net/sf/jsqlparser/statement/lock/LockStatement.java‎

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,25 @@
1212
import java.util.ArrayList;
1313
import java.util.List;
1414
import java.util.Objects;
15+
import net.sf.jsqlparser.expression.Alias;
1516
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
1617
import net.sf.jsqlparser.schema.Table;
1718
import net.sf.jsqlparser.statement.Statement;
1819
import net.sf.jsqlparser.statement.StatementVisitor;
1920

2021
/**
2122
* Statement to lock one or more tables.<br>
22-
* Example:<br>
23+
* Examples:<br>
2324
* LOCK TABLE t IN EXCLUSIVE MODE<br>
25+
* LOCK TABLES t READ, u AS a WRITE (MySQL and MariaDB, where each target has a
26+
* {@link TableLockType})<br>
2427
* <br>
2528
*/
2629
public class LockStatement extends ASTNodeAccessImpl implements Statement {
2730

2831
private final List<Target> targets = new ArrayList<>();
2932
private boolean useTableKeyword = true;
33+
private boolean tablesKeyword;
3034
private LockMode lockMode;
3135
private boolean noWait;
3236
private Long waitSeconds;
@@ -35,11 +39,44 @@ public enum Scope {
3539
DEFAULT, ONLY, INCLUDING_DESCENDANTS
3640
}
3741

42+
/**
43+
* A locked table. A MySQL or MariaDB target also has a lock type and may have an alias, which
44+
* is the {@link Table#getAlias() alias of its table}.
45+
*/
3846
public static class Target extends net.sf.jsqlparser.schema.TableReference {
47+
private TableLockType lockType;
48+
3949
public Target(Table table) {
4050
super(table);
4151
}
4252

53+
/** Returns the MySQL or MariaDB lock type, or null for a PostgreSQL or Oracle target. */
54+
public TableLockType getLockType() {
55+
return lockType;
56+
}
57+
58+
public void setLockType(TableLockType lockType) {
59+
this.lockType = lockType;
60+
}
61+
62+
public Target withLockType(TableLockType lockType) {
63+
setLockType(lockType);
64+
return this;
65+
}
66+
67+
@Override
68+
public StringBuilder appendTo(StringBuilder builder) {
69+
super.appendTo(builder);
70+
Alias alias = getTable().getAlias();
71+
if (alias != null) {
72+
builder.append(alias);
73+
}
74+
if (lockType != null) {
75+
builder.append(' ').append(lockType.getValue());
76+
}
77+
return builder;
78+
}
79+
4380
public Scope getScope() {
4481
return isOnly() ? Scope.ONLY
4582
: isIncludeDescendants() ? Scope.INCLUDING_DESCENDANTS : Scope.DEFAULT;
@@ -100,8 +137,25 @@ public boolean isUseTableKeyword() {
100137
return useTableKeyword;
101138
}
102139

140+
/** Setting false also clears {@link #isTablesKeyword()}. */
103141
public void setUseTableKeyword(boolean useTableKeyword) {
104142
this.useTableKeyword = useTableKeyword;
143+
if (!useTableKeyword) {
144+
tablesKeyword = false;
145+
}
146+
}
147+
148+
/** Whether the statement is spelled LOCK TABLES, as MySQL and MariaDB allow. */
149+
public boolean isTablesKeyword() {
150+
return tablesKeyword;
151+
}
152+
153+
/** Setting true also sets {@link #isUseTableKeyword()}. */
154+
public void setTablesKeyword(boolean tablesKeyword) {
155+
this.tablesKeyword = tablesKeyword;
156+
if (tablesKeyword) {
157+
useTableKeyword = true;
158+
}
105159
}
106160

107161
/** Returns null when IN ... MODE was omitted. */
@@ -150,12 +204,17 @@ public Long getWaitSeconds() {
150204
}
151205

152206
public StringBuilder appendTo(StringBuilder builder) {
153-
builder.append(useTableKeyword ? "LOCK TABLE " : "LOCK ");
207+
builder.append("LOCK ");
208+
if (tablesKeyword) {
209+
builder.append("TABLES ");
210+
} else if (useTableKeyword) {
211+
builder.append("TABLE ");
212+
}
154213
for (int i = 0; i < targets.size(); i++) {
155214
if (i > 0) {
156215
builder.append(", ");
157216
}
158-
builder.append(targets.get(i));
217+
targets.get(i).appendTo(builder);
159218
}
160219
if (lockMode != null) {
161220
builder.append(" IN ").append(lockMode.getValue()).append(" MODE");
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.lock;
11+
12+
/**
13+
* The lock type that MySQL and MariaDB give each table of {@code LOCK TABLES}. MySQL 8.4 has
14+
* {@code READ [LOCAL] | WRITE}.
15+
*
16+
* @see <a href="https://dev.mysql.com/doc/refman/8.4/en/lock-tables.html">MySQL LOCK TABLES</a>
17+
* @see <a href="https://mariadb.com/kb/en/lock-tables/">MariaDB LOCK TABLES</a>
18+
*/
19+
public enum TableLockType {
20+
READ("READ"), READ_LOCAL("READ LOCAL"), WRITE("WRITE"),
21+
22+
/**
23+
* MySQL 8.0, where it is deprecated and has no effect, and MariaDB. MySQL 8.4 rejects it.
24+
*
25+
* @see <a href="https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html">MySQL 8.0 LOCK
26+
* TABLES</a>
27+
*/
28+
LOW_PRIORITY_WRITE("LOW_PRIORITY WRITE"),
29+
30+
/** MariaDB only. */
31+
WRITE_CONCURRENT("WRITE CONCURRENT");
32+
33+
private final String value;
34+
35+
TableLockType(String value) {
36+
this.value = value;
37+
}
38+
39+
public String getValue() {
40+
return value;
41+
}
42+
}

0 commit comments

Comments
 (0)