Skip to content

feat: parse MySQL LOCK TABLES, UNLOCK TABLES and the backup lock statements - #2888

Open
minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/mysql-lock-tables-20261103
Open

minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/mysql-lock-tables-20261103

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

mysqldump wraps the rows of every table in LOCK TABLES and UNLOCK TABLES by default (--add-locks, part of --opt):

LOCK TABLES `t` WRITE;
/*!40000 ALTER TABLE `t` DISABLE KEYS */;
INSERT INTO `t` VALUES (1,'a'),(2,'b');
/*!40000 ALTER TABLE `t` ENABLE KEYS */;
UNLOCK TABLES;

CCJSqlParserUtil.parseStatements therefore fails at the first table of a dump. On master these statements fail in every dialect:

  • LOCK TABLES `t` WRITE and LOCK TABLES t AS a READ LOCAL, u WRITE fail at the table name, LOCK TABLE t READ at READ.
  • UNLOCK TABLES fails at UNLOCK; with unsupported statements enabled it becomes an UnsupportedStatement.
  • LOCK INSTANCE FOR BACKUP fails at FOR, and UNLOCK INSTANCE at UNLOCK.

Syntax: MySQL 8.4 LOCK TABLES and UNLOCK TABLES, MySQL 8.4 LOCK INSTANCE FOR BACKUP and UNLOCK INSTANCE, MariaDB LOCK TABLES.

LOCK {TABLE | TABLES} tbl_name [[AS] alias] lock_type [, tbl_name [[AS] alias] lock_type] ...
lock_type: READ [LOCAL] | WRITE
UNLOCK {TABLE | TABLES}
LOCK INSTANCE FOR BACKUP
UNLOCK INSTANCE

MariaDB adds the lock type WRITE CONCURRENT and a trailing WAIT n or NOWAIT. LOW_PRIORITY WRITE is also accepted, because MySQL 8.0 (where it is deprecated and has no effect) and MariaDB accept it; MySQL 8.4 rejects it.

Current design (As-is)

  • LockStatement models PostgreSQL's and Oracle's LOCK [TABLE] [ONLY] name [*], … [IN mode MODE] [NOWAIT | WAIT n]. Its targets (LockStatement.Target) come from RelationTarget(), which TRUNCATE shares. A target has no alias and no lock type, so the parser stops at READ, at WRITE or at an alias. LOCK TABLES t … reads TABLES as the name of a table and stops at t.
  • UNLOCK is a plain identifier. No statement starts with it, and isStatementStartAhead() does not know it. ALTER USER … ACCOUNT UNLOCK reads it as a contextual word.
  • LOCK INSTANCE FOR BACKUP reads instance as a table name.
  • StatementValidator.visit(LockStatement) is a TODO, so no LOCK statement is validated.

Target design (To-be)

  • MySQL table locks extend LockStatement. A MySQL LOCK TABLES is the same statement as PostgreSQL's LOCK TABLE: a list of tables to lock. Only each target carries more, so the existing model, its target list and its consumers (TablesNamesFinder, StatementDeParser, the validator) serve every dialect, and a consumer handles one class. A separate class would have duplicated the target list, the WAIT/NOWAIT clause and every visitor.

    • LockStatement.Target#getLockType() returns the new enum TableLockType: READ, READ_LOCAL, WRITE, LOW_PRIORITY_WRITE (MySQL 8.0 and MariaDB) or MariaDB's WRITE_CONCURRENT. It is null for a PostgreSQL or Oracle target.
    • The alias is the table's own Table#getAlias(), as for the tables of FROM, UPDATE and DELETE; the target renders it before the lock type.
    • isTablesKeyword() records the TABLES spelling next to isUseTableKeyword(). Setting TABLES sets the TABLE flag, and clearing the TABLE flag clears TABLES, as TableReference keeps ONLY and * consistent.
    • MariaDB's trailing WAIT n and NOWAIT are the clause Oracle already has, so they use getWaitSeconds() and isNoWait().
    • The public API of LockStatement, LockStatement.Target, LockMode and TableReference is unchanged, and so are the PostgreSQL and Oracle readings.
  • UNLOCK TABLES and the backup lock are new statements, because they name no table. Parsed as LockStatement, they would add a LockStatement without targets, whose getTable() is null. The split follows the two pages of the MySQL manual.

    • UnlockStatement: UNLOCK TABLE or UNLOCK TABLES (isTablesKeyword()).
    • BackupLockStatement: getAction() is LOCK (LOCK INSTANCE FOR BACKUP) or UNLOCK (UNLOCK INSTANCE).
  • Grammar:

    • LockStatement() takes the MySQL branch when isMySqlLockTablesAhead() sees TABLE or TABLES, a table name, and then AS, a lock type, or an alias followed by a lock type. PostgreSQL and Oracle never follow a table name with any of these, so LOCK tables, LOCK tables NOWAIT, LOCK tables.x IN SHARE MODE, LOCK TABLE tables, instance, LOCK instance and LOCK TABLE ONLY t … keep their reading. ONLY counts as a name in this check.
    • A MySQL target is tbl_name [[AS] alias] lock_type: the branch reads the name with the shared Table() and the alias with Alias(), not with PostgreSQL's RelationTarget(). MySQL has no ONLY or * there, so the ONLY token is taken as the table name, as in SELECT * FROM only: LOCK TABLES only WRITE, LOCK TABLES only t WRITE and LOCK TABLES t READ, only WRITE lock a table named only, while PostgreSQL's LOCK TABLE ONLY t IN SHARE MODE keeps its reading. The branch rejects IN … MODE and a target without a lock type (LOCK TABLES t READ, u).
    • UNLOCK becomes a non-reserved keyword token, like CHECKSUM in feat: parse MySQL CHECKSUM TABLE and histogram update options #2883, so that a statement can start with it without a bare identifier alternative. It remains usable as a name (KeywordsTest, keyword sweeps), and ACCOUNT UNLOCK reads the token. INSTANCE, BACKUP, WRITE and CONCURRENT remain contextual words.
    • isStatementStartAhead() recognizes UNLOCK TABLE(S) and UNLOCK INSTANCE with the same dialect gates as the statement, so they end a preceding statement and are not captured as unsupported statements.
  • Dialects:

    Form no dialect MYSQL MARIADB other dialects
    LOCK TABLE(S) … lock_type, UNLOCK TABLE(S) parsed parsed parsed rejected, as on master
    WRITE CONCURRENT, trailing WAIT n / NOWAIT after a MySQL list rejected rejected parsed rejected
    LOCK INSTANCE FOR BACKUP, UNLOCK INSTANCE parsed parsed rejected (MariaDB has no such statement) rejected, as on master
    PostgreSQL/Oracle LOCK [TABLE] … unchanged unchanged unchanged unchanged
  • Wiring:

    • StatementVisitor default methods and StatementVisitorAdapter for the two new statements; StatementDeParser uses the appendTo that toString() uses.
    • TablesNamesFinder reports the locked tables, not their aliases; UNLOCK and the backup lock report none.
    • Validation: new features lockTables, unlockTables and backupLock. MySqlVersion enables all three, MariaDbVersion the first two, and PostgresqlVersion and OracleVersion lockTables. LOCK statements are now validated: a capability without lockTables, such as SQL Server or H2, reports them.
    • Feature analysis reports MODIFIES_TRANSACTION for all three statements, as for LOCK.
    • usage.rst, the README and the documentation index list the statements.

