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'",