Skip to content

Keep routine signatures and owning tables in DROP statements - #2809

Open
minleejae wants to merge 2 commits into
JSQLParser:masterfrom
minleejae:fix/drop-targets-20261014
Open

minleejae wants to merge 2 commits into
JSQLParser:masterfrom
minleejae:fix/drop-targets-20261014

Conversation

@minleejae

@minleejae minleejae commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Builds on #2806, which shares MySQL account-name parsing (MySqlAccountName()); please review that first.

Purpose

Keep every part of a DROP statement in the AST and in the rendered SQL. Today DROP PROCEDURE p(int) silently loses (int) in both, so overloaded PostgreSQL procedures cannot be told apart, and several common DROP forms fail to parse:

  • PostgreSQL: DROP FUNCTION f(int), g(text), DROP TYPE, DROP DOMAIN, DROP EVENT TRIGGER, DROP TABLESPACE, DROP USER, DROP GROUP, DROP DATABASE … WITH (FORCE) and REASSIGN OWNED.
  • MySQL: DROP EVENT, DROP [UNDO] TABLESPACE … [ENGINE [=] e], DROP USER 'app'@'%' and DROP ROLE 'r'@'h'.

Current design (As-is)

  • Only DROP FUNCTION keeps a signature: one list of argument strings in getTypeToParameters(), written after the last name. Other routine kinds parse the argument list and discard it.
  • Everything after the names is a raw token list in getParameters(), except the owner table of DROP INDEX … ON t:
    • DROP TRIGGER trg ON users CASCADE gives [ON, users, CASCADE];
    • DROP OWNED BY old_role CASCADE gives the name BY and the parameters [old_role, CASCADE].
  • Role names are tables, so a MySQL account name such as 'r'@'h' cannot be parsed.

Target design (To-be)

  • Routine targets. FUNCTION, PROCEDURE, ROUTINE and AGGREGATE targets are RoutineReference items in getRoutines(), the model that GRANT and COMMENT use. The grammar reuses the shared RoutineReference() production:
    • every target keeps its own signature, including argument modes, VARIADIC, array and multi-word types;
    • for aggregates, * and ORDER BY are kept; DROP AGGREGATE requires a signature;
    • the unused FuncArgsList productions are removed.
  • Role and account targets. USER, ROLE and GROUP targets and the roles of DROP OWNED BY are UserAccount items in getAccounts():
    • they are read with the shared MySqlAccountName() from Share MySQL account names across CREATE USER and DEFINER #2806, so DROP accepts the same MySQL spellings as CREATE USER, such as app@'%' and 'app'@localhost;
    • a quoted PostgreSQL role name such as "Admin" round-trips, with Admin in getValue() and " in getQuoteStr().
  • Owner tables. DROP TRIGGER t ON tbl and DROP RULE r ON tbl store tbl in the existing getTable() owner field, like DROP INDEX … ON. Table discovery reports it. SQL Server's ON DATABASE and ON ALL SERVER trigger scopes stay parameters.
  • CASCADE/RESTRICT is a CascadeBehavior (Consolidate CASCADE/RESTRICT enums (breaking API change) #2745) in getBehavior(). It is accepted once, as the last clause.
  • New object types. DROP TYPE, DOMAIN, EVENT, EVENT TRIGGER, TABLESPACE, USER and GROUP are accepted. ObjectType gains AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE and USER.
  • Type-specific clauses:
    • MySQL DROP UNDO TABLESPACE and ENGINE [=] name: isUndo(), getEngine() and isEngineUseEquals();
    • PostgreSQL DROP DATABASE … [WITH] (FORCE): isForce(). The optional WITH is always rendered.
  • REASSIGN OWNED. PostgreSQL REASSIGN OWNED BY r [, …] TO r is a new ReassignOwned statement:
    • its roles use the same UserAccount production as DROP OWNED BY;
    • REASSIGN is a new non-reserved token, since a statement that starts with a plain identifier creates choice conflicts;
    • it is wired into StatementDeParser, TablesNamesFinder, StatementVisitorAdapter and statement features (MODIFIES_SCHEMA);
    • validation uses the new Feature.reassignOwned, enabled for PostgreSQL.
  • Compatibility. Public signatures are unchanged and the legacy accessors are derived:
    • getName() and getNames() return read-only snapshots of routine and account names;
    • getTypeToParameters() and getParamsByType() return a read-only map with the last routine's argument declarations, where the legacy single-signature form kept them. The map now also covers PROCEDURE, ROUTINE and AGGREGATE, and lower-case drop function no longer loses its arguments;
    • getParameters() is a snapshot that includes ON with the owner table and the CASCADE/RESTRICT keyword;
    • setParameters() replaces the whole clause with raw tokens and clears both; addParameters() keeps them. Raw tokens are never parsed;
    • the legacy name setters and setTypeToParameters() first convert routine and account targets to the legacy names and single signature. For example, setName() on DROP FUNCTION f(int) still renders DROP FUNCTION g(int).
  • Behavior changes:
    • Mutating getTypeToParameters() of a parsed routine DROP now throws UnsupportedOperationException.
    • Builders that call addParameters("CASCADE") render the same SQL, but their object tree differs from a parsed statement. Two existing DropTest comparisons now use withBehavior().
    • A parenthesized list after a non-routine name (DROP TABLE t(int)) and tokens after CASCADE/RESTRICT are rejected. Before, they were silently dropped or kept as raw tokens.
  • Documentation. usage.rst documents the model and migration. README and index list DROP OWNED/REASSIGN OWNED.
  • The MySQL DROP INDEX … ALGORITHM/LOCK handling is unchanged.

Validation

  • New tests. 72 test cases in DropRoutineSignatureTest, DropObjectTypeTest, DropRoleTest and DropValidatorTest cover:
    • parse → toString → reparse and StatementDeParser output with Dialect.MYSQL/POSTGRESQL;
    • AST fields and editing the typed state;
    • legacy accessors, setters and builders;
    • custom table deparsers and table discovery;
    • statement features and capability validation;
    • clause words used as names, and rejected forms.
  • Gradle. Java 17 offline Gradle spotlessApply check: 10,457 tests, 10,432 passed, 25 skipped, zero failures/errors. Grammar, Checkstyle, PMD, SpotBugs and coverage checks pass.
  • Corpus: 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:
    • 23 target cases (36 results across the dialect and default modes) become clean, with zero regressions and zero other status changes.
    • Four statements that already parsed now have structured ASTs: DROP TRIGGER … ON users CASCADE and DROP RULE r1 ON v (owner table), and two DROP OWNED BY forms (roles instead of the name BY).
    • DROP PROCEDURE p1(int) renders its signature again.
    • Comparing AST dumps, only DROP statements change. The differences are the intended routines, accounts, table, behavior and objectType fields.
  • 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 209 MySQL and 336 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>
DROP PROCEDURE p(int) lost its signature, DROP FUNCTION accepted only one
signature, and owner tables, CASCADE/RESTRICT and the roles of DROP OWNED BY
were kept as raw parameter tokens.

Model routine targets as RoutineReference, role and account targets as
UserAccount read with MySqlAccountName(), the ON table of DROP TRIGGER/RULE in
the existing owner field and CASCADE/RESTRICT as CascadeBehavior. Keep
getNames(), getTypeToParameters(), getParamsByType() and getParameters() as
derived values.

Parse DROP TYPE, DOMAIN, EVENT, EVENT TRIGGER, [UNDO] TABLESPACE ... ENGINE,
USER, GROUP, MySQL account names in DROP USER/ROLE, DROP DATABASE ...
WITH (FORCE) and PostgreSQL REASSIGN OWNED.

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