From 38c567b7d51c20701a057f2d3d3fcbfb355506e7 Mon Sep 17 00:00:00 2001 From: minleejae Date: Tue, 10 Nov 2026 15:51:35 +0900 Subject: [PATCH] feat: parse MySQL resource group statements CREATE RESOURCE GROUP and ALTER RESOURCE GROUP were read as UnsupportedStatement, SET RESOURCE GROUP and DROP RESOURCE GROUP ... FORCE failed, and DROP RESOURCE GROUP rg was a Drop of type RESOURCE named GROUP. Add CreateResourceGroup and AlterResourceGroup, which share the name, the VCPU list (VcpuRange), the thread priority and the ENABLE or DISABLE state through ResourceGroupDefinition, and SetResourceGroup with its thread IDs. DROP RESOURCE GROUP is a Drop with the object type RESOURCE_GROUP and the existing force flag. The statements are read with the MySQL dialect or none and follow MySQL's grammar: the option order, the optional = or :=, the optional commas, a signed THREAD_PRIORITY, and FORCE after ALTER and DROP. RESOURCE, VCPU and THREAD_PRIORITY stay usable as names; no token is added. Wire the statements into the deparser, visitors, table finder, feature analysis and validation, with new createResourceGroup, alterResourceGroup and setResourceGroup features. Signed-off-by: minleejae --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 21 + .../statement/StatementFeatureVisitor.java | 18 + .../statement/StatementVisitor.java | 27 + .../statement/StatementVisitorAdapter.java | 18 + .../sf/jsqlparser/statement/drop/Drop.java | 10 +- .../resourcegroup/AlterResourceGroup.java | 79 +++ .../resourcegroup/CreateResourceGroup.java | 94 +++ .../resourcegroup/OptionAssignment.java | 35 + .../ResourceGroupDefinition.java | 130 ++++ .../resourcegroup/SetResourceGroup.java | 91 +++ .../statement/resourcegroup/VcpuRange.java | 98 +++ .../sf/jsqlparser/util/TablesNamesFinder.java | 18 + .../util/deparser/StatementDeParser.java | 18 + .../util/validation/feature/MySqlVersion.java | 4 + .../validator/StatementValidator.java | 21 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 174 ++++- src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 34 + .../parser/ASTNodeCoverageTest.java | 3 + .../resourcegroup/ResourceGroupTest.java | 604 ++++++++++++++++++ .../validator/StatementValidatorTest.java | 3 + 22 files changed, 1499 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/resourcegroup/AlterResourceGroup.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/resourcegroup/CreateResourceGroup.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/resourcegroup/OptionAssignment.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupDefinition.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/resourcegroup/SetResourceGroup.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/resourcegroup/VcpuRange.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupTest.java diff --git a/README.md b/README.md index 0fdeacbe3..ac0e500a5 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` | | **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` | | **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` | +| **MySQL resource groups** | `CREATE`/`ALTER`/`DROP RESOURCE GROUP` · `SET RESOURCE GROUP` | | **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 | | **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` | | **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 7382b49e2..1cdc01298 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -60,6 +60,9 @@ import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.role.ReassignOwned; import net.sf.jsqlparser.statement.select.Fetch; import net.sf.jsqlparser.statement.select.First; @@ -796,6 +799,24 @@ public enum Feature { * @see BackupLockStatement */ backupLock, + /** + * MySQL "CREATE RESOURCE GROUP" statement is allowed + * + * @see CreateResourceGroup + */ + createResourceGroup, + /** + * MySQL "ALTER RESOURCE GROUP" statement is allowed + * + * @see AlterResourceGroup + */ + alterResourceGroup, + /** + * MySQL "SET RESOURCE GROUP" statement is allowed + * + * @see SetResourceGroup + */ + setResourceGroup, /** * SQL "COMMENT ON" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index be1c4cf27..5a80771c7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -111,6 +111,9 @@ import net.sf.jsqlparser.statement.oracle.OracleNullStatement; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.role.CreateRole; @@ -928,6 +931,21 @@ public Void visit(XaStatement statement, S context) { : transactionOnly(); } + @Override + public Void visit(CreateResourceGroup statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterResourceGroup statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(SetResourceGroup statement, S context) { + return sessionOnly(); + } + @Override public Void visit(Commit commit, S context) { return transactionOnly(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 2711d3a76..68a3a4007 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -100,6 +100,9 @@ import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.show.ShowCreateStatement; @@ -197,6 +200,30 @@ default void visit(XaStatement statement) { visit(statement, null); } + default T visit(CreateResourceGroup statement, S context) { + return null; + } + + default void visit(CreateResourceGroup statement) { + visit(statement, null); + } + + default T visit(AlterResourceGroup statement, S context) { + return null; + } + + default void visit(AlterResourceGroup statement) { + visit(statement, null); + } + + default T visit(SetResourceGroup statement, S context) { + return null; + } + + default void visit(SetResourceGroup statement) { + visit(statement, null); + } + T visit(Commit commit, S context); default void visit(Commit commit) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 2d43aff58..0fea4c99a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -105,6 +105,9 @@ import net.sf.jsqlparser.statement.merge.MergeOperationVisitorAdapter; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.FromItemVisitor; @@ -262,6 +265,21 @@ public T visit(XaStatement statement, S context) { return null; } + @Override + public T visit(CreateResourceGroup statement, S context) { + return null; + } + + @Override + public T visit(AlterResourceGroup statement, S context) { + return null; + } + + @Override + public T visit(SetResourceGroup statement, S context) { + return null; + } + @Override public T visit(Commit commit, S context) { diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java index 86ad0c03f..176ffd5f1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java +++ b/src/main/java/net/sf/jsqlparser/statement/drop/Drop.java @@ -35,7 +35,7 @@ public class Drop extends ASTNodeAccessImpl implements Statement { public enum ObjectType { - DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, CAST, OPERATOR, TRANSFORM, OTHER; + DATABASE, EVENT, FUNCTION, INDEX, PROCEDURE, SCHEMA, SEQUENCE, SERVER, TABLE, TABLESPACE, TRIGGER, VIEW, FOREIGN_TABLE, FOREIGN_DATA_WRAPPER, AGGREGATE, DOMAIN, EVENT_TRIGGER, GROUP, OWNED, ROLE, ROUTINE, RULE, TYPE, USER, SYNONYM, ACCESS_METHOD, LANGUAGE, OPERATOR_CLASS, OPERATOR_FAMILY, TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE, CAST, OPERATOR, TRANSFORM, RESOURCE_GROUP, OTHER; /** Targets are {@link RoutineReference}s, each with an optional argument signature. */ public boolean hasRoutineTargets() { @@ -432,7 +432,10 @@ public Drop withUndo(boolean undo) { return this; } - /** PostgreSQL DROP DATABASE ... WITH (FORCE), or Oracle DROP SYNONYM ... FORCE. */ + /** + * PostgreSQL DROP DATABASE ... WITH (FORCE), or Oracle DROP SYNONYM ... FORCE and MySQL DROP + * RESOURCE GROUP ... FORCE. + */ public boolean isForce() { return force; } @@ -634,7 +637,8 @@ public StringBuilder appendTo(StringBuilder builder, Consumer tablePrinte builder.append(" USING ").append(accessMethod); } if (force) { - builder.append(objectType == ObjectType.SYNONYM ? " FORCE" : " WITH (FORCE)"); + builder.append(objectType == ObjectType.SYNONYM + || objectType == ObjectType.RESOURCE_GROUP ? " FORCE" : " WITH (FORCE)"); } if (engine != null) { builder.append(engineUseEquals ? " ENGINE = " : " ENGINE ").append(engine); diff --git a/src/main/java/net/sf/jsqlparser/statement/resourcegroup/AlterResourceGroup.java b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/AlterResourceGroup.java new file mode 100644 index 000000000..76363a4ea --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/AlterResourceGroup.java @@ -0,0 +1,79 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +import java.util.Collection; +import java.util.List; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * MySQL {@code ALTER RESOURCE GROUP group_name [VCPU [=] vcpu_spec [, vcpu_spec] ...] + * [THREAD_PRIORITY [=] N] [ENABLE | DISABLE] [FORCE]}. MySQL's grammar takes FORCE without DISABLE + * too. + * + * @see MySQL ALTER + * RESOURCE GROUP Statement + */ +public class AlterResourceGroup extends ResourceGroupDefinition { + private boolean force; + + public AlterResourceGroup() { + // the parser fills the statement in + } + + public AlterResourceGroup(String name) { + super(name); + } + + /** Whether FORCE is written: the group is changed although threads are assigned to it. */ + public boolean isForce() { + return force; + } + + public void setForce(boolean force) { + this.force = force; + } + + public AlterResourceGroup withForce(boolean force) { + setForce(force); + return this; + } + + public AlterResourceGroup withVcpus(List vcpus) { + setVcpus(vcpus); + return this; + } + + public AlterResourceGroup addVcpuRanges(Collection vcpus) { + addVcpus(vcpus); + return this; + } + + public AlterResourceGroup withThreadPriority(Integer threadPriority) { + setThreadPriority(threadPriority); + return this; + } + + public AlterResourceGroup withState(State state) { + setState(state); + return this; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + appendOptionsTo(builder.append("ALTER RESOURCE GROUP ").append(getName())); + return force ? builder.append(" FORCE") : builder; + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/resourcegroup/CreateResourceGroup.java b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/CreateResourceGroup.java new file mode 100644 index 000000000..ef340be22 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/CreateResourceGroup.java @@ -0,0 +1,94 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * MySQL {@code CREATE RESOURCE GROUP group_name TYPE [=] {SYSTEM | USER} [VCPU [=] vcpu_spec [, + * vcpu_spec] ...] [THREAD_PRIORITY [=] N] [ENABLE | DISABLE]}. + * + * @see MySQL CREATE + * RESOURCE GROUP Statement + */ +public class CreateResourceGroup extends ResourceGroupDefinition { + /** Whether the group takes system threads or user threads. */ + public enum Type { + SYSTEM, USER + } + + private Type type; + private OptionAssignment typeAssignment = OptionAssignment.EQUALS; + + public CreateResourceGroup() { + // the parser fills the statement in + } + + public CreateResourceGroup(String name, Type type) { + super(name); + setType(type); + } + + public Type getType() { + return type; + } + + public final void setType(Type type) { + this.type = Objects.requireNonNull(type, "type"); + } + + public OptionAssignment getTypeAssignment() { + return typeAssignment; + } + + public void setTypeAssignment(OptionAssignment typeAssignment) { + this.typeAssignment = Objects.requireNonNull(typeAssignment, "typeAssignment"); + } + + public CreateResourceGroup withTypeAssignment(OptionAssignment typeAssignment) { + setTypeAssignment(typeAssignment); + return this; + } + + public CreateResourceGroup withVcpus(List vcpus) { + setVcpus(vcpus); + return this; + } + + public CreateResourceGroup addVcpuRanges(Collection vcpus) { + addVcpus(vcpus); + return this; + } + + public CreateResourceGroup withThreadPriority(Integer threadPriority) { + setThreadPriority(threadPriority); + return this; + } + + public CreateResourceGroup withState(State state) { + setState(state); + return this; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE RESOURCE GROUP ").append(getName()).append(' '); + typeAssignment.appendTo(builder, "TYPE").append(type); + return appendOptionsTo(builder); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/resourcegroup/OptionAssignment.java b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/OptionAssignment.java new file mode 100644 index 000000000..30b9364d3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/OptionAssignment.java @@ -0,0 +1,35 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +/** + * How a MySQL resource group option is joined to its value: {@code TYPE USER}, {@code TYPE = USER} + * or {@code TYPE := USER}. MySQL reads the three alike. + */ +public enum OptionAssignment { + NONE(""), EQUALS("="), COLON_EQUALS(":="); + + private final String symbol; + + OptionAssignment(String symbol) { + this.symbol = symbol; + } + + /** The symbol as written, empty for {@link #NONE}. */ + public String getSymbol() { + return symbol; + } + + /** Appends the option name, the symbol if there is one, and a space before the value. */ + StringBuilder appendTo(StringBuilder builder, String option) { + builder.append(option).append(' '); + return this == NONE ? builder : builder.append(symbol).append(' '); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupDefinition.java b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupDefinition.java new file mode 100644 index 000000000..beb77d1e5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupDefinition.java @@ -0,0 +1,130 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; + +/** + * The name and the options that MySQL's CREATE RESOURCE GROUP and ALTER RESOURCE GROUP share: + * {@code [VCPU [=] vcpu_spec [, vcpu_spec] ...] [THREAD_PRIORITY [=] N] [ENABLE | DISABLE]}, in + * that order. The commas of the VCPU list are optional in MySQL and are always written here. + */ +public abstract class ResourceGroupDefinition extends ASTNodeAccessImpl implements Statement { + /** Whether threads may be assigned to the group. */ + public enum State { + ENABLE, DISABLE + } + + private String name; + private List vcpus = new ArrayList<>(); + private OptionAssignment vcpuAssignment = OptionAssignment.EQUALS; + private Integer threadPriority; + private OptionAssignment threadPriorityAssignment = OptionAssignment.EQUALS; + private State state; + + protected ResourceGroupDefinition() { + // the parser fills the statement in + } + + protected ResourceGroupDefinition(String name) { + setName(name); + } + + public String getName() { + return name; + } + + public final void setName(String name) { + this.name = Objects.requireNonNull(name, "name"); + } + + /** The VCPU list; empty when the statement has no VCPU clause. */ + public List getVcpus() { + return vcpus; + } + + public void setVcpus(List vcpus) { + this.vcpus = vcpus == null ? new ArrayList<>() : vcpus; + } + + public void addVcpus(VcpuRange... vcpus) { + addVcpus(Arrays.asList(vcpus)); + } + + public void addVcpus(Collection vcpus) { + this.vcpus.addAll(vcpus); + } + + public OptionAssignment getVcpuAssignment() { + return vcpuAssignment; + } + + public void setVcpuAssignment(OptionAssignment vcpuAssignment) { + this.vcpuAssignment = Objects.requireNonNull(vcpuAssignment, "vcpuAssignment"); + } + + /** The THREAD_PRIORITY value, or null when the statement has no such clause. */ + public Integer getThreadPriority() { + return threadPriority; + } + + public void setThreadPriority(Integer threadPriority) { + this.threadPriority = threadPriority; + } + + public OptionAssignment getThreadPriorityAssignment() { + return threadPriorityAssignment; + } + + public void setThreadPriorityAssignment(OptionAssignment threadPriorityAssignment) { + this.threadPriorityAssignment = + Objects.requireNonNull(threadPriorityAssignment, "threadPriorityAssignment"); + } + + /** ENABLE or DISABLE, or null when the statement has neither. */ + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + /** Appends the shared options, each with a leading space. */ + protected StringBuilder appendOptionsTo(StringBuilder builder) { + if (!vcpus.isEmpty()) { + vcpuAssignment.appendTo(builder.append(' '), "VCPU"); + for (int i = 0; i < vcpus.size(); i++) { + vcpus.get(i).appendTo(i > 0 ? builder.append(", ") : builder); + } + } + if (threadPriority != null) { + threadPriorityAssignment.appendTo(builder.append(' '), "THREAD_PRIORITY") + .append(threadPriority); + } + if (state != null) { + builder.append(' ').append(state); + } + return builder; + } + + public abstract StringBuilder appendTo(StringBuilder builder); + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/resourcegroup/SetResourceGroup.java b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/SetResourceGroup.java new file mode 100644 index 000000000..8aae9ab74 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/SetResourceGroup.java @@ -0,0 +1,91 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +import java.util.Objects; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * MySQL {@code SET RESOURCE GROUP group_name [FOR thread_id [, thread_id] ...]}, which assigns the + * named threads, or the current thread, to a resource group. A thread ID is the number as written: + * a {@code LongValue}, or a {@code HexValue} for {@code 0x1F} and {@code X'1F'}. The commas are + * optional in MySQL and are always written here. + * + * @see MySQL SET RESOURCE + * GROUP Statement + */ +public class SetResourceGroup extends ASTNodeAccessImpl implements Statement { + private String name; + private ExpressionList threadIds; + + public SetResourceGroup() { + // the parser fills the statement in + } + + public SetResourceGroup(String name) { + setName(name); + } + + public String getName() { + return name; + } + + public final void setName(String name) { + this.name = Objects.requireNonNull(name, "name"); + } + + /** The thread IDs after FOR, or null when the statement assigns the current thread. */ + public ExpressionList getThreadIds() { + return threadIds; + } + + public void setThreadIds(ExpressionList threadIds) { + this.threadIds = threadIds; + } + + public SetResourceGroup withThreadIds(ExpressionList threadIds) { + setThreadIds(threadIds); + return this; + } + + public SetResourceGroup addThreadIds(Expression... threadIds) { + if (this.threadIds == null) { + this.threadIds = new ExpressionList<>(); + } + for (Expression threadId : threadIds) { + this.threadIds.add(threadId); + } + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("SET RESOURCE GROUP ").append(name); + if (threadIds != null && !threadIds.isEmpty()) { + for (int i = 0; i < threadIds.size(); i++) { + builder.append(i == 0 ? " FOR " : ", ").append(threadIds.get(i)); + } + } + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/resourcegroup/VcpuRange.java b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/VcpuRange.java new file mode 100644 index 000000000..be607ca08 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/resourcegroup/VcpuRange.java @@ -0,0 +1,98 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +import java.io.Serializable; +import java.util.Objects; + +/** + * One element of a MySQL resource group's VCPU list: a single CPU number such as {@code 5}, or an + * inclusive range such as {@code 0-3}. MySQL checks the numbers against the machine, and that a + * range does not run backwards, when it executes the statement. + */ +public class VcpuRange implements Serializable { + private int start; + private Integer end; + + /** A single CPU. */ + public VcpuRange(int start) { + this(start, null); + } + + /** The CPUs from {@code start} to {@code end}; a null {@code end} names the single CPU. */ + public VcpuRange(int start, Integer end) { + setStart(start); + setEnd(end); + } + + private static int requireCpuNumber(int number) { + if (number < 0) { + throw new IllegalArgumentException("A CPU number is not negative: " + number); + } + return number; + } + + public int getStart() { + return start; + } + + public final void setStart(int start) { + this.start = requireCpuNumber(start); + } + + /** The last CPU of a range written as {@code start-end}, or null for a single CPU. */ + public Integer getEnd() { + return end; + } + + public final void setEnd(Integer end) { + if (end != null) { + requireCpuNumber(end); + } + this.end = end; + } + + /** Whether the element is written as {@code start-end}. */ + public boolean isRange() { + return end != null; + } + + /** The last CPU the element covers: the end of a range, or the single CPU. */ + public int getLast() { + return end != null ? end : start; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(start); + return end != null ? builder.append('-').append(end) : builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof VcpuRange)) { + return false; + } + VcpuRange range = (VcpuRange) other; + return start == range.start && Objects.equals(end, range.end); + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f65ddf14f..890c138e6 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -238,6 +238,9 @@ import net.sf.jsqlparser.statement.piped.WindowPipeOperator; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.role.CreateRole; @@ -2272,6 +2275,21 @@ public Void visit(XaStatement statement, S context) { return null; } + @Override + public Void visit(CreateResourceGroup statement, S context) { + return null; + } + + @Override + public Void visit(AlterResourceGroup statement, S context) { + return null; + } + + @Override + public Void visit(SetResourceGroup statement, S context) { + return null; + } + @Override public Void visit(Commit commit, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 41defbd12..0fa80f076 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -135,6 +135,9 @@ import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; @@ -503,6 +506,21 @@ public StringBuilder visit(XaStatement statement, S context) { return statement.appendTo(builder); } + @Override + public StringBuilder visit(CreateResourceGroup statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(AlterResourceGroup statement, S context) { + return statement.appendTo(builder); + } + + @Override + public StringBuilder visit(SetResourceGroup statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(Commit commit, S context) { return commit.appendTo(builder); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java index 0b0eaf25e..aa6498362 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java @@ -85,6 +85,10 @@ public enum MySqlVersion implements Version { // https://dev.mysql.com/doc/refman/8.0/en/lock-instance-for-backup.html Feature.backupLock, + // https://dev.mysql.com/doc/refman/8.0/en/resource-groups.html + Feature.createResourceGroup, Feature.alterResourceGroup, + Feature.setResourceGroup, + // https://dev.mysql.com/doc/refman/8.0/en/account-management-statements.html Feature.createRole, Feature.createUser, Feature.alterUser, Feature.renameUser, Feature.setRole, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index a69059598..2e20b30b6 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -142,6 +142,9 @@ import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.AlterResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.CreateResourceGroup; +import net.sf.jsqlparser.statement.resourcegroup.SetResourceGroup; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; @@ -459,6 +462,24 @@ public Void visit(XaStatement statement, S context) { return null; } + @Override + public Void visit(CreateResourceGroup statement, S context) { + validateFeature(Feature.createResourceGroup); + return null; + } + + @Override + public Void visit(AlterResourceGroup statement, S context) { + validateFeature(Feature.alterResourceGroup); + return null; + } + + @Override + public Void visit(SetResourceGroup statement, S context) { + validateFeature(Feature.setResourceGroup); + return null; + } + @Override public Void visit(Commit commit, S context) { validateFeature(Feature.commit); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e4fcc2e76..0b15dceae 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -93,6 +93,7 @@ import net.sf.jsqlparser.statement.piped.*; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.refresh.*; import net.sf.jsqlparser.statement.reindex.Reindex; +import net.sf.jsqlparser.statement.resourcegroup.*; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.show.*; import net.sf.jsqlparser.statement.truncate.*; @@ -3620,6 +3621,60 @@ public class CCJSqlParser extends AbstractJSqlParser { return dialect == null || Dialect.POSTGRESQL.name().equals(dialect); } + /** + * Whether the words RESOURCE GROUP of a MySQL resource group statement are at the offset. Read + * with the MySQL dialect or none; MariaDB and the other products have no resource groups. + */ + private boolean isResourceGroupAt(int offset) { + String dialect = getAsString(Feature.dialect); + return isWordAt(offset, "RESOURCE") && getToken(offset + 1).kind == K_GROUP + && (dialect == null || Dialect.MYSQL.name().equals(dialect)); + } + + /** + * Whether DROP names a MySQL resource group. Without a dialect, DROP RESOURCE GROUP on its own + * keeps dropping an object called GROUP. + */ + private boolean isDropResourceGroupAhead() { + int next = getToken(3).kind; + return isResourceGroupAt(1) + && (getAsString(Feature.dialect) != null || next != EOF && next != ST_SEMICOLON); + } + + /** Whether MySQL CREATE, ALTER or SET RESOURCE GROUP starts here. */ + private boolean isResourceGroupStatementAhead() { + int kind = getToken(1).kind; + return (kind == K_CREATE || kind == K_ALTER || kind == K_SET) && isResourceGroupAt(2); + } + + /** A group name; MySQL reserves FORCE, the word that may follow the name. */ + private String resourceGroupName(String name) throws ParseException { + requireDdlSyntax(!"FORCE".equalsIgnoreCase(name), "Expected a resource group name"); + return name; + } + + /** + * A number that MySQL's lexer reads as NUM, as a CPU number and a thread priority must be: + * digits up to 2147483647. + */ + private int resourceGroupNumber(Token token) throws ParseException { + if (token.image.matches("[0-9]+")) { + java.math.BigInteger number = new java.math.BigInteger(token.image); + if (number.bitLength() < Integer.SIZE) { + return number.intValue(); + } + } + throw new ParseException("Expected an integer up to 2147483647 but found " + token.image); + } + + /** A thread ID: MySQL takes an unsigned integer of up to 64 bits. */ + private LongValue resourceGroupThreadId(Token token) throws ParseException { + requireDdlSyntax(token.image.matches("[0-9]+") + && new java.math.BigInteger(token.image).bitLength() <= Long.SIZE, + "Expected a thread ID but found " + token.image); + return linkToLastToken(new LongValue(token.image)); + } + /** * Whether a MySQL XA statement starts here: XA followed by its action. Read with the MySQL and * MariaDB dialects or none; XA stays an identifier elsewhere. @@ -4097,7 +4152,8 @@ public class CCJSqlParser extends AbstractJSqlParser { } static boolean isForceAhead(Drop drop, Token first, Token second, Token third) { - return drop.getObjectType() == Drop.ObjectType.SYNONYM && first.kind == K_FORCE + return (drop.getObjectType() == Drop.ObjectType.SYNONYM + || drop.getObjectType() == Drop.ObjectType.RESOURCE_GROUP) && first.kind == K_FORCE || drop.getObjectType() == Drop.ObjectType.DATABASE && (first.kind == K_WITH && "(".equals(second.image) && third.kind == K_FORCE || "(".equals(first.image) && second.kind == K_FORCE); @@ -4118,6 +4174,16 @@ public class CCJSqlParser extends AbstractJSqlParser { && "+-*/<>=~!@#%^&|?".indexOf(token.image.charAt(0)) >= 0; } + /** MySQL DROP RESOURCE GROUP names one group by a plain identifier, without IF EXISTS. */ + static boolean isResourceGroupTarget(Drop drop) { + if (drop.isIfExists() || drop.isMaterialized() || drop.getNames().size() != 1) { + return false; + } + Table name = drop.getNames().get(0); + return name.getSchemaName() == null && !name.getName().startsWith("'") + && !"FORCE".equalsIgnoreCase(name.getName()); + } + /** The targets are a cast, a transform or operators, named by their types. */ static boolean hasSignatureTargets(Drop drop) { return drop.getCast() != null || drop.getTransform() != null @@ -5616,6 +5682,8 @@ Statement SingleStatement() : | LOOKAHEAD(3) stm = Upsert() | + LOOKAHEAD({ isResourceGroupStatementAhead() }) stm = ResourceGroupStatement() + | LOOKAHEAD(2) stm = Alter() | LOOKAHEAD({ getToken(1).kind == K_RENAME && getToken(2).kind == K_USER @@ -21709,6 +21777,9 @@ Drop Drop(): // PostgreSQL DROP OWNED BY role [, ...] LOOKAHEAD({ isKeywordAhead("OWNED") && getToken(2).kind == K_BY }) tk= | + // MySQL DROP RESOURCE GROUP + LOOKAHEAD({ isDropResourceGroupAhead() }) { multiWordType = "RESOURCE GROUP"; } + | // MySQL DROP UNDO TABLESPACE LOOKAHEAD({ isKeywordAhead("UNDO") && getToken(2).kind == K_TABLESPACE }) tk= { drop.setUndo(true); } @@ -21815,6 +21886,10 @@ Drop Drop(): requireDdlSyntax(!DropLookahead.hasSignatureTargets(drop) || dropArgs.isEmpty() && indexTable == null && indexOptions.isEmpty(), "Unexpected clause after DROP " + drop.getType()); + requireDdlSyntax(drop.getObjectType() != Drop.ObjectType.RESOURCE_GROUP + || DropLookahead.isResourceGroupTarget(drop) && dropArgs.isEmpty() + && indexTable == null && indexOptions.isEmpty() && behavior == null, + "DROP RESOURCE GROUP takes one group name and FORCE"); if (dropArgs.size() > 0) { drop.setParameters(dropArgs); } @@ -24471,6 +24546,103 @@ Expression XaTransactionIdPart(): { return part; } } +/** + * MySQL CREATE, ALTER and SET RESOURCE GROUP. RESOURCE, VCPU and THREAD_PRIORITY are not tokens; + * isResourceGroupStatementAhead() routes here. DROP RESOURCE GROUP is read by Drop(). + */ +Statement ResourceGroupStatement(): +{ Statement statement; } +{ + ( + ContextualKeyword("RESOURCE") statement=CreateResourceGroup() + | + ContextualKeyword("RESOURCE") statement=AlterResourceGroup() + | + ContextualKeyword("RESOURCE") statement=SetResourceGroup() + ) + { return statement; } +} + +CreateResourceGroup CreateResourceGroup(): +{ CreateResourceGroup group = new CreateResourceGroup(); String name; OptionAssignment assignment; } +{ + name=RelObjectName() { group.setName(resourceGroupName(name)); } + assignment=ResourceGroupOptionAssignment() { group.setTypeAssignment(assignment); } + ( { group.setType(CreateResourceGroup.Type.USER); } + | { group.setType(CreateResourceGroup.Type.SYSTEM); } ) + ResourceGroupOptions(group) + { return group; } +} + +AlterResourceGroup AlterResourceGroup(): +{ AlterResourceGroup group = new AlterResourceGroup(); String name; } +{ + name=RelObjectName() { group.setName(resourceGroupName(name)); } + ResourceGroupOptions(group) + // FORCE INSTALL starts the next statement + [ LOOKAHEAD({ getToken(1).kind == K_FORCE && getToken(2).kind != K_INSTALL }) + { group.setForce(true); } ] + { return group; } +} + +/** The optional = or := between a resource group option and its value. */ +OptionAssignment ResourceGroupOptionAssignment(): +{ OptionAssignment assignment = OptionAssignment.NONE; } +{ + [ "=" { assignment = OptionAssignment.EQUALS; } | ":=" { assignment = OptionAssignment.COLON_EQUALS; } ] + { return assignment; } +} + +/** + * [VCPU [=] vcpu_spec [[,] vcpu_spec] ...] [THREAD_PRIORITY [=] [-] N] [ENABLE | DISABLE], in the + * order of MySQL's grammar. + */ +void ResourceGroupOptions(ResourceGroupDefinition group): +{ OptionAssignment assignment; VcpuRange range; Token number; boolean negative = false; } +{ + [ LOOKAHEAD({ isKeywordAhead("VCPU") }) ContextualKeyword("VCPU") + assignment=ResourceGroupOptionAssignment() { group.setVcpuAssignment(assignment); } + range=ResourceGroupVcpuRange() { group.addVcpus(range); } + ( [ "," ] range=ResourceGroupVcpuRange() { group.addVcpus(range); } )* ] + [ LOOKAHEAD({ isKeywordAhead("THREAD_PRIORITY") }) ContextualKeyword("THREAD_PRIORITY") + assignment=ResourceGroupOptionAssignment() { group.setThreadPriorityAssignment(assignment); } + [ "-" { negative = true; } ] number= + { group.setThreadPriority(negative ? -resourceGroupNumber(number) : resourceGroupNumber(number)); } ] + [ { group.setState(ResourceGroupDefinition.State.ENABLE); } + | { group.setState(ResourceGroupDefinition.State.DISABLE); } ] +} + +/** A CPU number, or two of them around a minus sign. */ +VcpuRange ResourceGroupVcpuRange(): +{ Token start; Token end = null; } +{ + start= [ "-" end= ] + { return new VcpuRange(resourceGroupNumber(start), end == null ? null : Integer.valueOf(resourceGroupNumber(end))); } +} + +SetResourceGroup SetResourceGroup(): +{ SetResourceGroup statement = new SetResourceGroup(); String name; Expression threadId; } +{ + name=RelObjectName() { statement.setName(resourceGroupName(name)); } + [ threadId=ResourceGroupThreadId() { statement.addThreadIds(threadId); } + ( [ "," ] threadId=ResourceGroupThreadId() { statement.addThreadIds(threadId); } )* ] + { linkElements(statement.getThreadIds()); return statement; } +} + +/** A thread ID: a decimal or hexadecimal number. */ +Expression ResourceGroupThreadId(): +{ Token token; Expression threadId; } +{ + ( + token= { threadId = resourceGroupThreadId(token); } + | + token= + { requireDdlSyntax(isXaHexPart(token.image), "Invalid hexadecimal literal: " + token.image); + threadId = linkToLastToken(new HexValue(token.image)); } + ) + { return threadId; } +} + /** * SET [scope] TRANSACTION modes, SET [scope] SESSION CHARACTERISTICS AS TRANSACTION modes and * SET [scope] TRANSACTION SNAPSHOT 'id'. The modes are those of BEGIN and START TRANSACTION, and diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 0097242f8..7ac1307bd 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -166,6 +166,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad - ``VACUUM`` · ``ANALYZE`` · ``CLUSTER`` · ``CHECKPOINT`` * - **MySQL locks** - ``LOCK TABLES`` · ``UNLOCK TABLES`` · ``LOCK INSTANCE FOR BACKUP`` · ``UNLOCK INSTANCE`` + * - **MySQL resource groups** + - ``CREATE``/``ALTER``/``DROP RESOURCE GROUP`` · ``SET RESOURCE GROUP`` * - **PostgreSQL session statements** - ``DISCARD`` · ``LISTEN`` · ``UNLISTEN`` · ``SHOW``/``RESET TIME ZONE``/``TRANSACTION ISOLATION LEVEL``/``SESSION AUTHORIZATION`` · ``COPY`` options from before 9.0 * - **MySQL account management** diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 9d8943000..9a468e41d 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -3011,6 +3011,40 @@ The validator checks ``twoPhaseCommit`` (PostgreSQL) and ``xa`` (MySQL and Maria Feature analysis reports ``XA RECOVER`` as returning a result set and the other statements as transaction control. +MySQL resource groups +===================== + +MySQL ``CREATE RESOURCE GROUP name TYPE [=] {SYSTEM | USER} [VCPU [=] vcpu_spec [, ...]] +[THREAD_PRIORITY [=] N] [ENABLE | DISABLE]`` produces a ``CreateResourceGroup`` and +``ALTER RESOURCE GROUP name [VCPU ...] [THREAD_PRIORITY ...] [ENABLE | DISABLE] [FORCE]`` +an ``AlterResourceGroup``. Both extend ``ResourceGroupDefinition``, which holds the name, +the VCPU list as ``VcpuRange`` elements (a single CPU or ``start-end``), the thread +priority and the ``State``. Each option remembers whether it was written with ``=``, ``:=`` +or nothing (``OptionAssignment``); the optional commas of the VCPU list are always printed. +``SET RESOURCE GROUP name [FOR thread_id [, ...]]`` produces a ``SetResourceGroup`` whose +thread IDs are the numbers as written (``LongValue`` or ``HexValue``); +``getThreadIds()`` is null without ``FOR``. +``DROP RESOURCE GROUP name [FORCE]`` is a ``Drop`` with the object type ``RESOURCE_GROUP`` +and ``isForce()``. + +.. code-block:: java + + CreateResourceGroup group = (CreateResourceGroup) CCJSqlParserUtil.parse( + "CREATE RESOURCE GROUP rg1 TYPE = USER VCPU = 0-3, 5 THREAD_PRIORITY = 10 ENABLE", + parser -> parser.withDialect(Dialect.MYSQL)); + // USER, [0-3, 5], 10, ENABLE + String parts = group.getType() + ", " + group.getVcpus() + ", " + + group.getThreadPriority() + ", " + group.getState(); + +The statements are read with the MySQL dialect or without one and follow MySQL's grammar: +the options come in the order above, and CPU numbers and the priority are plain integers. +What MySQL checks when it runs the statement, such as the CPU numbers of the machine or the +priority range of the group type, is not checked. In other dialects ``CREATE`` and ``ALTER +RESOURCE GROUP`` stay an ``UnsupportedStatement`` and ``DROP RESOURCE GROUP rg`` keeps the +generic ``Drop`` reading. The validator checks ``createResourceGroup``, +``alterResourceGroup`` and ``setResourceGroup`` (MySQL); feature analysis reports +``SET RESOURCE GROUP`` as a session change and the others as schema changes. + MySQL SHOW statements ===================== diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java index f65d16d82..8236c72f3 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -136,6 +136,9 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException { "PREPARE TRANSACTION 'x'", "COMMIT PREPARED 'x'", "ROLLBACK PREPARED 'x'", "ROLLBACK WORK FORCE '1.2.3'", "XA START X'0a', B'01', 1 JOIN", "XA END 'x' SUSPEND FOR MIGRATE", "XA RECOVER CONVERT XID", + "CREATE RESOURCE GROUP rg TYPE = USER VCPU = 0-3, 5 THREAD_PRIORITY = 10 ENABLE", + "ALTER RESOURCE GROUP rg VCPU = 0 DISABLE FORCE", + "SET RESOURCE GROUP rg FOR 14, 0x1F", "DROP RESOURCE GROUP rg FORCE", "CREATE SCHEMA s CREATE TABLE t (a INT) CREATE VIEW v AS SELECT 1", "CREATE TABLE t AS EXECUTE p", "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO r", diff --git a/src/test/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupTest.java b/src/test/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupTest.java new file mode 100644 index 000000000..40918d18a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/resourcegroup/ResourceGroupTest.java @@ -0,0 +1,604 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.resourcegroup; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.HexValue; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.ASTNodeAccess; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.Node; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.parser.feature.FeatureConfiguration; +import net.sf.jsqlparser.statement.SetStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.drop.Drop; +import net.sf.jsqlparser.statement.resourcegroup.ResourceGroupDefinition.State; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.MariaDbVersion; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** MySQL CREATE, ALTER, DROP and SET RESOURCE GROUP. */ +class ResourceGroupTest { + /** Resource groups are read with the MySQL dialect or without one. */ + private static final Dialect[] MYSQL = {Dialect.MYSQL, null}; + private static final Dialect[] OTHERS = + {Dialect.MARIADB, Dialect.POSTGRESQL, Dialect.ORACLE, Dialect.SQLSERVER, Dialect.H2}; + + private static Statement parse(String sql, Dialect dialect) throws JSQLParserException { + return dialect == null ? CCJSqlParserUtil.parse(sql) + : CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)); + } + + private static void assertSpans(String expected, String source, ASTNodeAccess access) { + Node node = access.getASTNode(); + assertNotNull(node, expected); + assertEquals(expected, source.substring(node.jjtGetFirstToken().absoluteBegin - 1, + node.jjtGetLastToken().absoluteEnd - 1)); + } + + /** + * Parses {@code sql}, which must print as {@code expected}, and checks the statement span, the + * deparser, the reparse, the feature analysis and the table finder. + */ + private static T roundTrip(Class type, String sql, String expected, + Dialect dialect, StmtFeature... features) throws Exception { + T statement = assertInstanceOf(type, parse(sql, dialect), sql); + assertEquals(expected, statement.toString()); + assertSpans(sql, sql, (ASTNodeAccess) statement); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output), null); + assertEquals(expected, output.toString()); + Statement reparsed = parse(expected, dialect); + assertEquals(type, reparsed.getClass()); + assertEquals(expected, reparsed.toString()); + assertEquals(Set.of(features), statement.getFeatures().getCertain()); + assertTrue(statement.getFeatures().getUncertain().isEmpty()); + assertTrue(new TablesNamesFinder<>().getTables(statement).isEmpty()); + return statement; + } + + private static T roundTrip(Class type, String sql, Dialect dialect) + throws Exception { + return roundTrip(type, sql, sql, dialect, features(type)); + } + + /** DROP RESOURCE GROUP is a Drop, which the analysis reports like every other DROP. */ + private static StmtFeature[] features(Class type) { + if (type == SetResourceGroup.class) { + return new StmtFeature[] {StmtFeature.MODIFIES_SESSION}; + } + return type == Drop.class + ? new StmtFeature[] {StmtFeature.MODIFIES_SCHEMA, StmtFeature.MODIFIES_DATA} + : new StmtFeature[] {StmtFeature.MODIFIES_SCHEMA}; + } + + private static void assertRejected(String sql) { + for (Dialect dialect : MYSQL) { + assertThrows(JSQLParserException.class, () -> parse(sql, dialect), sql); + } + } + + // ---- CREATE RESOURCE GROUP ----------------------------------------------------------------- + + @Test + void createKeepsTypeVcpusPriorityAndState() throws Exception { + for (Dialect dialect : MYSQL) { + CreateResourceGroup group = roundTrip(CreateResourceGroup.class, + "CREATE RESOURCE GROUP rg1 TYPE = USER VCPU = 0-3, 5 THREAD_PRIORITY = 10 ENABLE", + dialect); + assertEquals("rg1", group.getName()); + assertEquals(CreateResourceGroup.Type.USER, group.getType()); + assertEquals(OptionAssignment.EQUALS, group.getTypeAssignment()); + assertEquals(List.of(new VcpuRange(0, 3), new VcpuRange(5)), group.getVcpus()); + assertTrue(group.getVcpus().get(0).isRange()); + assertEquals(3, group.getVcpus().get(0).getLast()); + assertFalse(group.getVcpus().get(1).isRange()); + assertNull(group.getVcpus().get(1).getEnd()); + assertEquals(5, group.getVcpus().get(1).getLast()); + assertEquals(OptionAssignment.EQUALS, group.getVcpuAssignment()); + assertEquals(10, group.getThreadPriority()); + assertEquals(OptionAssignment.EQUALS, group.getThreadPriorityAssignment()); + assertEquals(State.ENABLE, group.getState()); + + group = roundTrip(CreateResourceGroup.class, "CREATE RESOURCE GROUP rg2 TYPE = SYSTEM", + dialect); + assertEquals(CreateResourceGroup.Type.SYSTEM, group.getType()); + assertTrue(group.getVcpus().isEmpty()); + assertNull(group.getThreadPriority()); + assertNull(group.getState()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE RESOURCE GROUP rg3 TYPE = USER VCPU = 0 THREAD_PRIORITY = 19", + "CREATE RESOURCE GROUP rg4 TYPE = SYSTEM THREAD_PRIORITY = -20 DISABLE", + "CREATE RESOURCE GROUP rg6 TYPE USER VCPU 1, 3 THREAD_PRIORITY 5", + "CREATE RESOURCE GROUP rg7 TYPE := USER VCPU := 0 THREAD_PRIORITY := 1", + "CREATE RESOURCE GROUP rg11 TYPE = USER VCPU = 3-1", + "CREATE RESOURCE GROUP rg12 TYPE = USER VCPU = 2147483647", + "CREATE RESOURCE GROUP rg13 TYPE = USER THREAD_PRIORITY = 2147483647", + "CREATE RESOURCE GROUP rg14 TYPE = USER THREAD_PRIORITY = -2147483647", + "CREATE RESOURCE GROUP `rg 16` TYPE = USER", + "CREATE RESOURCE GROUP type TYPE = USER", + "CREATE RESOURCE GROUP vcpu TYPE = USER VCPU = 0", + "CREATE RESOURCE GROUP rg17 TYPE = USER DISABLE"}) + void createPrintsAsWritten(String sql) throws Exception { + for (Dialect dialect : MYSQL) { + roundTrip(CreateResourceGroup.class, sql, dialect); + } + } + + @Test + void optionAssignmentsAreKeptPerOption() throws Exception { + CreateResourceGroup group = roundTrip(CreateResourceGroup.class, + "CREATE RESOURCE GROUP rg TYPE USER VCPU := 1 THREAD_PRIORITY = -5", Dialect.MYSQL); + assertEquals(OptionAssignment.NONE, group.getTypeAssignment()); + assertEquals(OptionAssignment.COLON_EQUALS, group.getVcpuAssignment()); + assertEquals(OptionAssignment.EQUALS, group.getThreadPriorityAssignment()); + assertEquals(-5, group.getThreadPriority()); + assertEquals("", OptionAssignment.NONE.getSymbol()); + assertEquals("=", OptionAssignment.EQUALS.getSymbol()); + assertEquals(":=", OptionAssignment.COLON_EQUALS.getSymbol()); + } + + static Stream normalizedForms() { + return Stream.of( + // the commas of the VCPU and thread ID lists are optional + Arguments.of("CREATE RESOURCE GROUP rg5 TYPE = USER VCPU = 0-3,9-10 ENABLE", + "CREATE RESOURCE GROUP rg5 TYPE = USER VCPU = 0-3, 9-10 ENABLE"), + Arguments.of("CREATE RESOURCE GROUP rg8 TYPE = USER VCPU = 0 1 2-3", + "CREATE RESOURCE GROUP rg8 TYPE = USER VCPU = 0, 1, 2-3"), + Arguments.of("CREATE RESOURCE GROUP rg9 TYPE = USER VCPU = 0 - 1", + "CREATE RESOURCE GROUP rg9 TYPE = USER VCPU = 0-1"), + Arguments.of("CREATE RESOURCE GROUP rg10 TYPE = USER THREAD_PRIORITY = - 5", + "CREATE RESOURCE GROUP rg10 TYPE = USER THREAD_PRIORITY = -5"), + Arguments.of("CREATE RESOURCE GROUP rg15 TYPE = USER VCPU = 007", + "CREATE RESOURCE GROUP rg15 TYPE = USER VCPU = 7"), + Arguments.of( + "create resource group rg18 type = user vcpu = 0 thread_priority = 1 enable", + "CREATE RESOURCE GROUP rg18 TYPE = USER VCPU = 0 THREAD_PRIORITY = 1 ENABLE"), + Arguments.of("ALTER RESOURCE GROUP rg1 VCPU = 2,3 THREAD_PRIORITY = 10", + "ALTER RESOURCE GROUP rg1 VCPU = 2, 3 THREAD_PRIORITY = 10"), + Arguments.of("ALTER RESOURCE GROUP rg1 VCPU 0 1", + "ALTER RESOURCE GROUP rg1 VCPU 0, 1"), + Arguments.of("SET RESOURCE GROUP rg1 FOR 14 78 4", + "SET RESOURCE GROUP rg1 FOR 14, 78, 4"), + Arguments.of("set resource group rg1 for 14", "SET RESOURCE GROUP rg1 FOR 14")); + } + + @ParameterizedTest + @MethodSource("normalizedForms") + void optionalCommasAndSpacingAreNormalized(String sql, String expected) throws Exception { + for (Dialect dialect : MYSQL) { + Statement statement = parse(sql, dialect); + roundTrip(statement.getClass(), sql, expected, dialect, + features(statement.getClass())); + } + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE RESOURCE GROUP rg20", "CREATE RESOURCE GROUP rg20 TYPE = OTHER", + "CREATE RESOURCE GROUP rg20 VCPU = 0 TYPE = USER", + "CREATE RESOURCE GROUP rg20 TYPE = USER THREAD_PRIORITY = 1 VCPU = 0", + "CREATE RESOURCE GROUP rg20 TYPE = USER ENABLE THREAD_PRIORITY = 1", + "CREATE RESOURCE GROUP rg20 TYPE = USER ENABLE DISABLE", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 0-", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 0-1-2", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = -1", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 0,", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 0,,1", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 1.5", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 2147483648", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = 1_0", + "CREATE RESOURCE GROUP rg20 TYPE = USER VCPU = (0)", + "CREATE RESOURCE GROUP rg20 TYPE = USER THREAD_PRIORITY = +5", + "CREATE RESOURCE GROUP rg20 TYPE = USER THREAD_PRIORITY = 2147483648", + "CREATE RESOURCE GROUP rg20 TYPE = USER THREAD_PRIORITY = 1.5", + "CREATE RESOURCE GROUP rg20 TYPE = USER THREAD_PRIORITY", + "CREATE RESOURCE GROUP rg20 TYPE = USER FORCE", + "CREATE RESOURCE GROUP rg20 TYPE = = USER", + "CREATE RESOURCE GROUP IF NOT EXISTS rg20 TYPE = USER", + "CREATE RESOURCE GROUP d.rg20 TYPE = USER", "CREATE RESOURCE GROUP 'rg20' TYPE = USER", + "CREATE RESOURCE GROUP force TYPE = USER", "CREATE RESOURCE GROUP"}) + void createRejectsWhatMySqlRejects(String sql) { + assertRejected(sql); + } + + // ---- ALTER RESOURCE GROUP ------------------------------------------------------------------ + + @Test + void alterKeepsOptionsAndForce() throws Exception { + for (Dialect dialect : MYSQL) { + AlterResourceGroup group = roundTrip(AlterResourceGroup.class, + "ALTER RESOURCE GROUP rg1 VCPU = 0-1 THREAD_PRIORITY = 5 DISABLE FORCE", + dialect); + assertEquals("rg1", group.getName()); + assertEquals(List.of(new VcpuRange(0, 1)), group.getVcpus()); + assertEquals(5, group.getThreadPriority()); + assertEquals(State.DISABLE, group.getState()); + assertTrue(group.isForce()); + + group = roundTrip(AlterResourceGroup.class, "ALTER RESOURCE GROUP rg1", dialect); + assertTrue(group.getVcpus().isEmpty()); + assertNull(group.getThreadPriority()); + assertNull(group.getState()); + assertFalse(group.isForce()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER RESOURCE GROUP rg1 DISABLE FORCE", + "ALTER RESOURCE GROUP rg1 ENABLE", "ALTER RESOURCE GROUP rg1 ENABLE FORCE", + "ALTER RESOURCE GROUP rg1 FORCE", "ALTER RESOURCE GROUP rg1 THREAD_PRIORITY -3", + "ALTER RESOURCE GROUP rg1 VCPU := 0 THREAD_PRIORITY := 2 FORCE", + "ALTER RESOURCE GROUP `rg 16` VCPU = 0, 2-3"}) + void alterPrintsAsWritten(String sql) throws Exception { + for (Dialect dialect : MYSQL) { + roundTrip(AlterResourceGroup.class, sql, dialect); + } + } + + @ParameterizedTest + @ValueSource(strings = {"ALTER RESOURCE GROUP rg1 TYPE = USER", + "ALTER RESOURCE GROUP rg1 FORCE DISABLE", + "ALTER RESOURCE GROUP rg1 THREAD_PRIORITY = 5 VCPU = 0", + "ALTER RESOURCE GROUP rg1 DISABLE VCPU = 0", "ALTER RESOURCE GROUP rg1 FORCE FORCE", + "ALTER RESOURCE GROUP rg1, rg2 DISABLE", "ALTER RESOURCE GROUP", + "ALTER RESOURCE GROUP IF EXISTS rg1 DISABLE", "ALTER RESOURCE GROUP force DISABLE", + "ALTER RESOURCE GROUP rg1 VCPU = 2147483648"}) + void alterRejectsWhatMySqlRejects(String sql) { + assertRejected(sql); + } + + // ---- SET RESOURCE GROUP -------------------------------------------------------------------- + + @Test + void setNamesTheGroupAndTheThreads() throws Exception { + for (Dialect dialect : MYSQL) { + String sql = "SET RESOURCE GROUP rg1 FOR 14, 78, 4"; + SetResourceGroup set = roundTrip(SetResourceGroup.class, sql, dialect); + assertEquals("rg1", set.getName()); + assertEquals(3, set.getThreadIds().size()); + assertEquals(78L, assertInstanceOf(LongValue.class, set.getThreadIds().get(1)) + .getValue()); + assertSpans("78", sql, (ASTNodeAccess) set.getThreadIds().get(1)); + assertSpans("14, 78, 4", sql, set.getThreadIds()); + + set = roundTrip(SetResourceGroup.class, "SET RESOURCE GROUP rg1", dialect); + assertNull(set.getThreadIds()); + + sql = "SET RESOURCE GROUP `rg 16` FOR 0x1F, X'1f', 18446744073709551615"; + set = roundTrip(SetResourceGroup.class, sql, dialect); + assertEquals("`rg 16`", set.getName()); + assertEquals("0x1F", assertInstanceOf(HexValue.class, set.getThreadIds().get(0)) + .getValue()); + assertSpans("X'1f'", sql, (ASTNodeAccess) set.getThreadIds().get(1)); + assertEquals("18446744073709551615", + ((LongValue) set.getThreadIds().get(2)).getStringValue()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SET RESOURCE GROUP", "SET RESOURCE GROUP rg1 FOR", + "SET RESOURCE GROUP rg1 FOR -1", "SET RESOURCE GROUP rg1 FOR 1,", + "SET RESOURCE GROUP rg1 FOR 1.5", "SET RESOURCE GROUP rg1 FOR 18446744073709551616", + "SET RESOURCE GROUP rg1 FOR 0X1F", "SET RESOURCE GROUP rg1 FOR X'1'", + "SET RESOURCE GROUP rg1 FOR '14'", "SET RESOURCE GROUP rg1 FOR (14)", + "SET RESOURCE GROUP rg1 FOR @id", "SET RESOURCE GROUP rg1 FOR 1_0", + "SET RESOURCE GROUP rg1, rg2", "SET RESOURCE GROUP = rg1", + "SET RESOURCE GROUP rg1 FORCE", "SET RESOURCE GROUP force", + "SET GLOBAL RESOURCE GROUP rg1"}) + void setRejectsWhatMySqlRejects(String sql) { + assertRejected(sql); + } + + @Test + void otherSetFormsKeepTheirReading() throws Exception { + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, null, Dialect.POSTGRESQL}) { + for (String sql : List.of("SET resource = 1", "SET resource.grp = 1", + "SET @resource = 1", "SET autocommit = 0")) { + assertEquals(sql, assertInstanceOf(SetStatement.class, parse(sql, dialect)) + .toString()); + } + } + } + + // ---- DROP RESOURCE GROUP ------------------------------------------------------------------- + + @Test + void dropIsADropOfAResourceGroup() throws Exception { + for (Dialect dialect : MYSQL) { + Drop drop = roundTrip(Drop.class, "DROP RESOURCE GROUP rg1 FORCE", dialect); + assertEquals(Drop.ObjectType.RESOURCE_GROUP, drop.getObjectType()); + assertEquals("RESOURCE GROUP", drop.getType()); + assertEquals("rg1", drop.getName().getName()); + assertEquals(1, drop.getNames().size()); + assertTrue(drop.isForce()); + assertNull(drop.getParameters()); + + drop = roundTrip(Drop.class, "DROP RESOURCE GROUP rg2", dialect); + assertEquals(Drop.ObjectType.RESOURCE_GROUP, drop.getObjectType()); + assertFalse(drop.isForce()); + + drop = roundTrip(Drop.class, "DROP RESOURCE GROUP `rg 16`", dialect); + assertEquals("`rg 16`", drop.getName().getName()); + roundTrip(Drop.class, "drop resource group rg1 force", "DROP RESOURCE GROUP rg1 FORCE", + dialect, features(Drop.class)); + } + assertEquals("DROP RESOURCE GROUP rg1 FORCE", + new Drop().withObjectType(Drop.ObjectType.RESOURCE_GROUP) + .withName(new net.sf.jsqlparser.schema.Table("rg1")).withForce(true) + .toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"DROP RESOURCE GROUP IF EXISTS rg3", "DROP RESOURCE GROUP rg3, rg4", + "DROP RESOURCE GROUP rg3 FORCE FORCE", "DROP RESOURCE GROUP rg3 CASCADE", + "DROP RESOURCE GROUP rg3 RESTRICT", "DROP RESOURCE GROUP d.rg3", + "DROP RESOURCE GROUP 'rg3'", "DROP RESOURCE GROUP rg3 extra", + "DROP RESOURCE GROUP rg3 ON t", "DROP RESOURCE GROUP FORCE", + "DROP TEMPORARY RESOURCE GROUP rg3", "DROP MATERIALIZED RESOURCE GROUP rg3"}) + void dropRejectsWhatMySqlRejects(String sql) { + assertRejected(sql); + } + + @Test + void dropWithoutANameIsRejectedByMySql() throws Exception { + assertThrows(JSQLParserException.class, () -> parse("DROP RESOURCE GROUP", Dialect.MYSQL)); + // without a dialect it keeps dropping an object called GROUP + Drop drop = assertInstanceOf(Drop.class, parse("DROP RESOURCE GROUP", null)); + assertEquals(Drop.ObjectType.OTHER, drop.getObjectType()); + assertEquals("RESOURCE", drop.getType()); + } + + // ---- other dialects ------------------------------------------------------------------------ + + @Test + void otherDialectsKeepTheirReading() throws Exception { + for (Dialect dialect : OTHERS) { + Drop drop = assertInstanceOf(Drop.class, parse("DROP RESOURCE GROUP rg1", dialect)); + assertEquals(Drop.ObjectType.OTHER, drop.getObjectType()); + assertEquals("RESOURCE", drop.getType()); + assertEquals("GROUP", drop.getName().getName()); + assertEquals(List.of("rg1"), drop.getParameters()); + assertThrows(JSQLParserException.class, + () -> parse("DROP RESOURCE GROUP rg1 FORCE", dialect)); + assertThrows(JSQLParserException.class, + () -> parse("SET RESOURCE GROUP rg1", dialect)); + assertThrows(JSQLParserException.class, + () -> parse("SET RESOURCE GROUP rg1 FOR 14", dialect)); + assertInstanceOf(UnsupportedStatement.class, + parse("CREATE RESOURCE GROUP rg1 TYPE = USER", dialect)); + assertInstanceOf(UnsupportedStatement.class, + parse("ALTER RESOURCE GROUP rg1 DISABLE FORCE", dialect)); + } + } + + @Test + void theWordsRemainIdentifiers() throws Exception { + for (Dialect dialect : MYSQL) { + for (String sql : List.of("SELECT resource, vcpu, thread_priority FROM resource", + "CREATE TABLE resource (vcpu INT, thread_priority INT)", + "ALTER TABLE resource ADD COLUMN vcpu INT", "DROP TABLE resource", + "INSERT INTO resource (vcpu) VALUES (1)")) { + assertEquals(sql, parse(sql, dialect).toString()); + } + } + } + + // ---- statement boundaries, visitors and validation ----------------------------------------- + + static Stream statements() { + return Stream.of( + Arguments.of("CREATE RESOURCE GROUP rg1 TYPE = USER VCPU = 0-3, 5 ENABLE", + CreateResourceGroup.class), + Arguments.of("CREATE RESOURCE GROUP rg1 TYPE = SYSTEM", CreateResourceGroup.class), + Arguments.of("ALTER RESOURCE GROUP rg1 THREAD_PRIORITY = 5 DISABLE FORCE", + AlterResourceGroup.class), + Arguments.of("ALTER RESOURCE GROUP rg1", AlterResourceGroup.class), + Arguments.of("SET RESOURCE GROUP rg1 FOR 14, 78", SetResourceGroup.class), + Arguments.of("SET RESOURCE GROUP rg1", SetResourceGroup.class), + Arguments.of("DROP RESOURCE GROUP rg1 FORCE", Drop.class), + Arguments.of("DROP RESOURCE GROUP rg1", Drop.class)); + } + + @ParameterizedTest + @MethodSource("statements") + void statementsEndAtTheStatementBoundary(String sql, Class type) + throws Exception { + // the statement on its own line is closed by the next statement start + String script = "SELECT 1;\nCOMMIT\n" + sql + "\nSELECT 2;" + sql + ";\nSAVEPOINT sp"; + for (Dialect dialect : MYSQL) { + for (boolean opaque : new boolean[] {false, true}) { + Statements statements = CCJSqlParserUtil.parseStatements(script, p -> { + if (dialect != null) { + p.withDialect(dialect); + } + p.withUnsupportedStatements(opaque); + }); + assertEquals(6, statements.size(), script); + assertEquals("COMMIT", statements.get(1).toString()); + assertInstanceOf(type, statements.get(2)); + assertEquals(sql, statements.get(2).toString()); + assertSpans(sql, script, (ASTNodeAccess) statements.get(2)); + assertEquals("SELECT 2", statements.get(3).toString()); + assertEquals(sql, statements.get(4).toString()); + assertEquals("SAVEPOINT sp", statements.get(5).toString()); + } + } + } + + @Test + void forceInstallStartsTheNextStatement() throws Exception { + Statements statements = CCJSqlParserUtil.parseStatements( + "SELECT 1; ALTER RESOURCE GROUP rg1 DISABLE\nFORCE INSTALL httpfs"); + assertEquals(3, statements.size()); + assertFalse(((AlterResourceGroup) statements.get(1)).isForce()); + } + + @Test + void scriptsKeepEachStatement() throws Exception { + String script = "CREATE RESOURCE GROUP rg1 TYPE = USER VCPU = 0-3 THREAD_PRIORITY = 10;" + + " SET RESOURCE GROUP rg1; INSERT INTO t VALUES (1);" + + " ALTER RESOURCE GROUP rg1 DISABLE FORCE; DROP RESOURCE GROUP rg1 FORCE"; + Statements statements = + CCJSqlParserUtil.parseStatements(script, p -> p.withDialect(Dialect.MYSQL)); + assertEquals(5, statements.size()); + assertEquals(Set.of("t"), new TablesNamesFinder<>().getTables(statements.get(2))); + assertInstanceOf(AlterResourceGroup.class, statements.get(3)); + assertEquals(statements.toString(), CCJSqlParserUtil + .parseStatements(statements.toString(), p -> p.withDialect(Dialect.MYSQL)) + .toString()); + } + + @Test + void visitorDispatchPassesContextAndResult() throws Exception { + StatementVisitorAdapter visitor = new StatementVisitorAdapter<>() { + @Override + public String visit(CreateResourceGroup statement, S context) { + return context + ":create:" + statement.getType(); + } + + @Override + public String visit(AlterResourceGroup statement, S context) { + return context + ":alter:" + statement.getState(); + } + + @Override + public String visit(SetResourceGroup statement, S context) { + return context + ":set:" + statement.getThreadIds(); + } + }; + String create = "CREATE RESOURCE GROUP rg TYPE = SYSTEM"; + String alter = "ALTER RESOURCE GROUP rg ENABLE"; + String set = "SET RESOURCE GROUP rg FOR 1, 2"; + assertEquals("ctx:create:SYSTEM", parse(create, null).accept(visitor, "ctx")); + assertEquals("ctx:alter:ENABLE", parse(alter, null).accept(visitor, "ctx")); + assertEquals("ctx:set:1, 2", parse(set, null).accept(visitor, "ctx")); + for (String sql : List.of(create, alter, set)) { + assertNull(parse(sql, null).accept(new StatementVisitorAdapter<>(), null)); + Statement statement = parse(sql, null); + statement.accept(new StatementVisitorAdapter()); + } + } + + private static boolean isValid(Dialect dialect, String sql, + ValidationCapability... capabilities) { + FeatureConfiguration configuration = new FeatureConfiguration(); + if (dialect != null) { + configuration.setValue(Feature.dialect, dialect.name()); + } + return new Validation(configuration, Arrays.asList(capabilities), sql).validate() + .isEmpty(); + } + + @Test + void validationKnowsMySqlTakesResourceGroups() { + String script = "CREATE RESOURCE GROUP rg TYPE = USER; ALTER RESOURCE GROUP rg DISABLE;" + + " SET RESOURCE GROUP rg; DROP RESOURCE GROUP rg FORCE"; + assertTrue(isValid(Dialect.MYSQL, script, MySqlVersion.V8_0)); + assertTrue(isValid(null, script, MySqlVersion.V8_0)); + assertTrue(isValid(null, script, new FeaturesAllowed(Feature.createResourceGroup, + Feature.alterResourceGroup, Feature.setResourceGroup, Feature.drop))); + for (String sql : script.split("; ")) { + if (!sql.startsWith("DROP")) { + assertFalse(isValid(null, sql, MariaDbVersion.V10_5_4), sql); + assertFalse(isValid(null, sql, PostgresqlVersion.V14), sql); + } + } + assertFalse(isValid(null, "SET RESOURCE GROUP rg", new FeaturesAllowed(Feature.set))); + assertFalse(isValid(null, "CREATE RESOURCE GROUP rg TYPE = USER", + new FeaturesAllowed(Feature.alterResourceGroup))); + assertFalse(isValid(null, "ALTER RESOURCE GROUP rg ENABLE", + new FeaturesAllowed(Feature.createResourceGroup))); + } + + // ---- the model ----------------------------------------------------------------------------- + + @Test + void statementsAreBuiltWithoutTheParser() { + CreateResourceGroup create = new CreateResourceGroup("rg", CreateResourceGroup.Type.USER) + .withTypeAssignment(OptionAssignment.NONE) + .withVcpus(new java.util.ArrayList<>(List.of(new VcpuRange(0, 3)))) + .addVcpuRanges(List.of(new VcpuRange(5))).withThreadPriority(-3) + .withState(State.DISABLE); + create.setVcpuAssignment(OptionAssignment.NONE); + create.setThreadPriorityAssignment(OptionAssignment.COLON_EQUALS); + assertEquals("CREATE RESOURCE GROUP rg TYPE USER VCPU 0-3, 5 THREAD_PRIORITY := -3 DISABLE", + create.toString()); + create.setVcpus(null); + create.addVcpus(new VcpuRange(1)); + assertEquals(List.of(new VcpuRange(1)), create.getVcpus()); + + AlterResourceGroup alter = new AlterResourceGroup("rg") + .withVcpus(new java.util.ArrayList<>()).addVcpuRanges(List.of(new VcpuRange(2))) + .withThreadPriority(1).withState(State.ENABLE).withForce(true); + assertEquals("ALTER RESOURCE GROUP rg VCPU = 2 THREAD_PRIORITY = 1 ENABLE FORCE", + alter.toString()); + + SetResourceGroup set = new SetResourceGroup("rg").addThreadIds(new LongValue(1)) + .addThreadIds(new HexValue("0x1F")); + assertEquals("SET RESOURCE GROUP rg FOR 1, 0x1F", set.toString()); + set.withThreadIds(new ExpressionList<>(new LongValue(7), new LongValue(8))); + assertEquals("SET RESOURCE GROUP rg FOR 7, 8", set.toString()); + set.setThreadIds(new ExpressionList<>()); + assertEquals("SET RESOURCE GROUP rg", set.toString()); + set.addThreadIds(new LongValue(1), new HexValue("0x1F")); + assertEquals("SET RESOURCE GROUP rg FOR 1, 0x1F", set.toString()); + set.setThreadIds(null); + assertEquals("SET RESOURCE GROUP rg", set.toString()); + + assertThrows(NullPointerException.class, () -> new CreateResourceGroup("rg", null)); + assertThrows(NullPointerException.class, () -> new AlterResourceGroup(null)); + assertThrows(NullPointerException.class, () -> new SetResourceGroup(null)); + } + + @Test + void vcpuRangesCompareByTheirNumbers() { + VcpuRange range = new VcpuRange(0, 3); + assertEquals("0-3", range.toString()); + assertEquals(range, range); + assertEquals(new VcpuRange(0, 3), range); + assertEquals(new VcpuRange(0, 3).hashCode(), range.hashCode()); + assertNotEquals(new VcpuRange(0), range); + assertNotEquals(new VcpuRange(1, 3), range); + assertNotEquals(new VcpuRange(0, 0), new VcpuRange(0)); + assertNotEquals("0-3", range); + range.setStart(2); + range.setEnd(null); + assertEquals("2", range.toString()); + assertThrows(IllegalArgumentException.class, () -> new VcpuRange(-1)); + assertThrows(IllegalArgumentException.class, () -> new VcpuRange(0, -1)); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/StatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/StatementValidatorTest.java index a9967d6c5..f32eaf464 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/StatementValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/StatementValidatorTest.java @@ -52,6 +52,9 @@ static Stream statementsOutsideSelect() { Arguments.of("ROLLBACK", Feature.rollback), Arguments.of("PREPARE TRANSACTION 'x'", Feature.twoPhaseCommit), Arguments.of("XA RECOVER", Feature.xa), + Arguments.of("CREATE RESOURCE GROUP rg TYPE = USER", Feature.createResourceGroup), + Arguments.of("ALTER RESOURCE GROUP rg DISABLE", Feature.alterResourceGroup), + Arguments.of("SET RESOURCE GROUP rg", Feature.setResourceGroup), Arguments.of("LOCK TABLE t IN EXCLUSIVE MODE", Feature.lock), Arguments.of("ALTER SESSION SET CURRENT_SCHEMA = other", Feature.alterSession), Arguments.of("ALTER SYSTEM SET archive_command = 'cp %p /tmp/%f'",