Skip to content

Parse MySQL GRANT and REVOKE privilege levels, privileges and accounts - #2807

Open
minleejae wants to merge 2 commits into
JSQLParser:masterfrom
minleejae:feat/mysql-grant-revoke-20261014
Open

minleejae wants to merge 2 commits into
JSQLParser:masterfrom
minleejae:feat/mysql-grant-revoke-20261014

Conversation

@minleejae

@minleejae minleejae commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #2806, which shares the MySQL account name rule (MySqlAccountName()) of CREATE USER and DEFINER.

Purpose

Parse MySQL GRANT and REVOKE, and give their privileges, privilege levels and accounts a typed model that MySQL and PostgreSQL share. Today only the simplest MySQL form parses (GRANT SELECT ON db1.t1 TO app). Common statements fail, for example:

  • privilege levels: ON *.*, ON db1.*, ON *;
  • quoted accounts: TO 'app'@'%', TO `app`@`%`, TO app@'%', TO 'app';
  • privilege names such as INDEX, CREATE VIEW, LOCK TABLES and BACKUP_ADMIN. The CDC grant GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'%' fails with Invalid Kind: REPLICATION;
  • quoted role grants, PROXY, REVOKE ALL PRIVILEGES, GRANT OPTION, REVOKE IF EXISTS ... IGNORE UNKNOWN USER and GRANT ... AS user WITH ROLE ....

PostgreSQL GRANT r TO u WITH INHERIT TRUE, SET FALSE (PostgreSQL 16) also fails; a single option works.

Current design (As-is)

  • PrivilegeClause keeps grantees, roles and the grantor as strings. TO GROUP staff becomes the grantee "GROUP staff".
  • Grantees and roles are single identifiers, not MySQL account names, so only the unquoted app@localhost passes (the lexer reads it as one identifier).
  • AccessPrivilege() accepts one token and maps it with Privilege.Kind.valueOf. Multi-word and unknown privilege names fail, some with Invalid Kind.
  • AccessTarget() accepts identifier lists only, so * levels fail.
  • A role membership (GRANT r TO u) carries an empty PrivilegeTarget of kind TABLE.
  • Grant holds a single WITH option.
  • The MySQL and MariaDB validation capabilities do not include revoke.

Target design (To-be)

  • Privilege levels: PrivilegeTarget.getWildcard() returns DEFAULT_DATABASE (*), GLOBAL (*.*) or DATABASE (db.*). The database of db.* is the single entry of getNames(); * is never stored as a name part. Wildcards accept the optional TABLE keyword and no other object type. Table-name discovery skips wildcard levels.
  • Privileges: the MySQL static privileges are new Privilege.Kind constants, for example CREATE_TEMPORARY_TABLES, REPLICATION_SLAVE, SHOW_DATABASES and GRANT_OPTION. Any other identifier is a dynamic privilege: kind DYNAMIC, with getDynamicName() as written. getName() returns the SQL spelling.
  • Grantees: the new Grantee is shared by MySQL and PostgreSQL. It is used for grantees, the PostgreSQL grantor, the MySQL AS user and the proxied account.
    • ACCOUNT holds a UserAccount: a MySQL user[@host] with its quoting, or a PostgreSQL role name without host.
    • The other kinds are GROUP, PUBLIC, CURRENT_USER (also CURRENT_USER()), CURRENT_ROLE and SESSION_USER.
    • Accounts and role lists are read with the MySqlAccountName() rule of Share MySQL account names across CREATE USER and DEFINER #2806, so every spelling that CREATE USER accepts works here too.
  • Role memberships: getRoleItems() holds UserAccount values, so 'reader' and 'writer'@'%' keep quoting and host. A parsed membership has no target (null).
  • MySQL clauses:
    • PROXY ON user is a PROXY privilege on a PrivilegeTarget of the new kind ACCOUNT.
    • REVOKE ALL [PRIVILEGES], GRANT OPTION FROM ... has the privileges ALL and GRANT_OPTION and no target.
    • Revoke records IF EXISTS and IGNORE UNKNOWN USER.
    • Grant.getAsUser() and getWithRole() hold AS user [WITH ROLE DEFAULT | NONE | ALL [EXCEPT ...] | role, ...]. The role selection is the new RoleSelection in statement.create.user, next to UserAccount, so the MySQL account statements can reuse it.
  • Options: Grant.getOptions() lists every WITH option, including PostgreSQL membership options. Privileges take only GRANT OPTION; memberships take distinct ADMIN, INHERIT and SET options.
  • Grammar:
    • New account productions next to MySqlAccountName(): MySqlAccountNameList() and MySqlRoleSelection(boolean). They are meant for reuse by the MySQL account statements.
    • New Access* productions: AccessGrantee() (account or GROUP/PUBLIC/CURRENT_USER/CURRENT_ROLE/SESSION_USER), AccessGrantOption() and AccessGrantor().
    • No new tokens or reserved words. The MySQL clauses are rejected inside ALTER DEFAULT PRIVILEGES.
  • Wiring:
    • toString and StatementDeParser render the new state.
    • GrantValidator validates named grantees and roles only, not PUBLIC or CURRENT_USER.
    • The MySQL and MariaDB capabilities include revoke.
    • The usage guide documents the model.

