Skip to content

Parse MySQL ALTER USER, RENAME USER and role statements with shared user options - #2808

Open
minleejae wants to merge 3 commits into
JSQLParser:masterfrom
minleejae:feat/mysql-account-statements-20261014
Open

minleejae wants to merge 3 commits into
JSQLParser:masterfrom
minleejae:feat/mysql-account-statements-20261014

Conversation

@minleejae

@minleejae minleejae commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #2807, which builds on #2806. It uses their shared MySqlAccountName(), MySqlAccountNameList() and MySqlRoleSelection() productions and the RoleSelection class.

Purpose

Parse the MySQL/MariaDB account management statements. Today ALTER USER fails in every form, even ALTER USER app IDENTIFIED BY 'x'. RENAME USER, SET DEFAULT ROLE, SET PASSWORD FOR …, SET ROLE ALL EXCEPT … and CREATE ROLE 'reader'/CREATE ROLE IF NOT EXISTS … fail as well. CREATE USER fails as soon as any option follows the accounts, for example DEFAULT ROLE, REQUIRE SSL, WITH MAX_QUERIES_PER_HOUR, PASSWORD EXPIRE, ACCOUNT LOCK or COMMENT. Tools that provision, migrate or audit MySQL accounts cannot read this SQL.

Current design (As-is)

  • CreateUser keeps only the account list (UserAccount with UserAuthentication). The grammar has no option clauses.
  • Every ALTER USER goes to PostgreSQL's AlterRole. CREATE ROLE accepts a single PostgreSQL name.
  • SET ROLE x and SET PASSWORD = 'x' parse as a generic SetStatement that assigns a variable named ROLE or PASSWORD. The other SET forms fail.

Target design (To-be)

  • Shared options. UserOptions (getOptions() on CreateUser and the new AlterUser) holds MySQL's clauses in their fixed order:

    • DEFAULT ROLE as a RoleSelection;
    • REQUIRE as TlsOptions: NONE, SSL, X509, CIPHER/ISSUER/SUBJECT 'v', keeping the optional AND;
    • WITH as ResourceLimits;
    • password and locking options as PasswordLockOptions in source order. Each has a kind plus a number or a keyword (DEFAULT, NEVER, OPTIONAL, UNBOUNDED);
    • COMMENT or ATTRIBUTE.

    One production, MySqlUserOptions, parses them for both statements. Omitted clauses stay omitted.

  • Authentication. UserAuthentication adds REPLACE 'current' (getCurrentCredential()), RETAIN CURRENT PASSWORD and the mode DISCARD_OLD_PASSWORD. These are accepted only after a password assignment.

  • New statements

    • AlterUser: [IF EXISTS], accounts with authentication changes, and the shared options. ALTER USER USER() … keeps USER() as the unquoted account name, as DEFINER = CURRENT_USER does; isUserFunction() identifies it. ALTER USER acct DEFAULT ROLE {NONE | ALL | role, …} stores the roles in getOptions().getDefaultRoles().
    • RenameUser with ordered from/to pairs.
    • SetRoleStatement, SetDefaultRoleStatement (MySQL TO, MariaDB FOR or no account) and SetPasswordStatement (= expr or TO RANDOM, with REPLACE and RETAIN CURRENT PASSWORD). The password is an expression, so MariaDB's PASSWORD('x') keeps parsing.
    • CreateRole gains isIfNotExists() and getRoles() for MySQL role names such as 'r' and 'r'@'h'.
  • Account and role names come from the shared MySqlAccountName()/MySqlAccountNameList(), so every spelling that CREATE USER accepts works here too. They are UserAccount values without authentication. The shared RoleSelection (DEFAULT, NONE, ALL, ALL_EXCEPT, ROLES) is used by SET ROLE, SET DEFAULT ROLE and both DEFAULT ROLE clauses.

  • Dispatch keeps PostgreSQL behavior

    • ALTER USER becomes MySQL only for IF EXISTS, USER(), a quoted name, a host, an account list, no action, or a MySQL account clause. ALTER USER app WITH LOGIN, PASSWORD 'x', SET …, RENAME TO and ALL RESET ALL remain AlterRole.
    • CREATE USER app WITH MAX_…/PASSWORD EXPIRE now selects CreateUser. These forms used to fail on the PostgreSQL path. WITH LOGIN, PASSWORD 'x' and Dialect.POSTGRESQL still select CreateRole.
    • CREATE ROLE uses the MySQL form only for IF NOT EXISTS, quoted names, hosts or a list. A single unquoted name keeps the PostgreSQL getName(). So does an unquoted r@h, which the lexer reads as one identifier, except in the MySQL and MariaDB dialects.
    • SET DEFAULT ROLE, SET PASSWORD FOR, SET ROLE ALL EXCEPT … and role names with a quoted user or host are typed in every dialect. They have no generic form.
    • SET ROLE … and SET PASSWORD = …/TO RANDOM share the variable-assignment shape that PostgreSQL uses (SET ROLE name, SET ROLE NONE). They are typed only with Dialect.MYSQL/MARIADB; otherwise they remain a SetStatement. SET LOCAL ROLE … and SET role = … stay a SetStatement in every dialect.
  • New reserved words: none. Option words are matched contextually, and the grammar check reports no choice conflicts.

  • Wiring

    • StatementVisitor default methods and the adapter, StatementDeParser (the SET PASSWORD expression goes through the expression deparser) and TablesNamesFinder (no tables).
    • Validation: new features alterUser, renameUser, setRole, setDefaultRole and setPassword. The MySQL and MariaDB capabilities enable them and createRole.
    • StatementFeatureVisitor: MODIFIES_SCHEMA, or MODIFIES_SESSION for SET ROLE.
  • Docs: a usage guide section, plus a README and index row.

  • Compatibility

    • The public API change is additive.
    • With Dialect.MYSQL/MARIADB, SET ROLE … and SET PASSWORD = … now return the typed statements instead of SetStatement, and CREATE ROLE r@h fills getRoles(). Rendered SQL is unchanged.
  • Not included