Validation

  • 80 new tests. On master (7123d25a1) every MySQL form they parse is a ParseException:
    • MySqlLockTablesTest (75): every form with and without Dialect.MYSQL (parse, toString(), StatementDeParser, reparse), MariaDB's forms, a table named only (with and without an alias, after a comma, qualified and quoted) in the MySQL and MariaDB dialects and without a dialect, the AST fields (lock types, aliases, TABLE/TABLES, wait clause), rejected forms, the MySQL forms rejected with Dialect.POSTGRESQL and Dialect.ORACLE, the unchanged PostgreSQL readings (LOCK tables NOWAIT, LOCK TABLE tables, instance, LOCK TABLE ONLY t IN SHARE MODE, …) in every dialect, the words as names and ACCOUNT UNLOCK, a mysqldump script, statement boundaries with and without unsupported-statement capture, TablesNamesFinder, feature analysis, AST node ranges and statements built in code.
    • LockStatementValidatorTest (5): the capabilities of MySQL, MariaDB, PostgreSQL and Oracle, and the three features.
    • KeywordsTest covers UNLOCK as a schema, table, column and alias name.
    • The existing LockTest, PostgreSqlLockTest and PostgreSqlTruncateTest pass unchanged.
  • Java 17 offline Gradle spotlessApply cleanTest check jar: 12,513 tests, 12,488 passed, 25 skipped, zero failures/errors. ASTNodeCoverageTest passes; a one-off run of its coverage walk over a script of the new statements reported no problem. The grammar choice conflict check, Checkstyle, PMD, SpotBugs and JaCoCo pass.
  • The statements were run against a MySQL 8.4.11 server. Every form the parser accepts in the MySQL dialect is accepted by the server's grammar, and every form it rejects is rejected, with two exceptions: LOW_PRIORITY WRITE, which MySQL 8.4 rejects and which is accepted for MySQL 8.0 and MariaDB as above, and LOCK INSTANCE without FOR BACKUP, which keeps the PostgreSQL/Oracle reading as a lock of a table named instance, as on master. The server also accepts an alias without AS (LOCK TABLES t a READ) and a table named only (LOCK TABLES only t WRITE), and so does the parser.
  • Not covered: a bare alias named only (LOCK TABLES t only WRITE) stays rejected, as SELECT * FROM t only is on master; LOCK TABLES t AS only WRITE parses.
  • Compared with master (7123d25a1) on a corpus of 188 MySQL 8.4 and 166 PostgreSQL 18 statements other than DDL/DCL, each accepted by the server, with the MySQL or PostgreSQL dialect and without a dialect:
    • Fixed with Dialect.MYSQL and without a dialect: LOCK TABLES t READ, s AS x WRITE, LOCK TABLES t READ LOCAL, UNLOCK TABLES and LOCK INSTANCE FOR BACKUP. Each renders identically to the source.
    • 0 regressed; no other case changed status, statement class or rendering. The PostgreSQL LOCK TABLE t IN SHARE ROW EXCLUSIVE MODE NOWAIT and LOCK t, s are unchanged.
  • Compared with master (7123d25a1) on a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, plus variants, scripts and the keyword sweeps (which use instance, backup, concurrent and write as table and column names): 0 regressed, 0 changed. ALTER USER … ACCOUNT UNLOCK and GRANT LOCK TABLES … are among them.

AI assistance: prepared with Claude Code.

…ements

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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant