Skip to content

feat: parse MySQL loadable aggregate functions and the ALTER TABLE validation modifier - #2917

Merged
manticore-projects merged 2 commits into
JSQLParser:masterfrom
minleejae:feat/mysql-loadable-function-validation-20261113
Nov 14, 2026
Merged

manticore-projects merged 2 commits into
JSQLParser:masterfrom
minleejae:feat/mysql-loadable-function-validation-20261113

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

Two MySQL 8.4 forms did not get a usable AST:

  • CREATE AGGREGATE FUNCTION [IF NOT EXISTS] name RETURNS {STRING | INTEGER | REAL | DECIMAL} SONAME 'library' (CREATE FUNCTION Statement for Loadable Functions) came back as an UnsupportedStatement, while the same statement without AGGREGATE is a CreateFunction.
  • {WITH | WITHOUT} VALIDATION as a modifier of the ALTER TABLE action list (ALTER TABLE Statement, alter_commands_modifier in sql_yacc.yy) was read as raw text that swallowed the rest of the statement.

Current design (As-is)

CREATE AGGREGATE FUNCTION f RETURNS INTEGER SONAME 'udf.so'              -- UnsupportedStatement
CREATE FUNCTION f RETURNS INTEGER SONAME 'udf.so'                        -- CreateFunction, token list only
ALTER TABLE t1 WITHOUT VALIDATION, EXCHANGE PARTITION p0 WITH TABLE t2   -- one UNSPECIFIC action holding
                                                                         -- "WITHOUT VALIDATION , EXCHANGE PARTITION p0 WITH TABLE t2"
ALTER TABLE t1 ADD COLUMN c INT, WITHOUT VALIDATION                      -- ADD + UNSPECIFIC "WITHOUT VALIDATION"
  • Create() has no branch for AGGREGATE, so the statement reaches the unsupported fallback. The form without AGGREGATE goes through the routine token capture: CreateFunction only has getFunctionDeclarationParts(), and the capture also accepts what MySQL rejects (RETURNS DOUBLE, a trailing COMMENT, …).
  • AlterExpression() knows WITH | WITHOUT VALIDATION only as the tail of EXCHANGE PARTITION … WITH TABLE t. Anywhere else it falls into the captureRest() alternative, which takes every remaining token, so the actions after it are lost (TablesNamesFinder did not see t2 above) and toString() prints ,.

Target design (To-be)

Loadable functions

  • CreateFunction gets isAggregate() and getLoadableFunction(). The new LoadableFunction holds isIfNotExists(), the name, the ReturnType (STRING, INTEGER, REAL, DECIMAL; the synonyms INT, INT4 and DEC keep their spelling in getReturnTypeKeyword()) and the library as a StringValue.
  • The definition is the single source: getFunctionDeclarationParts() is derived from it, and setFunctionDeclarationParts(…) removes it. toString() and StatementDeParser share CreateFunctionalStatement.appendTo, which now asks the subclass for the kind keywords (AGGREGATE FUNCTION); getKind() stays FUNCTION.
  • Both forms use one production, CreateLoadableFunction(). SONAME is not a token.
  • TablesNamesFinder returns no tables for a loadable function (it still refuses other CreateFunction statements). Validation is unchanged: functionalStatement and createFunction.

Validation modifier

  • AlterOperation.VALIDATION, carried by AlterExpressionTableOption with getValidationMode(), next to the ALGORITHM and LOCK modifiers. It reuses AlterExpressionPartition.ExchangeValidationMode, and one production (AlterValidationMode()) reads the keywords for both the modifier and the EXCHANGE PARTITION tail, which keeps its own getExchangeValidationMode().
ALTER TABLE t1 WITHOUT VALIDATION, EXCHANGE PARTITION p0 WITH TABLE t2          -- VALIDATION, EXCHANGE_PARTITION
ALTER TABLE t1 WITH VALIDATION, EXCHANGE PARTITION p0 WITH TABLE t2 WITHOUT VALIDATION
ALTER TABLE t1 ADD COLUMN c INT, WITHOUT VALIDATION, ADD COLUMN d INT           -- ADD, VALIDATION, ADD
ALTER TABLE t1 ALGORITHM = COPY, WITH VALIDATION, LOCK = SHARED
ALTER TABLE t1 WITH VALIDATION PARTITION BY HASH (id) PARTITIONS 2              -- VALIDATION, PARTITION_BY

Dialects

