Skip to content

Commit dd63575

Browse files
authored
feat: parse MySQL loadable aggregate functions and the ALTER TABLE validation modifier (#2917)
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>
1 parent 8d8fa7f commit dd63575

13 files changed

Lines changed: 1183 additions & 21 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
152152
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
153153
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
154154
| **MySQL resource groups** | `CREATE`/`ALTER`/`DROP RESOURCE GROUP` · `SET RESOURCE GROUP` |
155+
| **MySQL loadable functions** | `CREATE [AGGREGATE] FUNCTION … RETURNS … SONAME` |
155156
| **MySQL servers, plugins and components** | `CREATE SERVER` · `ALTER SERVER` with `OPTIONS (… PORT 3306)` · `INSTALL`/`UNINSTALL PLUGIN` · `INSTALL`/`UNINSTALL COMPONENT` |
156157
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
157158
| **MySQL server administration** | `FLUSH` · `KILL` · `RESET PERSIST` · `HANDLER … OPEN`/`READ`/`CLOSE` |

‎src/main/java/net/sf/jsqlparser/statement/CreateFunctionalStatement.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ public String getKind() {
142142
return kind;
143143
}
144144

145+
/**
146+
* @return the keywords that name the kind in the statement, which may carry a modifier of
147+
* {@link #getKind()}
148+
*/
149+
protected String getKindKeywords() {
150+
return kind;
151+
}
152+
145153
public void setOrReplace(boolean orReplace) {
146154
this.orReplace = orReplace;
147155
}
@@ -156,7 +164,7 @@ public String formatDeclaration() {
156164

157165
private StringBuilder appendDeclarationTo(StringBuilder builder,
158166
Consumer<TableElement> printer) {
159-
appendTokens(builder, functionDeclarationParts);
167+
appendTokens(builder, getFunctionDeclarationParts());
160168
if (returnType != null) {
161169
builder.append(' ');
162170
returnType.appendTo(builder, printer);
@@ -188,7 +196,7 @@ public StringBuilder appendTo(StringBuilder builder, Consumer<TableElement> prin
188196
if (definer != null) {
189197
builder.append("DEFINER = ").append(definer).append(' ');
190198
}
191-
builder.append(kind).append(' ');
199+
builder.append(getKindKeywords()).append(' ');
192200
return appendDeclarationTo(builder, printer);
193201
}
194202

‎src/main/java/net/sf/jsqlparser/statement/alter/AlterExpressionTableOption.java‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@
1111

1212
import java.util.Collections;
1313
import java.util.List;
14+
import java.util.Objects;
1415

1516
import net.sf.jsqlparser.statement.create.table.TableOption;
1617
import net.sf.jsqlparser.statement.create.table.ConstraintKind;
1718

1819
/**
1920
* Internal subclass for table-level option operations within ALTER TABLE. Handles ENGINE,
2021
* ALGORITHM, LOCK, KEY_BLOCK_SIZE, COMMENT, ENCRYPTION, AUTO_INCREMENT (SET_TABLE_OPTION),
21-
* DISCARD/IMPORT TABLESPACE, DISABLE/ENABLE KEYS.
22+
* DISCARD/IMPORT TABLESPACE, DISABLE/ENABLE KEYS, and MySQL's {@code WITH | WITHOUT VALIDATION}
23+
* modifier (VALIDATION).
2224
*/
2325
public class AlterExpressionTableOption extends AlterExpression {
2426

2527
private TableOption structuredTableOption;
28+
private AlterExpressionPartition.ExchangeValidationMode validationMode;
2629

2730
public TableOption getStructuredTableOption() {
2831
return structuredTableOption;
@@ -36,6 +39,28 @@ public void setStructuredTableOption(TableOption option) {
3639
super.setUseEqual(false);
3740
}
3841

42+
/**
43+
* @return the mode of MySQL's {@code WITH | WITHOUT VALIDATION} modifier, which applies to the
44+
* whole statement, or {@code null} for any other action. The mode written after
45+
* {@code EXCHANGE PARTITION ... WITH TABLE} belongs to that action instead.
46+
*/
47+
public AlterExpressionPartition.ExchangeValidationMode getValidationMode() {
48+
return getOperation() == AlterOperation.VALIDATION ? validationMode : null;
49+
}
50+
51+
/** Makes this action the {@code WITH | WITHOUT VALIDATION} modifier. */
52+
public void setValidationMode(AlterExpressionPartition.ExchangeValidationMode validationMode) {
53+
this.validationMode = Objects.requireNonNull(validationMode, "validationMode");
54+
structuredTableOption = null;
55+
setOperation(AlterOperation.VALIDATION);
56+
}
57+
58+
public AlterExpressionTableOption withValidationMode(
59+
AlterExpressionPartition.ExchangeValidationMode validationMode) {
60+
setValidationMode(validationMode);
61+
return this;
62+
}
63+
3964
@Override
4065
public List<ColumnIdentityAction> getColumnIdentityActions() {
4166
return Collections.emptyList();
@@ -164,6 +189,9 @@ protected void appendBody(StringBuilder b) {
164189
return;
165190
}
166191
switch (getOperation()) {
192+
case VALIDATION:
193+
b.append(validationMode).append(" VALIDATION");
194+
break;
167195
case SET_TABLE_OPTION:
168196
b.append(getTableOption());
169197
break;

‎src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import java.util.Locale;
1313

1414
public enum AlterOperation {
15-
ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY, ALTER_RELATION, ORDER_BY, FOREIGN_OPTIONS;
15+
ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, DROP_CHECK, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, ATTACH_PARTITION, DETACH_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY, ALTER_PRIMARY_KEY, ALTER_RELATION, ORDER_BY, FOREIGN_OPTIONS, VALIDATION;
1616

1717
public static AlterOperation from(String operation) {
1818
return Enum.valueOf(AlterOperation.class, operation.toUpperCase(Locale.ROOT));

‎src/main/java/net/sf/jsqlparser/statement/create/function/CreateFunction.java‎

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@
1010
package net.sf.jsqlparser.statement.create.function;
1111

1212
import java.util.Collection;
13+
import java.util.Collections;
1314
import java.util.List;
1415

1516
import net.sf.jsqlparser.statement.CreateFunctionalStatement;
17+
import net.sf.jsqlparser.statement.RoutineCharacteristic;
1618

1719
/**
18-
* A {@code CREATE PROCEDURE} statement
20+
* A {@code CREATE FUNCTION} statement. A MySQL loadable function
21+
* ({@code CREATE [AGGREGATE] FUNCTION name RETURNS type SONAME 'library'}) is held as a
22+
* {@link LoadableFunction}, which then is the source of the declaration parts.
1923
*/
2024
public class CreateFunction extends CreateFunctionalStatement {
2125

26+
private boolean aggregate;
27+
private LoadableFunction loadableFunction;
28+
2229
public CreateFunction() {
2330
super("FUNCTION");
2431
}
@@ -31,6 +38,71 @@ public CreateFunction(boolean orReplace, List<String> functionDeclarationParts)
3138
super(orReplace, "FUNCTION", functionDeclarationParts);
3239
}
3340

41+
/**
42+
* @return whether the function is declared with {@code AGGREGATE}, as a MySQL loadable
43+
* aggregate function is
44+
*/
45+
public boolean isAggregate() {
46+
return aggregate;
47+
}
48+
49+
public void setAggregate(boolean aggregate) {
50+
this.aggregate = aggregate;
51+
}
52+
53+
public CreateFunction withAggregate(boolean aggregate) {
54+
setAggregate(aggregate);
55+
return this;
56+
}
57+
58+
/**
59+
* @return the definition of a MySQL loadable function, or {@code null} for any other function
60+
*/
61+
public LoadableFunction getLoadableFunction() {
62+
return loadableFunction;
63+
}
64+
65+
/**
66+
* Makes this statement a MySQL loadable function. The definition replaces the declaration parts
67+
* and the characteristics; {@link #getFunctionDeclarationParts()} is derived from it.
68+
*
69+
* @param loadableFunction the definition, or {@code null} to remove it
70+
*/
71+
public void setLoadableFunction(LoadableFunction loadableFunction) {
72+
if (loadableFunction != null) {
73+
super.setFunctionDeclarationParts(null, Collections.emptyList());
74+
}
75+
this.loadableFunction = loadableFunction;
76+
}
77+
78+
public CreateFunction withLoadableFunction(LoadableFunction loadableFunction) {
79+
setLoadableFunction(loadableFunction);
80+
return this;
81+
}
82+
83+
@Override
84+
protected String getKindKeywords() {
85+
return aggregate ? "AGGREGATE " + getKind() : getKind();
86+
}
87+
88+
/**
89+
* @return the declaration parts; for a loadable function, a snapshot of
90+
* {@link #getLoadableFunction()}
91+
*/
92+
@Override
93+
public List<String> getFunctionDeclarationParts() {
94+
return loadableFunction != null ? loadableFunction.toDeclarationParts()
95+
: super.getFunctionDeclarationParts();
96+
}
97+
98+
/** Replaces the declaration parts, which also removes a loadable function definition. */
99+
@Override
100+
public void setFunctionDeclarationParts(List<String> functionDeclarationParts,
101+
List<RoutineCharacteristic> characteristics) {
102+
loadableFunction = null;
103+
super.setFunctionDeclarationParts(functionDeclarationParts, characteristics);
104+
}
105+
34106
@Override
35107
public CreateFunction withFunctionDeclarationParts(List<String> functionDeclarationParts) {
36108
return (CreateFunction) super.withFunctionDeclarationParts(functionDeclarationParts);
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.function;
11+
12+
import java.io.Serializable;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.Locale;
16+
import java.util.Objects;
17+
18+
import net.sf.jsqlparser.expression.StringValue;
19+
20+
/**
21+
* The definition of a MySQL loadable function:
22+
* {@code [IF NOT EXISTS] name RETURNS {STRING | INTEGER | REAL | DECIMAL} SONAME 'library'}.
23+
*/
24+
public class LoadableFunction implements Serializable {
25+
26+
/** The result type of a loadable function. */
27+
public enum ReturnType {
28+
STRING, INTEGER, REAL, DECIMAL;
29+
30+
/**
31+
* @param keyword a type keyword as MySQL spells it, including the synonyms {@code INT},
32+
* {@code INT4} and {@code DEC}
33+
* @return the result type, or {@code null} if the keyword names none
34+
*/
35+
public static ReturnType from(String keyword) {
36+
switch (keyword.toUpperCase(Locale.ROOT)) {
37+
case "STRING":
38+
return STRING;
39+
case "INT":
40+
case "INT4":
41+
case "INTEGER":
42+
return INTEGER;
43+
case "REAL":
44+
return REAL;
45+
case "DEC":
46+
case "DECIMAL":
47+
return DECIMAL;
48+
default:
49+
return null;
50+
}
51+
}
52+
}
53+
54+
private boolean ifNotExists;
55+
private String name;
56+
private String returnTypeKeyword;
57+
private StringValue library;
58+
59+
public LoadableFunction() {
60+
// filled by the setters
61+
}
62+
63+
public LoadableFunction(String name, ReturnType returnType, String library) {
64+
setName(name);
65+
setReturnType(returnType);
66+
setLibrary(new StringValue(library));
67+
}
68+
69+
public boolean isIfNotExists() {
70+
return ifNotExists;
71+
}
72+
73+
public void setIfNotExists(boolean ifNotExists) {
74+
this.ifNotExists = ifNotExists;
75+
}
76+
77+
public String getName() {
78+
return name;
79+
}
80+
81+
public void setName(String name) {
82+
this.name = name;
83+
}
84+
85+
public ReturnType getReturnType() {
86+
return returnTypeKeyword == null ? null : ReturnType.from(returnTypeKeyword);
87+
}
88+
89+
public void setReturnType(ReturnType returnType) {
90+
returnTypeKeyword = Objects.requireNonNull(returnType, "returnType").name();
91+
}
92+
93+
/**
94+
* @return the result type as written, which may be one of the synonyms {@code INT},
95+
* {@code INT4} and {@code DEC}
96+
*/
97+
public String getReturnTypeKeyword() {
98+
return returnTypeKeyword;
99+
}
100+
101+
/**
102+
* @param keyword a keyword that {@link ReturnType#from(String)} resolves
103+
*/
104+
public void setReturnTypeKeyword(String keyword) {
105+
if (ReturnType.from(keyword) == null) {
106+
throw new IllegalArgumentException(
107+
"A loadable function returns STRING, INTEGER, REAL or DECIMAL, not " + keyword);
108+
}
109+
returnTypeKeyword = keyword;
110+
}
111+
112+
/**
113+
* @return the shared library file named by {@code SONAME}
114+
*/
115+
public StringValue getLibrary() {
116+
return library;
117+
}
118+
119+
public void setLibrary(StringValue library) {
120+
this.library = library;
121+
}
122+
123+
public LoadableFunction withIfNotExists(boolean ifNotExists) {
124+
setIfNotExists(ifNotExists);
125+
return this;
126+
}
127+
128+
/**
129+
* @return the definition as the declaration parts of the CREATE FUNCTION statement
130+
*/
131+
public List<String> toDeclarationParts() {
132+
List<String> parts = new ArrayList<>();
133+
if (ifNotExists) {
134+
parts.add("IF");
135+
parts.add("NOT");
136+
parts.add("EXISTS");
137+
}
138+
parts.add(name);
139+
parts.add("RETURNS");
140+
parts.add(returnTypeKeyword);
141+
parts.add("SONAME");
142+
parts.add(String.valueOf(library));
143+
return parts;
144+
}
145+
146+
@Override
147+
public String toString() {
148+
return String.join(" ", toDeclarationParts());
149+
}
150+
}

‎src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
import net.sf.jsqlparser.statement.create.extension.CreateExtension;
170170
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
171171
import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper;
172+
import net.sf.jsqlparser.statement.create.function.CreateFunction;
172173
import net.sf.jsqlparser.statement.create.index.CreateIndex;
173174
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
174175
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
@@ -2634,7 +2635,11 @@ public void visit(AlterSequence alterSequence) {
26342635

26352636
@Override
26362637
public <S> Void visit(CreateFunctionalStatement createFunctionalStatement, S context) {
2637-
throwUnsupported(createFunctionalStatement);
2638+
// a MySQL loadable function names a shared library and no table
2639+
if (!(createFunctionalStatement instanceof CreateFunction)
2640+
|| ((CreateFunction) createFunctionalStatement).getLoadableFunction() == null) {
2641+
throwUnsupported(createFunctionalStatement);
2642+
}
26382643
return null;
26392644
}
26402645

0 commit comments

Comments
 (0)