Validation

  • 101 new test cases cover:
    • every form in the MySQL dialect: parse, deparse and reparse, plus the AST fields;
    • MariaDB forms and default-mode dispatch;
    • quoted and lexer-merged account spellings in ALTER/RENAME USER, SET PASSWORD, SET DEFAULT ROLE, CREATE ROLE and SET ROLE, with and without a dialect;
    • unchanged PostgreSQL ALTER USER, CREATE USER, CREATE ROLE and SET ROLE;
    • rejected combinations;
    • builder deparsing;
    • option words used as identifiers;
    • statement features, table discovery and validation capabilities.
  • Java 17 offline Gradle spotlessApply check on top of Parse MySQL GRANT and REVOKE privilege levels, privileges and accounts #2807: 10,602 tests, 10,577 passed, 25 skipped, zero failures/errors. The grammar choice conflict check passes.
  • Compared with Parse MySQL GRANT and REVOKE privilege levels, privileges and accounts #2807 on a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, plus variants: no regressions, and no output changed except:
    • Fixed with Dialect.MYSQL:
      • CREATE USER with options, ALTER USER, RENAME USER, quoted/IF NOT EXISTS CREATE ROLE, SET DEFAULT ROLE, SET ROLE ALL EXCEPT and SET PASSWORD FOR (16 statements, plus 10 variants), now parsed.
    • The same 16 statements also parse without a dialect.
    • SET ROLE 'reader', SET ROLE DEFAULT, SET PASSWORD = 'x' change from SetStatement to the typed statements with Dialect.MYSQL only, as intended. They stay SetStatement in default mode.
  • MySQL 8.4 and PostgreSQL 18.3: every corpus statement this branch parses ran as written on one server and as rendered by toString() on a second, identical server, in the same order. All 250 MySQL and 328 PostgreSQL statements give the same result (success, or the same error).

Integration validation

Applied #2806–#2819 together on master (bdf8ac31), with the stacked PRs on top of their bases (#2806 → #2807 → #2808, #2806 → #2809 → #2810 and #2811 → #2812).

  • Each branch merges cleanly into master. Between the branches, conflicts are limited to adjacent additions (README/index rows, usage sections, imports, visitor and feature lists, one enum line and one alias guard).
  • The integrated build passes Java 17 offline Gradle spotlessApply check: 11,494 tests, 11,469 passed, 25 skipped, zero failures/errors. The grammar choice conflict check passes.
  • On a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, the integrated build parses 265 of 271 MySQL statements (master: 206) and 363 of 364 PostgreSQL statements (master: 328). Every MySQL 8.4 non-reserved word and PostgreSQL 18 unreserved or column-name keyword parses as a column and table name.
  • On paired MySQL 8.4 and PostgreSQL 18.3 servers, all 263 MySQL and 362 PostgreSQL statements rendered by the integrated build give the same results as the originals.

CREATE USER and DEFINER read user@host with their own copies of the
same rule. Neither accepted app@'%' or 'app'@localhost, because the
lexer reads app@ and @localhost as single tokens. Without a dialect a
double-quoted part also lost its quotes, so CREATE USER "app"@"%"
rendered as app@%.

Add one MySqlAccountName() production that splits those tokens, keep
the quote character of double-quoted and backquoted parts apart from
their value, and use it for CREATE USER and DEFINER.

Signed-off-by: minleejae <mmj9808@gmail.com>
…ccounts

Parse MySQL privilege levels (*, *.*, db.*), static and dynamic
privileges, user@host accounts, quoted role grants, PROXY,
REVOKE ALL PRIVILEGES, GRANT OPTION, REVOKE IF EXISTS ... IGNORE
UNKNOWN USER and GRANT ... AS user WITH ROLE.

Grantees, granted roles and the grantor use a typed Grantee/UserAccount
model shared with PostgreSQL, which also gains membership option lists
such as WITH INHERIT TRUE, SET FALSE. Accounts are read with the shared
MySqlAccountName() rule, and WITH ROLE uses a RoleSelection next to
UserAccount. Role memberships no longer carry an empty table target.
The string accessors remain as derived views.

Signed-off-by: minleejae <mmj9808@gmail.com>
…ared user options

Model the account options of CREATE USER and ALTER USER once in UserOptions
(DEFAULT ROLE, REQUIRE, WITH resource limits, password and locking options,
COMMENT/ATTRIBUTE), parsed by one shared production. Add ALTER USER, RENAME
USER, MySQL CREATE ROLE names, SET ROLE, SET DEFAULT ROLE and SET PASSWORD,
reading accounts and roles with the shared MySqlAccountName(),
MySqlAccountNameList() and MySqlRoleSelection() productions.

PostgreSQL ALTER USER and SET ROLE keep their existing AST. SET ROLE and
SET PASSWORD without FOR become typed statements with the MySQL and MariaDB
dialects.

Signed-off-by: minleejae <mmj9808@gmail.com>
@minleejae
minleejae force-pushed the feat/mysql-account-statements-20261014 branch from 819970e to f5b522a Compare October 14, 2026 08:21

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