Dialect Loadable function Validation modifier
MySQL AGGREGATE, or a function name directly followed by RETURNS, must continue as a loadable function, as in sql_yacc.yy (udf_tail); no OR REPLACE WITH/WITHOUT at the start of an action must be the modifier; rejected after a comma that follows a command MySQL allows only at the end of the list (standalone_alter_commands, PARTITION BY, REMOVE PARTITIONING)
MariaDB, none a statement of exactly this shape gets the definition (OR REPLACE included); everything else keeps its reading read where it is a whole item of the list; everything else keeps its reading
others unchanged unchanged

Behaviour changes

  • With the MySQL dialect, statements that MySQL 8.4 rejects and master accepted as tokens or raw text are now parse errors, for example CREATE FUNCTION f RETURNS DOUBLE SONAME 'udf.so', CREATE OR REPLACE FUNCTION f RETURNS INTEGER SONAME 'udf.so', CREATE AGGREGATE FUNCTION f (a INT) RETURNS INTEGER RETURN 1, ALTER TABLE t1 WITH NO VALIDATION, ALTER TABLE t1 EXCHANGE PARTITION p0 WITH TABLE t2, WITHOUT VALIDATION.
  • With MySQL, MariaDB or no dialect, an action list that contained the modifier now has one action per item (VALIDATION instead of UNSPECIFIC), and toString() prints , instead of , after it.
  • TablesNamesFinder returns an empty result for a loadable function instead of throwing.
  • No public signature changed.

Validation

  • New tests: MySqlLoadableFunctionTest (45) and MySqlAlterValidationModifierTest (36): parse per dialect, toString, StatementDeParser, reparse, AST fields and node spans, rejected forms, other dialects, statement boundaries and scripts, visitors, TablesNamesFinder, validation features, model mutation.

  • ./gradlew spotlessApply cleanTest check jar: 14,533 tests, 0 failures, 25 skipped (including checkGrammarAmbiguity, Checkstyle, PMD, SpotBugs, JaCoCo).

  • Compared with master (cf057dd33) on the MySQL and PostgreSQL DDL/DCL and non-DDL statement corpora, in the dialect and without one: 1 statement fixed in each MySQL run (ALTER TABLE t1 WITHOUT VALIDATION, EXCHANGE PARTITION p0 WITH TABLE t2, now two actions), 0 regressions, 0 other changes.

  • MySQL 8.4.11: of 128 statements, the 55 whose syntax the server accepts all parse with Dialect.MYSQL (some stop only at execution, for example with a missing shared library), and 63 of the 73 it rejects are parse errors. The other 10 are accepted as on master, by code this change does not touch: the stored-routine token capture (CREATE DEFINER = … FUNCTION f RETURNS INTEGER SONAME …, CREATE FUNCTION f() RETURNS INTEGER SONAME …), the unsupported-statement fallbacks (CREATE DEFINER = … AGGREGATE FUNCTION …, ALTER AGGREGATE FUNCTION …), the raw-text action (ALTER TABLE t , WITH VALIDATION, ALTER TABLE t VALIDATION), and the end-of-list position rule for other items (… DISCARD TABLESPACE, LOCK = NONE, … EXCHANGE PARTITION p0 WITH TABLE t2, ALGORITHM = INPLACE, … REMOVE PARTITIONING, ALGORITHM = COPY, … WITH VALIDATION, ADD COLUMN c INT, DISCARD TABLESPACE).

AI assistance: prepared with Claude Code.

minleejae and others added 2 commits November 13, 2026 10:51
…lidation modifier

CREATE [AGGREGATE] FUNCTION [IF NOT EXISTS] name RETURNS type SONAME 'library'
is a CreateFunction with a LoadableFunction definition; the AGGREGATE form was
an UnsupportedStatement. WITH | WITHOUT VALIDATION is a typed ALTER TABLE
action (AlterOperation.VALIDATION) wherever MySQL accepts the modifier in the
action list, instead of raw text that swallowed the following actions.

With the MySQL dialect both follow the server grammar. MariaDB and no dialect
read them only where the statement has exactly that shape; other dialects are
unchanged.

Signed-off-by: minleejae <mmj9808@gmail.com>
Preserve MySQL loadable functions alongside PostgreSQL aggregate support.
Update overlapping unsupported-statement expectations to the combined
feature set while keeping both implementations unchanged.

Validation: mvn --batch-mode --no-transfer-progress test
15,888 tests; zero failures/errors; 25 existing skips.
JavaCC reports no choice conflicts.
@manticore-projects
manticore-projects merged commit dd63575 into JSQLParser:master Nov 14, 2026
4 of 5 checks passed
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.

2 participants