Compatibility: existing public methods keep their signatures and are derived from the typed state.

  • getUsers(), getGrantees() and getRoles() are mutable string views that write through; assigned text is converted with Grantee.valueOf().
  • getOption() returns the first option; setOption() replaces all options.
  • getGrantedBy() and setGrantedBy() render and convert the grantor.
  • getObjectName() returns a wildcard level as written, for example db1.*; setObjectName() replaces it with a named target.
  • new Privilege("CREATE VIEW") now yields the typed kind; the rendered SQL is unchanged.
  • Parsed PUBLIC and CURRENT_USER are rendered in upper case.
  • setRoles() and setGrantees() copy their input instead of keeping the caller's list.
  • Parsed role memberships have a null target. new Grant() still starts with an empty table target, and setRole() clears it.

Validation

  • Two new test classes, 87 tests:
    • MySqlGrantRevokeTest: 26 MySQL forms, each parsed, deparsed and reparsed with Dialect.MYSQL and in the default mode. Also the AST of privilege levels, privileges, accounts, CURRENT_USER, role memberships, PROXY, REVOKE ALL PRIVILEGES, GRANT OPTION, IF EXISTS/IGNORE UNKNOWN USER and AS ... WITH ROLE; 23 rejected forms; typed builders; MySQL/MariaDB validation.
    • GranteeModelTest: PostgreSQL role specifications and membership option lists with Dialect.POSTGRESQL, rejected forms, and the legacy string API (write-through views, builder/parse object-tree equality, wildcard object names).
  • Existing GRANT/REVOKE and ALTER DEFAULT PRIVILEGES tests pass unchanged.
  • Java 17 offline Gradle spotlessApply check on top of Share MySQL account names across CREATE USER and DEFINER #2806: 10,474 tests, 10,449 passed, 25 skipped, zero failures/errors. The grammar choice conflict check passes.
  • Compared with master (bdf8ac31) on a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, plus variants:
    • 50 target cases now parse, deparse and reparse with their dialect: 30 MySQL GRANT/REVOKE cases, 17 MySQL variants, 2 user@'host' variants and PostgreSQL WITH INHERIT TRUE, SET FALSE. The 31 cases of the main MySQL and PostgreSQL corpora are also fixed without a dialect.
    • No regressions and no other status changes.
    • SQL output is unchanged for every statement that parsed before. Only the AST of the 63 previously parsed GRANT, REVOKE and ALTER DEFAULT PRIVILEGES cases changes, as intended: typed grantees, and no target for role memberships.
  • 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 235 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>

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