diff --git a/src/main/java/net/sf/jsqlparser/statement/create/user/RoleSelection.java b/src/main/java/net/sf/jsqlparser/statement/create/user/RoleSelection.java new file mode 100644 index 0000000000..eb78089c30 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/user/RoleSelection.java @@ -0,0 +1,100 @@ +/*- + * #%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.create.user; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The roles named by MySQL {@code SET ROLE}, {@code SET DEFAULT ROLE}, the {@code DEFAULT ROLE} + * clause of {@code CREATE USER} and {@code ALTER USER}, and {@code GRANT ... AS user WITH ROLE}. + * Role names use the account syntax, so each role is a {@link UserAccount} without authentication. + */ +public class RoleSelection implements Serializable { + + public enum Kind { + /** + * {@code DEFAULT}: the account's default roles, only in {@code SET ROLE} and + * {@code WITH ROLE}. + */ + DEFAULT, + /** {@code NONE}. */ + NONE, + /** {@code ALL}. */ + ALL, + /** {@code ALL EXCEPT role [, role] ...}, only in {@code SET ROLE} and {@code WITH ROLE}. */ + ALL_EXCEPT, + /** {@code role [, role] ...}. */ + ROLES + } + + private Kind kind; + private final List roles = new ArrayList<>(); + + public RoleSelection() { + // empty constructor + } + + public RoleSelection(Kind kind) { + this.kind = kind; + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + /** Returns the listed roles of {@link Kind#ROLES} and {@link Kind#ALL_EXCEPT}. */ + public List getRoles() { + return Collections.unmodifiableList(roles); + } + + public void setRoles(Collection roles) { + this.roles.clear(); + if (roles != null) { + this.roles.addAll(roles); + } + } + + @Override + public String toString() { + String names = roles.stream().map(Object::toString).collect(Collectors.joining(", ")); + switch (kind) { + case ROLES: + return names; + case ALL_EXCEPT: + return "ALL EXCEPT " + names; + default: + return kind.name(); + } + } + + public RoleSelection withKind(Kind kind) { + setKind(kind); + return this; + } + + public RoleSelection withRoles(Collection roles) { + setRoles(roles); + return this; + } + + public RoleSelection addRoles(UserAccount... roles) { + Collections.addAll(this.roles, roles); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Grant.java b/src/main/java/net/sf/jsqlparser/statement/grant/Grant.java index a9c43dc267..3962e967d6 100644 --- a/src/main/java/net/sf/jsqlparser/statement/grant/Grant.java +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Grant.java @@ -9,22 +9,23 @@ */ package net.sf.jsqlparser.statement.grant; -import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.user.RoleSelection; public class Grant implements Statement { private PrivilegeClause clause = new PrivilegeClause(); - private GrantOption option; + private final List options = new ArrayList<>(); + private Grantee asUser; + private RoleSelection withRole; public PrivilegeClause getClause() { return clause; @@ -34,12 +35,46 @@ public void setClause(PrivilegeClause clause) { this.clause = clause; } + /** + * The {@code WITH} options as a mutable list: {@code GRANT OPTION}, {@code ADMIN OPTION} or + * PostgreSQL membership options such as {@code INHERIT TRUE, SET FALSE}. + */ + public List getOptions() { + return options; + } + + public void setOptions(Collection options) { + List copy = options == null ? new ArrayList<>() : new ArrayList<>(options); + this.options.clear(); + this.options.addAll(copy); + } + + /** The first {@code WITH} option, or null; see {@link #getOptions()}. */ public GrantOption getOption() { - return option; + return options.isEmpty() ? null : options.get(0); } + /** Replaces all {@code WITH} options with the given option, or removes them for null. */ public void setOption(GrantOption option) { - this.option = option; + setOptions(option == null ? null : Collections.singletonList(option)); + } + + /** The MySQL {@code AS user} account, or null when omitted. */ + public Grantee getAsUser() { + return asUser; + } + + public void setAsUser(Grantee asUser) { + this.asUser = asUser; + } + + /** The MySQL {@code WITH ROLE} selection of the {@code AS user} clause, or null. */ + public RoleSelection getWithRole() { + return withRole; + } + + public void setWithRole(RoleSelection withRole) { + this.withRole = withRole; } public List getPrivilegeItems() { @@ -50,6 +85,7 @@ public void setPrivilegeItems(List privileges) { clause.setPrivileges(privileges); } + /** The target, or null for a role membership. */ public PrivilegeTarget getTarget() { return clause.getTarget(); } @@ -67,14 +103,16 @@ public void setRoles(List roles) { } public String getRole() { - return clause.getRoles().isEmpty() ? null : clause.getRoles().get(0); + return clause.getRoleItems().isEmpty() ? null : clause.getRoles().get(0); } + /** Makes this a membership of the given role, which has neither privileges nor a target. */ public void setRole(String role) { clause.setRoles(new ArrayList<>()); if (role != null) { clause.getRoles().add(role); clause.setPrivileges(null); + clause.setTarget(null); } } @@ -83,32 +121,7 @@ public List getPrivileges() { if (clause.getPrivileges() == null) { return null; } - return new AbstractList() { - @Override - public int size() { - return clause.getPrivileges().size(); - } - - @Override - public String get(int index) { - return clause.getPrivileges().get(index).toString(); - } - - @Override - public String set(int index, String value) { - return clause.getPrivileges().set(index, new Privilege(value)).toString(); - } - - @Override - public void add(int index, String value) { - clause.getPrivileges().add(index, new Privilege(value)); - } - - @Override - public String remove(int index) { - return clause.getPrivileges().remove(index).toString(); - } - }; + return new StringListView<>(() -> clause.getPrivileges(), Privilege::new); } public void setPrivileges(List privileges) { @@ -122,12 +135,22 @@ public void setPrivileges(List privileges) { clause.setPrivileges(items); } + /** + * The first named target as dotted text. MySQL wildcard levels keep their wildcard, for example + * {@code db.*} or {@code *.*}. + */ public String getObjectName() { - if (clause.getTarget().getNames().isEmpty() - || clause.getTarget().getNames().get(0).isEmpty()) { + PrivilegeTarget target = clause.getTarget(); + if (target == null) { return null; } - List parts = clause.getTarget().getNames().get(0); + if (target.getWildcard() != null) { + return target.getTargetText(); + } + if (target.getNames().isEmpty() || target.getNames().get(0).isEmpty()) { + return null; + } + List parts = target.getNames().get(0); return parts.stream().map(part -> part == null ? "" : part) .collect(Collectors.joining(".")); } @@ -136,14 +159,19 @@ public void setObjectName(String objectName) { setObjectName(Collections.singletonList(objectName)); } + /** Names the first target, replacing a MySQL wildcard level. */ public void setObjectName(List objectName) { List parts = new ArrayList<>(objectName); getObjectNameParts().clear(); getObjectNameParts().addAll(parts); + clause.getTarget().setWildcard(null); } /** Mutable parts of the first named target, retained for compatibility. */ public List getObjectNameParts() { + if (clause.getTarget() == null) { + clause.setTarget(new PrivilegeTarget()); + } if (clause.getTarget().getNames().isEmpty()) { clause.getTarget().getNames().add(new ArrayList<>()); } @@ -161,11 +189,18 @@ public void setUsers(List users) { public void appendTo(StringBuilder sql, Consumer visitor) { sql.append("GRANT "); clause.appendTo(sql, false, visitor); - if (option != null) { - sql.append(" WITH ").append(option); + if (!options.isEmpty()) { + sql.append(" WITH ").append( + options.stream().map(Object::toString).collect(Collectors.joining(", "))); + } + if (asUser != null) { + sql.append(" AS ").append(asUser); + if (withRole != null) { + sql.append(" WITH ROLE ").append(withRole); + } } - if (clause.getGrantedBy() != null) { - sql.append(" GRANTED BY ").append(clause.getGrantedBy()); + if (clause.getGrantor() != null) { + sql.append(" GRANTED BY ").append(clause.getGrantor()); } } @@ -222,14 +257,17 @@ public Grant addPrivileges(Collection privileges) { } public Grant addUsers(String... users) { - List collection = Optional.ofNullable(getUsers()).orElseGet(ArrayList::new); - Collections.addAll(collection, users); - return this.withUsers(collection); + return addUsers(Arrays.asList(users)); } public Grant addUsers(Collection users) { - List collection = Optional.ofNullable(getUsers()).orElseGet(ArrayList::new); - collection.addAll(users); - return this.withUsers(collection); + List additions = new ArrayList<>(users); + if (clause.getGranteeItems() == null) { + clause.setGranteeItems(new ArrayList<>()); + } + for (String user : additions) { + clause.getGranteeItems().add(Grantee.valueOf(user)); + } + return this; } } diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Grantee.java b/src/main/java/net/sf/jsqlparser/statement/grant/Grantee.java new file mode 100644 index 0000000000..a53b020640 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Grantee.java @@ -0,0 +1,125 @@ +/*- + * #%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.grant; + +import java.io.Serializable; +import java.util.Locale; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.create.user.UserAccount; + +/** + * A role specification in GRANT and REVOKE: a grantee, the PostgreSQL grantor, the MySQL {@code AS} + * user or the MySQL proxied account. + * + *

+ * Named users and roles are held as a {@link UserAccount}: a MySQL account {@code user[@host]}, or + * a PostgreSQL role name without host. + */ +public class Grantee implements Serializable { + + public enum Kind { + /** A named user or role, see {@link Grantee#getAccount()}. */ + ACCOUNT, + /** PostgreSQL {@code GROUP name}, see {@link Grantee#getAccount()}. */ + GROUP, PUBLIC, CURRENT_USER, CURRENT_ROLE, SESSION_USER + } + + private Kind kind; + private UserAccount account; + private boolean currentUserParentheses; + + public Grantee(Kind kind) { + this.kind = kind; + } + + public Grantee(UserAccount account) { + this.kind = Kind.ACCOUNT; + this.account = account; + } + + /** + * Converts the text of the string-based API. Special role names are recognized; any other text + * becomes an account whose user name is kept verbatim. + */ + public static Grantee valueOf(String text) { + String upper = text.trim().toUpperCase(Locale.ROOT); + if (upper.startsWith("GROUP ")) { + return new Grantee(Kind.GROUP) + .withAccount(accountOf(text.trim().substring("GROUP ".length()).trim())); + } + if ("CURRENT_USER()".equals(upper.replace(" ", ""))) { + return new Grantee(Kind.CURRENT_USER).withCurrentUserParentheses(true); + } + for (Kind special : new Kind[] {Kind.PUBLIC, Kind.CURRENT_USER, Kind.CURRENT_ROLE, + Kind.SESSION_USER}) { + if (special.name().equals(upper)) { + return new Grantee(special); + } + } + return new Grantee(accountOf(text)); + } + + /** An account or role name kept as written, without a host. */ + static UserAccount accountOf(String name) { + StringValue user = new StringValue(); + user.setValue(name); + return new UserAccount().withUser(user.setQuoteStr("")); + } + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + /** The named user or role of an {@link Kind#ACCOUNT} or {@link Kind#GROUP} grantee. */ + public UserAccount getAccount() { + return account; + } + + public void setAccount(UserAccount account) { + this.account = account; + } + + /** Whether MySQL {@code CURRENT_USER()} was written with parentheses. */ + public boolean isCurrentUserParentheses() { + return currentUserParentheses; + } + + public void setCurrentUserParentheses(boolean currentUserParentheses) { + this.currentUserParentheses = currentUserParentheses; + } + + public Grantee withAccount(UserAccount account) { + setAccount(account); + return this; + } + + public Grantee withCurrentUserParentheses(boolean currentUserParentheses) { + setCurrentUserParentheses(currentUserParentheses); + return this; + } + + @Override + public String toString() { + switch (kind) { + case ACCOUNT: + return String.valueOf(account); + case GROUP: + return "GROUP " + account; + case CURRENT_USER: + return currentUserParentheses ? "CURRENT_USER()" : "CURRENT_USER"; + default: + return kind.name(); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java b/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java index b1f3b461aa..116ae93df9 100644 --- a/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java @@ -15,13 +15,23 @@ import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.schema.Column; +/** + * A privilege of GRANT and REVOKE. Static privileges are {@link Kind} constants, rendered with + * spaces for underscores; MySQL dynamic privileges such as {@code BACKUP_ADMIN} are + * {@link Kind#DYNAMIC} with {@link #getDynamicName()}. + */ public class Privilege implements Serializable { public enum Kind { - ALL, SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, MAINTAIN, USAGE, CREATE, CONNECT, TEMPORARY, TEMP, EXECUTE, SET, ALTER_SYSTEM, ALTER, DROP + ALL, SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, MAINTAIN, USAGE, CREATE, CONNECT, TEMPORARY, TEMP, EXECUTE, SET, ALTER_SYSTEM, ALTER, DROP, + // MySQL static privileges + ALTER_ROUTINE, CREATE_ROLE, CREATE_ROUTINE, CREATE_TABLESPACE, CREATE_TEMPORARY_TABLES, CREATE_USER, CREATE_VIEW, DROP_ROLE, EVENT, FILE, GRANT_OPTION, INDEX, LOCK_TABLES, PROCESS, PROXY, RELOAD, REPLICATION_CLIENT, REPLICATION_SLAVE, SHOW_DATABASES, SHOW_VIEW, SHUTDOWN, SUPER, + /** A MySQL dynamic privilege, see {@link Privilege#getDynamicName()}. */ + DYNAMIC } private String legacyName; private Kind kind; + private String dynamicName; private boolean usePrivileges; private ExpressionList columns; @@ -49,21 +59,56 @@ public void setColumns(ExpressionList columns) { this.columns = columns; } + /** The identifier of a {@link Kind#DYNAMIC} privilege, as written. */ + public String getDynamicName() { + return dynamicName; + } + + public void setDynamicName(String dynamicName) { + this.dynamicName = dynamicName; + } + + /** The privilege name as rendered, for example {@code CREATE VIEW} or {@code BACKUP_ADMIN}. */ + public String getName() { + if (kind == null) { + return legacyName; + } + return kind == Kind.DYNAMIC ? dynamicName : kind.name().replace('_', ' '); + } + public Privilege(Kind kind) { this.kind = kind; } - /** Preserves values supplied through the pre-existing string-based Grant API. */ + /** + * Preserves values supplied through the pre-existing string-based Grant API. Names of static + * privileges, such as {@code CREATE VIEW}, become typed; other text is kept as written. + */ public Privilege(String name) { - try { - this.kind = Kind.valueOf(name); - } catch (IllegalArgumentException ex) { + this.kind = staticKind(name); + if (kind == null) { this.legacyName = name; } } + private static Kind staticKind(String name) { + String constant = name.replace(' ', '_'); + for (Kind candidate : Kind.values()) { + if (candidate != Kind.DYNAMIC && candidate.name().equals(constant)) { + return candidate; + } + } + return null; + } + + public static Privilege dynamic(String name) { + Privilege privilege = new Privilege(Kind.DYNAMIC); + privilege.setDynamicName(name); + return privilege; + } + public void appendTo(StringBuilder sql, Consumer visitor) { - sql.append(kind == null ? legacyName : kind.name().replace('_', ' ')); + sql.append(getName()); if (usePrivileges) { sql.append(" PRIVILEGES"); } diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java index 4f517d1229..49f2ddb77f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java +++ b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java @@ -11,17 +11,29 @@ import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.user.UserAccount; -/** Shared, editable GRANT/REVOKE payload. Null privileges denote role membership. */ +/** + * Shared, editable GRANT/REVOKE payload. Null privileges denote role membership; role memberships, + * and MySQL {@code REVOKE ALL PRIVILEGES, GRANT OPTION}, have no target. + * + *

+ * Roles, grantees and the grantor are typed. The string accessors {@link #getRoles()}, + * {@link #getGrantees()} and {@link #getGrantedBy()} are derived from them and convert assigned + * text with {@link Grantee#valueOf(String)}. + */ public class PrivilegeClause implements Serializable { private List privileges; - private List grantees; - private String grantedBy; - private List roles = new ArrayList<>(); + private List granteeItems; + private Grantee grantor; + private List roleItems = new ArrayList<>(); private PrivilegeTarget target = new PrivilegeTarget(); public List getPrivileges() { @@ -32,12 +44,25 @@ public void setPrivileges(List privileges) { this.privileges = privileges; } + /** The granted or revoked roles of a role membership, as a mutable list. */ + public List getRoleItems() { + return roleItems; + } + + public void setRoleItems(Collection roleItems) { + this.roleItems = roleItems == null ? new ArrayList<>() : new ArrayList<>(roleItems); + } + + /** Mutable legacy string view of {@link #getRoleItems()}. */ public List getRoles() { - return roles; + return new StringListView<>(() -> roleItems, Grantee::accountOf); } public void setRoles(List roles) { - this.roles = roles; + roleItems = convert(roles, Grantee::accountOf); + if (roleItems == null) { + roleItems = new ArrayList<>(); + } } public PrivilegeTarget getTarget() { @@ -48,25 +73,56 @@ public void setTarget(PrivilegeTarget target) { this.target = target; } + /** The grantees or revokees as a mutable list, or null when unset. */ + public List getGranteeItems() { + return granteeItems; + } + + public void setGranteeItems(Collection granteeItems) { + this.granteeItems = granteeItems == null ? null : new ArrayList<>(granteeItems); + } + + /** Mutable legacy string view of {@link #getGranteeItems()}, or null when unset. */ public List getGrantees() { - return grantees; + return granteeItems == null ? null + : new StringListView<>(() -> granteeItems, Grantee::valueOf); } public void setGrantees(List grantees) { - this.grantees = grantees; + granteeItems = convert(grantees, Grantee::valueOf); + } + + /** The PostgreSQL {@code GRANTED BY} role, or null when omitted. */ + public Grantee getGrantor() { + return grantor; + } + + public void setGrantor(Grantee grantor) { + this.grantor = grantor; } public String getGrantedBy() { - return grantedBy; + return grantor == null ? null : grantor.toString(); } public void setGrantedBy(String grantedBy) { - this.grantedBy = grantedBy; + grantor = grantedBy == null ? null : Grantee.valueOf(grantedBy); + } + + private static List convert(List values, Function parse) { + if (values == null) { + return null; + } + List items = new ArrayList<>(); + for (String value : new ArrayList<>(values)) { + items.add(parse.apply(value)); + } + return items; } public void appendTo(StringBuilder sql, boolean revoke, Consumer visitor) { if (privileges == null) { - sql.append(String.join(", ", roles)); + sql.append(join(roleItems)); } else { for (int i = 0; i < privileges.size(); i++) { if (i > 0) { @@ -74,14 +130,22 @@ public void appendTo(StringBuilder sql, boolean revoke, Consumer vis } privileges.get(i).appendTo(sql, visitor); } - sql.append(" ON ").append(target); + if (target != null) { + sql.append(" ON ").append(target); + } } - sql.append(revoke ? " FROM " : " TO ").append(String.join(", ", grantees)); + sql.append(revoke ? " FROM " : " TO ").append(join(granteeItems)); + } + + private static String join(List items) { + return items.stream().map(Object::toString).collect(Collectors.joining(", ")); } public void visit(Consumer tables, Consumer expressions) { if (privileges != null) { - target.visitTables(tables); + if (target != null) { + target.visitTables(tables); + } for (Privilege privilege : privileges) { if (privilege.getColumns() != null) { privilege.getColumns().forEach(expressions); diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java index a92ac85391..7b815c072f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java +++ b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java @@ -17,16 +17,40 @@ import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.RoutineReference; -/** Targets have distinct kinds; a routine signature is not a function invocation. */ +/** + * Targets have distinct kinds; a routine signature is not a function invocation. MySQL privilege + * levels written with {@code *} are a {@link Wildcard}, and the account of MySQL + * {@code PROXY ON user} is an {@link Kind#ACCOUNT} target. + */ public class PrivilegeTarget implements Serializable { public enum Kind { - TABLE, SEQUENCE, DATABASE, DOMAIN, FOREIGN_DATA_WRAPPER, FOREIGN_SERVER, FUNCTION, PROCEDURE, ROUTINE, LANGUAGE, LARGE_OBJECT, PARAMETER, SCHEMA, TABLESPACE, TYPE + TABLE, SEQUENCE, DATABASE, DOMAIN, FOREIGN_DATA_WRAPPER, FOREIGN_SERVER, FUNCTION, PROCEDURE, ROUTINE, LANGUAGE, LARGE_OBJECT, PARAMETER, SCHEMA, TABLESPACE, TYPE, + /** + * The proxied account of MySQL {@code PROXY ON user}, see + * {@link PrivilegeTarget#getAccount()}. + */ + ACCOUNT + } + + /** MySQL privilege levels written with {@code *}. */ + public enum Wildcard { + /** {@code *}: the default database. */ + DEFAULT_DATABASE, + /** {@code *.*}: all databases. */ + GLOBAL, + /** + * {@code db.*}: the database named by the single entry of + * {@link PrivilegeTarget#getNames()}. + */ + DATABASE } private Kind kind = Kind.TABLE; private boolean explicitKind; private boolean allInSchema; private boolean defaultPrivileges; + private Wildcard wildcard; + private Grantee account; private List> names = new ArrayList<>(); private List routines = new ArrayList<>(); @@ -62,6 +86,24 @@ public void setDefaultPrivileges(boolean defaultPrivileges) { this.defaultPrivileges = defaultPrivileges; } + /** The MySQL wildcard privilege level, or null for named targets. */ + public Wildcard getWildcard() { + return wildcard; + } + + public void setWildcard(Wildcard wildcard) { + this.wildcard = wildcard; + } + + /** The proxied account of an {@link Kind#ACCOUNT} target. */ + public Grantee getAccount() { + return account; + } + + public void setAccount(Grantee account) { + this.account = account; + } + public List> getNames() { return names; } @@ -79,7 +121,7 @@ public void setRoutines(List routines) { } public void visitTables(Consumer
visitor) { - if (kind == Kind.TABLE && !allInSchema && !defaultPrivileges) { + if (kind == Kind.TABLE && wildcard == null && !allInSchema && !defaultPrivileges) { for (List name : names) { if (!name.isEmpty()) { visitor.accept(new Table(name)); @@ -88,19 +130,37 @@ public void visitTables(Consumer
visitor) { } } + /** The target names as written after the optional kind, for example {@code db.*}. */ + String getTargetText() { + if (kind == Kind.ACCOUNT) { + return String.valueOf(account); + } + if (wildcard == Wildcard.DEFAULT_DATABASE) { + return "*"; + } + if (wildcard == Wildcard.GLOBAL) { + return "*.*"; + } + String targets = routines.isEmpty() + ? names.stream().map(PrivilegeTarget::joinName).collect(Collectors.joining(", ")) + : routines.stream().map(Object::toString).collect(Collectors.joining(", ")); + return wildcard == Wildcard.DATABASE ? targets + ".*" : targets; + } + + private static String joinName(List name) { + return name.stream().map(part -> part == null ? "" : part) + .collect(Collectors.joining(".")); + } + @Override public String toString() { String type = kind.name().replace('_', ' '); - String targets = routines.isEmpty() - ? names.stream().map(n -> n.stream().map(part -> part == null ? "" : part) - .collect(Collectors.joining("."))).collect(Collectors.joining(", ")) - : routines.stream().map(Object::toString).collect(Collectors.joining(", ")); if (defaultPrivileges) { return type + "S"; } if (allInSchema) { - return "ALL " + type + "S IN SCHEMA " + targets; + return "ALL " + type + "S IN SCHEMA " + getTargetText(); } - return (explicitKind ? type + " " : "") + targets; + return (explicitKind ? type + " " : "") + getTargetText(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java b/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java index f613e2d028..620827f582 100644 --- a/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java @@ -17,8 +17,10 @@ public class Revoke implements Statement { private PrivilegeClause clause = new PrivilegeClause(); + private boolean ifExists; private GrantOption.Kind optionFor; private CascadeBehavior behavior; + private boolean ignoreUnknownUser; public PrivilegeClause getClause() { return clause; @@ -28,6 +30,24 @@ public void setClause(PrivilegeClause clause) { this.clause = clause; } + /** MySQL {@code REVOKE IF EXISTS}. */ + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean ifExists) { + this.ifExists = ifExists; + } + + /** MySQL {@code IGNORE UNKNOWN USER}. */ + public boolean isIgnoreUnknownUser() { + return ignoreUnknownUser; + } + + public void setIgnoreUnknownUser(boolean ignoreUnknownUser) { + this.ignoreUnknownUser = ignoreUnknownUser; + } + public GrantOption.Kind getOptionFor() { return optionFor; } @@ -46,16 +66,22 @@ public void setBehavior(CascadeBehavior behavior) { public void appendTo(StringBuilder sql, Consumer visitor) { sql.append("REVOKE "); + if (ifExists) { + sql.append("IF EXISTS "); + } if (optionFor != null) { sql.append(optionFor).append(" OPTION FOR "); } clause.appendTo(sql, true, visitor); - if (clause.getGrantedBy() != null) { - sql.append(" GRANTED BY ").append(clause.getGrantedBy()); + if (clause.getGrantor() != null) { + sql.append(" GRANTED BY ").append(clause.getGrantor()); } if (behavior != null) { sql.append(' ').append(behavior); } + if (ignoreUnknownUser) { + sql.append(" IGNORE UNKNOWN USER"); + } } @Override diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/StringListView.java b/src/main/java/net/sf/jsqlparser/statement/grant/StringListView.java new file mode 100644 index 0000000000..9f220aa0eb --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/StringListView.java @@ -0,0 +1,52 @@ +/*- + * #%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.grant; + +import java.util.AbstractList; +import java.util.List; +import java.util.RandomAccess; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Mutable string view over a typed list, retained for the string-based GRANT/REVOKE API. */ +final class StringListView extends AbstractList implements RandomAccess { + private final Supplier> items; + private final Function parse; + + StringListView(Supplier> items, Function parse) { + this.items = items; + this.parse = parse; + } + + @Override + public int size() { + return items.get().size(); + } + + @Override + public String get(int index) { + return String.valueOf(items.get().get(index)); + } + + @Override + public String set(int index, String value) { + return String.valueOf(items.get().set(index, parse.apply(value))); + } + + @Override + public void add(int index, String value) { + items.get().add(index, parse.apply(value)); + } + + @Override + public String remove(int index) { + return String.valueOf(items.get().remove(index)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java index de84d3f823..ca4698e784 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java @@ -124,7 +124,8 @@ public enum MariaDbVersion implements Version { Feature.showColumns, // https://mariadb.com/kb/en/show-index/ Feature.showIndex, // https://mariadb.com/kb/en/use/ Feature.use, // https://mariadb.com/kb/en/grant/ - Feature.grant, // https://mariadb.com/kb/en/commit/ + Feature.grant, // https://mariadb.com/kb/en/revoke/ + Feature.revoke, // https://mariadb.com/kb/en/commit/ Feature.commit, // https://mariadb.com/kb/en/optimizer-hints/ Feature.mySqlHintStraightJoin, Feature.mysqlSelectModifiers, Feature.mysqlCalcFoundRows, 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 6aabf1fe0d..2aee4aef6b 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 @@ -112,7 +112,8 @@ public enum MySqlVersion implements Version { Feature.showTables, // https://dev.mysql.com/doc/refman/8.0/en/show-columns.html Feature.showColumns, // https://dev.mysql.com/doc/refman/8.0/en/show-index.html Feature.showIndex, // https://dev.mysql.com/doc/refman/8.0/en/grant.html - Feature.grant, // https://dev.mysql.com/doc/refman/8.0/en/use.html + Feature.grant, // https://dev.mysql.com/doc/refman/8.0/en/revoke.html + Feature.revoke, // https://dev.mysql.com/doc/refman/8.0/en/use.html Feature.use, // https://dev.mysql.com/doc/refman/8.0/en/commit.html Feature.commit, Feature.startTransaction, Feature.releaseSavepoint, Feature.transactionChain, Feature.transactionConsistentSnapshot, // diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java index c37f214ad1..952163a67a 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/GrantValidator.java @@ -23,10 +23,15 @@ public class GrantValidator extends AbstractValidator { public void validate(Grant grant) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.grant); - if (isNotEmpty(grant.getUsers())) { - grant.getUsers().forEach(u -> validateName(NamedObject.user, u)); + // PUBLIC and CURRENT_USER are not names + if (isNotEmpty(grant.getClause().getGranteeItems())) { + grant.getClause().getGranteeItems().stream() + .filter(grantee -> grantee.getAccount() != null) + .forEach(grantee -> validateName(NamedObject.user, + grantee.getAccount().toString())); } - grant.getRoles().forEach(role -> validateName(NamedObject.role, role)); + grant.getClause().getRoleItems() + .forEach(role -> validateName(NamedObject.role, role.toString())); grant.getClause().visit(this::validateOptionalFromItem, this::validateOptionalExpression); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d8..6bfd7a0bce 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2230,6 +2230,37 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** MySQL {@code REVOKE ALL [PRIVILEGES], GRANT OPTION FROM ...}, which has no ON clause. */ + private boolean isAccessAllGrantOptionAhead() { + if (getToken(1).kind != K_ALL) { return false; } + int i = "PRIVILEGES".equalsIgnoreCase(getToken(2).image) ? 3 : 2; + return ",".equals(getToken(i).image) && getToken(i + 1).kind == K_GRANT + && getToken(i + 2).kind == K_OPTION && getToken(i + 3).kind == K_FROM; + } + + /** PUBLIC, CURRENT_USER, CURRENT_ROLE or SESSION_USER, unless used as the user of user@host. */ + private boolean isAccessSpecialGranteeAhead() { + Token token = getToken(1); + int next = getToken(2).kind; + return (token.kind == K_PUBLIC || token.kind == S_IDENTIFIER) + && next != K_AT_SIGN && next != S_AT_IDENTIFIER + && Arrays.asList("PUBLIC", "CURRENT_USER", "CURRENT_ROLE", "SESSION_USER") + .contains(token.image.toUpperCase(Locale.ROOT)); + } + + /** Privileges take only GRANT OPTION; role memberships take distinct ADMIN, INHERIT and SET options. */ + private static boolean isValidAccessGrantOptions(PrivilegeClause clause, List options) { + if (clause.getPrivileges() != null) { + return options.size() == 1 && options.get(0).getKind() == GrantOption.Kind.GRANT + && options.get(0).getValue() == GrantOption.Value.OPTION; + } + EnumSet kinds = EnumSet.noneOf(GrantOption.Kind.class); + for (GrantOption option : options) { + if (option.getKind() == GrantOption.Kind.GRANT || !kinds.add(option.getKind())) { return false; } + } + return true; + } + private boolean isPostgreSqlTriggerAhead() { if (getToken(1).kind == K_CONSTRAINT) { return true; } if (getToken(1).kind != K_TRIGGER) { return false; } @@ -15502,11 +15533,112 @@ StringValue MySqlAccountNamePart(): ( token= { value = stringValue(token); } | - name=RelObjectName() { value = new StringValue(name).setQuoteStr(""); } + name=RelObjectName() + { + // a double-quoted or backquoted part keeps its quote character separately from its + // value, like a string literal; new StringValue(name) would drop the double quotes + value = name.length() > 1 && (name.charAt(0) == '"' || name.charAt(0) == '`') + && name.charAt(name.length() - 1) == name.charAt(0) + ? new StringValue().withValue(name.substring(1, name.length() - 1)) + .setQuoteStr(name.substring(0, 1)) + : new StringValue().withValue(name).setQuoteStr(""); + } ) { return value; } } +/** + * A MySQL account name, user[@host], shared by CREATE USER, DEFINER and the account statements. + * Each part is unquoted or quoted with ', " or `. The lexer reads an unquoted user@host, and the + * user@ in front of a quoted host, as one identifier, and @host after a quoted user as one + * at-identifier, so those spellings are split here. + */ +UserAccount MySqlAccountName(): +{ + UserAccount account = new UserAccount(); + StringValue part; + Token token; + int at; +} +{ + ( + // user@ followed by a quoted host + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0 + && getToken(1).image.indexOf('@') == getToken(1).image.length() - 1 }) + token= + { account.setUser(new StringValue().withValue(token.image.substring(0, token.image.length() - 1)) + .setQuoteStr("")); } + part=MySqlAccountNamePart() { account.setHost(part); } + | + // unquoted user@host + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0 + && getToken(1).image.indexOf('@') == getToken(1).image.lastIndexOf('@') }) + token= + { + at = token.image.indexOf('@'); + account.setUser(new StringValue().withValue(token.image.substring(0, at)).setQuoteStr("")); + account.setHost(new StringValue().withValue(token.image.substring(at + 1)).setQuoteStr("")); + } + | + part=MySqlAccountNamePart() { account.setUser(part); } + [ + part=MySqlAccountNamePart() { account.setHost(part); } + | + // a quoted user followed by an unquoted @host + LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && !getToken(1).image.startsWith("@@") }) + token= + { account.setHost(new StringValue().withValue(token.image.substring(1)).setQuoteStr("")); } + ] + ) + { return account; } +} + +List MySqlAccountNameList(): +{ + List accounts = new ArrayList(); + UserAccount account; +} +{ + account=MySqlAccountName() { accounts.add(account); } + ( "," account=MySqlAccountName() { accounts.add(account); } )* + { return accounts; } +} + +/** + * Roles of SET ROLE, SET DEFAULT ROLE, ALTER USER ... DEFAULT ROLE and GRANT ... AS user WITH ROLE. + * Only SET ROLE and WITH ROLE (setRole) accept DEFAULT and ALL EXCEPT. + */ +RoleSelection MySqlRoleSelection(boolean setRole): +{ + RoleSelection selection; + List roles; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == K_DEFAULT }) + { selection = new RoleSelection(RoleSelection.Kind.DEFAULT); } + | + LOOKAHEAD({ getToken(1).kind == K_NONE }) + { selection = new RoleSelection(RoleSelection.Kind.NONE); } + | + LOOKAHEAD({ getToken(1).kind == K_ALL }) + { selection = new RoleSelection(RoleSelection.Kind.ALL); } + [ + LOOKAHEAD({ getToken(1).kind == K_EXCEPT }) roles=MySqlAccountNameList() + { selection.setKind(RoleSelection.Kind.ALL_EXCEPT); selection.setRoles(roles); } + ] + | + roles=MySqlAccountNameList() + { selection = new RoleSelection(RoleSelection.Kind.ROLES).withRoles(roles); } + ) + { + requireAccessSyntax(setRole || selection.getKind() != RoleSelection.Kind.DEFAULT + && selection.getKind() != RoleSelection.Kind.ALL_EXCEPT, + "DEFAULT and ALL EXCEPT require SET ROLE"); + return selection; + } +} + UserAuthentication MySqlUserAuthentication(): { UserAuthentication authentication = new UserAuthentication(); @@ -15557,14 +15689,11 @@ UserAuthentication MySqlUserAuthentication(): UserAccount MySqlUserAccount(): { - UserAccount account = new UserAccount(); - StringValue user = null; - StringValue host = null; + UserAccount account; UserAuthentication authentication = null; } { - user=MySqlAccountNamePart() { account.setUser(user); } - [ host=MySqlAccountNamePart() { account.setHost(host); } ] + account=MySqlAccountName() [ authentication=MySqlUserAuthentication() { account.setAuthentication(authentication); } ] { return account; } } @@ -15710,8 +15839,7 @@ AlterEvent AlterEvent(): /** Account spelling is shared, including CURRENT_USER and CURRENT_USER(). */ void MySqlDefiner(MySqlDefiner definer): { - StringValue user; - StringValue host; + UserAccount account; String name; } { @@ -15721,8 +15849,7 @@ void MySqlDefiner(MySqlDefiner definer): { definer.setUser(new StringValue(name).setQuoteStr("")); } [ "(" ")" { definer.setCurrentUserParentheses(true); } ] | - user=MySqlAccountNamePart() { definer.setUser(user); } - [ host=MySqlAccountNamePart() { definer.setHost(host); } ] + account=MySqlAccountName() { definer.setUser(account.getUser()); definer.setHost(account.getHost()); } ) } @@ -20460,12 +20587,46 @@ Expression AccessSettingValue(): { Expression value; Token token; } { ( LOOKAHEAD(1) token= { value = new Column(token.image); } | value=PrimaryExpression() ) { return value; } } +/** PostgreSQL and MySQL static privileges; another identifier is a MySQL dynamic privilege. */ Privilege AccessPrivilege(): -{ Privilege result; Token token; boolean all = false; String column; ExpressionList columns = null; } +{ Privilege result; Privilege.Kind kind = null; Token token = null; String column; ExpressionList columns = null; } { - ( token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= ) - { result = new Privilege(accessEnum(Privilege.Kind.class, token.image)); } - [ { requireAccessSyntax(result.getKind() == Privilege.Kind.ALTER, "SYSTEM requires ALTER"); result.setKind(Privilege.Kind.ALTER_SYSTEM); } ] + ( + { kind = Privilege.Kind.ALTER; } + [ { kind = Privilege.Kind.ALTER_SYSTEM; } + | LOOKAHEAD({ isAccessKeywordAhead("ROUTINE") }) AccessKeyword("ROUTINE") { kind = Privilege.Kind.ALTER_ROUTINE; } ] + | + { kind = Privilege.Kind.CREATE; } + [ { kind = Privilege.Kind.CREATE_VIEW; } + | { kind = Privilege.Kind.CREATE_USER; } + | { kind = Privilege.Kind.CREATE_TABLESPACE; } + | { kind = Privilege.Kind.CREATE_TEMPORARY_TABLES; } + | LOOKAHEAD({ isAccessKeywordAhead("ROLE") }) AccessKeyword("ROLE") { kind = Privilege.Kind.CREATE_ROLE; } + | LOOKAHEAD({ isAccessKeywordAhead("ROUTINE") }) AccessKeyword("ROUTINE") { kind = Privilege.Kind.CREATE_ROUTINE; } ] + | + { kind = Privilege.Kind.DROP; } + [ LOOKAHEAD({ isAccessKeywordAhead("ROLE") }) AccessKeyword("ROLE") { kind = Privilege.Kind.DROP_ROLE; } ] + | + { kind = Privilege.Kind.GRANT_OPTION; } + | + { kind = Privilege.Kind.LOCK_TABLES; } + | + ( { kind = Privilege.Kind.SHOW_VIEW; } | AccessKeyword("DATABASES") { kind = Privilege.Kind.SHOW_DATABASES; } ) + | + LOOKAHEAD({ isAccessKeywordAhead("REPLICATION") && (getToken(2).kind == K_SLAVE || "CLIENT".equalsIgnoreCase(getToken(2).image)) }) + AccessKeyword("REPLICATION") + ( { kind = Privilege.Kind.REPLICATION_SLAVE; } | AccessKeyword("CLIENT") { kind = Privilege.Kind.REPLICATION_CLIENT; } ) + | + ( token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= ) + { kind = accessEnum(Privilege.Kind.class, token.image); } + | + token= + { if (Arrays.asList("MAINTAIN", "USAGE", "PROCESS", "PROXY", "RELOAD", "SUPER").contains(token.image.toUpperCase(Locale.ROOT))) { + kind = accessEnum(Privilege.Kind.class, token.image); + requireAccessSyntax(kind != Privilege.Kind.PROXY, "PROXY requires an account target"); + } } + ) + { result = kind == null ? Privilege.dynamic(token.image) : new Privilege(kind); } [ LOOKAHEAD({ isAccessKeywordAhead("PRIVILEGES") }) AccessKeyword("PRIVILEGES") { requireAccessSyntax(result.getKind() == Privilege.Kind.ALL, "PRIVILEGES requires ALL"); result.setUsePrivileges(true); } ] [ "(" { columns = new ExpressionList(); } @@ -20541,34 +20702,101 @@ PrivilegeTarget AccessTarget(boolean defaults): ( "," number= { target.getNames().add(Collections.singletonList(number.image)); } )* | [ LOOKAHEAD({ isAccessTargetKindAhead() }) kind=AccessObjectKind(false) { target.setKind(kind); target.setExplicitKind(true); } ] - name=RelObjectNames() { target.getNames().add(name.getNames()); } - ( "," name=RelObjectNames() { target.getNames().add(name.getNames()); } )* + ( + // MySQL privilege levels *, *.* and db.* + "*" { target.setWildcard(PrivilegeTarget.Wildcard.DEFAULT_DATABASE); } + [ "." "*" { target.setWildcard(PrivilegeTarget.Wildcard.GLOBAL); } ] + | + name=RelObjectNames() { target.getNames().add(name.getNames()); } + ( + "." "*" + { requireAccessSyntax(name.getNames().size() == 1, "Expected database.*"); target.setWildcard(PrivilegeTarget.Wildcard.DATABASE); } + | + ( "," name=RelObjectNames() { target.getNames().add(name.getNames()); } )* + ) + ) + { requireAccessSyntax(target.getWildcard() == null || target.getKind() == PrivilegeTarget.Kind.TABLE, "Wildcards require a table target"); } ) { requireAccessSyntax(target.isDefaultPrivileges() == defaults, "Invalid default-privilege target"); return target; } } -String AccessGrantee(): -{ String name; boolean group = false; } -{ [ LOOKAHEAD(1) { group = true; } ] name=RelObjectName() { return (group ? "GROUP " : "") + name; } } +/** A role specification shared by MySQL and PostgreSQL grantees, grantors and accounts. */ +Grantee AccessGrantee(): +{ Grantee grantee; StringValue name; UserAccount account; Token token; } +{ + ( + LOOKAHEAD({ getToken(1).kind == K_GROUP }) name=MySqlAccountNamePart() + { grantee = new Grantee(Grantee.Kind.GROUP).withAccount(new UserAccount().withUser(name)); } + | + LOOKAHEAD({ isAccessSpecialGranteeAhead() }) ( token= | token= ) + { grantee = new Grantee(accessEnum(Grantee.Kind.class, token.image)); } + [ LOOKAHEAD(2) "(" ")" { requireAccessSyntax(grantee.getKind() == Grantee.Kind.CURRENT_USER, "Only CURRENT_USER takes parentheses"); + grantee.setCurrentUserParentheses(true); } ] + | + account=MySqlAccountName() { grantee = new Grantee(account); } + ) + { return grantee; } +} -List AccessGrantees(): -{ List names = new ArrayList(); String name; } -{ name=AccessGrantee() { names.add(name); } ( "," name=AccessGrantee() { names.add(name); } )* { return names; } } +List AccessGrantees(): +{ List grantees = new ArrayList(); Grantee grantee; } +{ grantee=AccessGrantee() { grantees.add(grantee); } ( "," grantee=AccessGrantee() { grantees.add(grantee); } )* { return grantees; } } PrivilegeClause AccessClause(boolean revoke, boolean defaults): -{ PrivilegeClause clause = new PrivilegeClause(); Privilege privilege; PrivilegeTarget target; List names; } +{ PrivilegeClause clause = new PrivilegeClause(); Privilege privilege; PrivilegeTarget target; List roles; List grantees; Grantee account; } { ( + LOOKAHEAD({ isAccessKeywordAhead("PROXY") && getToken(2).kind == K_ON }) + AccessKeyword("PROXY") account=AccessGrantee() + { + requireAccessSyntax(!defaults, "PROXY is not part of default privileges"); + requireAccessSyntax(account.getKind() == Grantee.Kind.ACCOUNT || account.getKind() == Grantee.Kind.CURRENT_USER, "PROXY requires an account"); + clause.setPrivileges(new ArrayList()); + clause.getPrivileges().add(new Privilege(Privilege.Kind.PROXY)); + target = new PrivilegeTarget(); target.setKind(PrivilegeTarget.Kind.ACCOUNT); target.setAccount(account); + clause.setTarget(target); + } + | + LOOKAHEAD({ isAccessAllGrantOptionAhead() }) + { privilege = new Privilege(Privilege.Kind.ALL); } + [ AccessKeyword("PRIVILEGES") { privilege.setUsePrivileges(true); } ] + "," + { + requireAccessSyntax(!defaults, "ALL, GRANT OPTION is not part of default privileges"); + clause.setPrivileges(new ArrayList()); + clause.getPrivileges().add(privilege); + clause.getPrivileges().add(new Privilege(Privilege.Kind.GRANT_OPTION)); + clause.setTarget(null); + } + | LOOKAHEAD({ isObjectPrivilegeAhead() }) { clause.setPrivileges(new ArrayList()); } privilege=AccessPrivilege() { clause.getPrivileges().add(privilege); } ( "," privilege=AccessPrivilege() { clause.getPrivileges().add(privilege); } )* target=AccessTarget(defaults) { clause.setTarget(target); } | - names=AccessNames() { clause.setRoles(names); } + roles=MySqlAccountNameList() { clause.setRoleItems(roles); clause.setTarget(null); } ) ( { requireAccessSyntax(!revoke, "REVOKE requires FROM"); } | { requireAccessSyntax(revoke, "GRANT requires TO"); } ) - names=AccessGrantees() { clause.setGrantees(names); return clause; } + grantees=AccessGrantees() { clause.setGranteeItems(grantees); return clause; } +} + +GrantOption AccessGrantOption(): +{ GrantOption option; Token token; } +{ + ( token= | token= | token= ) { option = new GrantOption(accessEnum(GrantOption.Kind.class, token.image)); } + ( token= | token= | token= ) { option.setValue(accessEnum(GrantOption.Value.class, token.image)); return option; } +} + +Grantee AccessGrantor(boolean defaults): +{ Grantee grantor; } +{ + AccessKeyword("GRANTED") grantor=AccessGrantee() + { + requireAccessSyntax(!defaults, "GRANTED BY is not part of default privileges"); + requireAccessSyntax(grantor.getKind() != Grantee.Kind.PUBLIC && grantor.getKind() != Grantee.Kind.GROUP, "Invalid grantor"); + return grantor; + } } Grant Grant(): @@ -20576,31 +20804,41 @@ Grant Grant(): { grant=AccessGrant(false) { return grant; } } Grant AccessGrant(boolean defaults): -{ Grant grant = new Grant(); PrivilegeClause clause; Token token; GrantOption option; String name; } +{ Grant grant = new Grant(); PrivilegeClause clause; GrantOption option; Grantee grantee; RoleSelection roles; } { clause=AccessClause(false, defaults) { grant.setClause(clause); } [ LOOKAHEAD(1) - ( token= | token= | token= ) { option = new GrantOption(accessEnum(GrantOption.Kind.class, token.image)); } - ( token= | token= | token= ) - { option.setValue(accessEnum(GrantOption.Value.class, token.image)); grant.setOption(option); - requireAccessSyntax(clause.getPrivileges() == null ? option.getKind() != GrantOption.Kind.GRANT - : option.getKind() == GrantOption.Kind.GRANT && option.getValue() == GrantOption.Value.OPTION, "Invalid GRANT option"); } + option=AccessGrantOption() { grant.getOptions().add(option); } + ( "," option=AccessGrantOption() { grant.getOptions().add(option); } )* + { requireAccessSyntax(isValidAccessGrantOptions(clause, grant.getOptions()), "Invalid GRANT option"); } + ] + [ grantee=AccessGrantee() + { + requireAccessSyntax(!defaults && clause.getPrivileges() != null && clause.getTarget() != null + && clause.getTarget().getKind() != PrivilegeTarget.Kind.ACCOUNT, "AS requires object privileges"); + requireAccessSyntax(grantee.getKind() == Grantee.Kind.ACCOUNT || grantee.getKind() == Grantee.Kind.CURRENT_USER, "AS requires an account"); + grant.setAsUser(grantee); + } + [ LOOKAHEAD({ getToken(1).kind == K_WITH && "ROLE".equalsIgnoreCase(getToken(2).image) }) + AccessKeyword("ROLE") roles=MySqlRoleSelection(true) { grant.setWithRole(roles); } ] ] - [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) AccessKeyword("GRANTED") name=RelObjectName() - { requireAccessSyntax(!defaults, "GRANTED BY is not part of default privileges"); clause.setGrantedBy(name); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) grantee=AccessGrantor(defaults) { clause.setGrantor(grantee); } ] { return grant; } } Revoke Revoke(boolean defaults): -{ Revoke revoke = new Revoke(); PrivilegeClause clause; Token token; String name; } +{ Revoke revoke = new Revoke(); PrivilegeClause clause; Token token; Grantee grantor; } { + [ LOOKAHEAD(2) + { requireAccessSyntax(!defaults, "IF EXISTS is not part of default privileges"); revoke.setIfExists(true); } ] [ LOOKAHEAD(3) ( token= | token= | token= ) { revoke.setOptionFor(accessEnum(GrantOption.Kind.class, token.image)); } ] clause=AccessClause(true, defaults) { revoke.setClause(clause); } - [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) AccessKeyword("GRANTED") name=RelObjectName() - { requireAccessSyntax(!defaults, "GRANTED BY is not part of default privileges"); clause.setGrantedBy(name); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) grantor=AccessGrantor(defaults) { clause.setGrantor(grantor); } ] [ ( { revoke.setBehavior(CascadeBehavior.CASCADE); } | { revoke.setBehavior(CascadeBehavior.RESTRICT); } ) ] + [ + { requireAccessSyntax(!defaults, "IGNORE UNKNOWN USER is not part of default privileges"); revoke.setIgnoreUnknownUser(true); } ] { requireAccessSyntax(revoke.getOptionFor() == null || (clause.getPrivileges() == null ? revoke.getOptionFor() != GrantOption.Kind.GRANT : revoke.getOptionFor() == GrantOption.Kind.GRANT), "Invalid REVOKE option"); return revoke; } diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 901608d62c..944293b502 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1718,12 +1718,26 @@ the PostgreSQL ``CreateRole`` AST for this ambiguous form:: "CREATE USER app", parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)); +MySQL account names (``user[@host]``) of ``CREATE USER`` and of view, trigger and +routine ``DEFINER`` clauses share one grammar rule and are read into separate user +and host ``StringValue`` parts (``UserAccount`` and ``MySqlDefiner``). Every MySQL +spelling is accepted, including ``app@localhost``, ``app@'%'`` and +``'app'@localhost``. A part quoted with ``'``, ``"`` or a backquote keeps its quote +character in ``getQuoteStr()`` and its text in ``getValue()``:: + + CreateUser createUser = (CreateUser) CCJSqlParserUtil.parse( + "CREATE USER `app`@'%' IDENTIFIED BY 'secret'"); + UserAccount account = createUser.getAccounts().get(0); + account.getUser().getValue(); // app + account.getUser().getQuoteStr(); // ` + account.getHost().getValue(); // % + ``Grant`` retains its string-based getters, setters and fluent methods. Its ``PrivilegeClause`` exposes typed ``Privilege`` items, column lists, ``PrivilegeTarget`` kinds, multiple role memberships and grantor information. ``getPrivileges()`` is a mutable string view of the typed privilege list. ``getRole()`` and ``getObjectName()`` expose the first role or named target for -legacy callers; use ``getRoles()`` and ``getTarget()`` for the complete lists. +legacy callers; use ``getRoleItems()`` and ``getTarget()`` for the complete lists. Names in ``PrivilegeTarget`` are multipart identifiers, not SQL clause text. Routine targets use ``RoutineReference`` data-type signatures, with null arguments for an omitted signature and an empty list for explicit ``()``. @@ -1752,6 +1766,63 @@ References: `CREATE ROLE `_, `CREATE TRIGGER `_. +MySQL privileges and accounts in GRANT and REVOKE +------------------------------------------------- + +``Grant`` and ``Revoke`` also parse the MySQL forms, in every dialect: + +- **Privilege levels**: ``PrivilegeTarget.getWildcard()`` is ``DEFAULT_DATABASE`` for ``*``, + ``GLOBAL`` for ``*.*`` and ``DATABASE`` for ``db.*``, whose database is the single entry + of ``getNames()``. Named levels such as ``db.tbl`` keep a null wildcard. Only named + table targets are reported by table-name discovery. +- **Privileges**: MySQL static privileges are ``Privilege.Kind`` constants, for example + ``CREATE_TEMPORARY_TABLES`` or ``REPLICATION_SLAVE``; ``getName()`` returns the SQL + spelling. Another identifier, such as ``BACKUP_ADMIN``, is a dynamic privilege of kind + ``DYNAMIC`` with ``getDynamicName()``. +- **Grantees**: ``PrivilegeClause.getGranteeItems()`` returns ``Grantee`` values, which + MySQL and PostgreSQL share. ``ACCOUNT`` holds a ``UserAccount``: a MySQL ``user[@host]``, + read like the ``CREATE USER`` account names above, or a PostgreSQL role name without + host. The other kinds are + PostgreSQL ``GROUP name``, ``PUBLIC``, ``CURRENT_USER`` (MySQL also ``CURRENT_USER()``), + ``CURRENT_ROLE`` and ``SESSION_USER``. ``getGrantor()`` types ``GRANTED BY`` the same way. +- **Role memberships**: ``getRoleItems()`` holds the granted roles as ``UserAccount`` + values, so ``'reader'`` and ``'writer'@'%'`` keep their quoting and host. A membership + has no privileges and no target. +- **MySQL clauses**: ``PROXY ON user`` is a ``PROXY`` privilege on an ``ACCOUNT`` target + (``PrivilegeTarget.getAccount()``). ``REVOKE ALL [PRIVILEGES], GRANT OPTION FROM ...`` + has the privileges ``ALL`` and ``GRANT_OPTION`` and no target. ``Revoke`` records + ``IF EXISTS`` and ``IGNORE UNKNOWN USER``; ``Grant.getAsUser()`` and ``getWithRole()`` + hold ``AS user [WITH ROLE ...]``; the role selection is a ``RoleSelection`` from + ``net.sf.jsqlparser.statement.create.user``. + +.. code-block:: java + + Grant grant = (Grant) CCJSqlParserUtil.parse( + "GRANT SELECT, BACKUP_ADMIN ON db1.* TO 'app'@'%', CURRENT_USER"); + grant.getTarget().getWildcard(); // DATABASE + grant.getPrivilegeItems().get(1).getDynamicName(); // BACKUP_ADMIN + UserAccount app = grant.getClause().getGranteeItems().get(0).getAccount(); + app.getHost().getValue(); // % + +``Grant.getOptions()`` lists every ``WITH`` option, including PostgreSQL membership +options such as ``WITH INHERIT TRUE, SET FALSE``. Privileges take only ``GRANT OPTION``; +memberships take distinct ``ADMIN``, ``INHERIT`` and ``SET`` options. + +The string accessors are kept and derived from the typed state: ``getUsers()``, +``getGrantees()`` and ``getRoles()`` are mutable string views that write through, +``getOption()`` is the first option and ``getGrantedBy()`` renders the grantor. +``getObjectName()`` returns a wildcard level as written, for example ``db1.*``, and +``setObjectName()`` replaces it with a named target. Assigned text is converted with +``Grantee.valueOf()``: special role names are recognized and other text is kept as the +account name. Parsed memberships have no target; ``new Grant()`` still starts with an +empty table target, and ``setRole()`` clears it. + +The MySQL and MariaDB validation capabilities now include ``revoke``. + +References: `GRANT `_, +`REVOKE `_, +`Specifying Account Names `_. + Migrating shared ALTER enums ---------------------------- diff --git a/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java b/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java new file mode 100644 index 0000000000..455f18b6a5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java @@ -0,0 +1,123 @@ +/*- + * #%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.create.user; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.MySqlDefiner; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +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; +import java.util.stream.Stream; + +class MySqlAccountNameTest { + + private static final Consumer MYSQL = p -> p.withDialect(Dialect.MYSQL); + private static final Consumer NO_DIALECT = p -> { + }; + + /** SQL spelling, user value, user quote, host value, host quote. */ + static Stream accounts() { + return Stream.of( + Arguments.of("app", "app", "", null, null), + Arguments.of("app@localhost", "app", "", "localhost", ""), + Arguments.of("app@'%'", "app", "", "%", "'"), + Arguments.of("app@'10.0.0.%'", "app", "", "10.0.0.%", "'"), + Arguments.of("'app'@localhost", "app", "'", "localhost", ""), + Arguments.of("'app'@'%'", "app", "'", "%", "'"), + Arguments.of("`app`@`%`", "app", "`", "%", "`"), + Arguments.of("\"app\"@\"%\"", "app", "\"", "%", "\""), + Arguments.of("'app'", "app", "'", null, null), + Arguments.of("`my app`@'%'", "my app", "`", "%", "'")); + } + + @ParameterizedTest + @MethodSource("accounts") + void createUserSplitsEveryAccountSpelling(String account, String user, String userQuote, + String host, String hostQuote) throws JSQLParserException { + for (Consumer mode : java.util.Arrays.asList(MYSQL, NO_DIALECT)) { + String sql = "CREATE USER " + account + " IDENTIFIED BY 'x'"; + CreateUser createUser = assertInstanceOf(CreateUser.class, + assertSqlCanBeParsedAndDeparsed(sql, true, mode)); + assertEquals(sql, createUser.toString()); + assertEquals(sql, deparse(createUser)); + assertPart(createUser.getAccounts().get(0).getUser(), user, userQuote); + assertPart(createUser.getAccounts().get(0).getHost(), host, hostQuote); + } + } + + @ParameterizedTest + @MethodSource("accounts") + void definerSplitsEveryAccountSpelling(String account, String user, String userQuote, + String host, String hostQuote) throws JSQLParserException { + String view = "CREATE DEFINER = " + account + " VIEW v AS SELECT 1"; + CreateView createView = assertInstanceOf(CreateView.class, + assertSqlCanBeParsedAndDeparsed(view, true, MYSQL)); + MySqlDefiner definer = createView.getMySqlOptions().getDefiner(); + assertPart(definer.getUser(), user, userQuote); + assertPart(definer.getHost(), host, hostQuote); + + String trigger = "CREATE DEFINER = " + account + + " TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET NEW.a = 1"; + CreateTrigger createTrigger = assertInstanceOf(CreateTrigger.class, + assertSqlCanBeParsedAndDeparsed(trigger, true, MYSQL)); + assertEquals(trigger, createTrigger.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE USER app@", "CREATE USER @host", "CREATE USER 'app'@"}) + void rejectsMalformedAccounts(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + } + + @org.junit.jupiter.api.Test + void keepsDoubleQuotesWithoutDialect() throws JSQLParserException { + // before, the double quotes were dropped and the account rendered as app@% + String sql = "CREATE USER \"app\"@\"%\" IDENTIFIED BY 'x'"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, MYSQL).toString()); + } + + @org.junit.jupiter.api.Test + void definerCurrentUserIsUnchanged() throws JSQLParserException { + CreateView view = assertInstanceOf(CreateView.class, assertSqlCanBeParsedAndDeparsed( + "CREATE DEFINER = CURRENT_USER() VIEW v AS SELECT 1", true, MYSQL)); + assertNull(view.getMySqlOptions().getDefiner().getHost()); + } + + private static void assertPart(StringValue part, String value, String quote) { + if (value == null) { + assertNull(part); + return; + } + assertEquals(value, part.getValue()); + assertEquals(quote, part.getQuoteStr()); + } + + private static String deparse(CreateUser createUser) { + StringBuilder builder = new StringBuilder(); + createUser.accept(new StatementDeParser(builder), null); + return builder.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/grant/GranteeModelTest.java b/src/test/java/net/sf/jsqlparser/statement/grant/GranteeModelTest.java new file mode 100644 index 0000000000..dd26645eed --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/grant/GranteeModelTest.java @@ -0,0 +1,195 @@ +/*- + * #%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.grant; + +import static net.sf.jsqlparser.test.TestUtils.assertEqualsObjectTree; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** The grantee model shared by MySQL and PostgreSQL, and the string-based legacy API. */ +class GranteeModelTest { + + private static final Consumer POSTGRESQL = + parser -> parser.withDialect(Dialect.POSTGRESQL); + + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, POSTGRESQL); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), POSTGRESQL).toString()); + return statement; + } + + @Test + void typesPostgreSqlRoleSpecifications() throws JSQLParserException { + Grant grant = (Grant) parse("GRANT SELECT ON users TO app, GROUP staff, PUBLIC, " + + "CURRENT_USER, CURRENT_ROLE, SESSION_USER, \"Mixed Case\" GRANTED BY CURRENT_ROLE"); + + assertThat(grant.getClause().getGranteeItems()).extracting(Grantee::getKind) + .containsExactly(Grantee.Kind.ACCOUNT, Grantee.Kind.GROUP, Grantee.Kind.PUBLIC, + Grantee.Kind.CURRENT_USER, Grantee.Kind.CURRENT_ROLE, + Grantee.Kind.SESSION_USER, Grantee.Kind.ACCOUNT); + Grantee group = grant.getClause().getGranteeItems().get(1); + assertEquals("staff", group.getAccount().getUser().getValue()); + assertNull(group.getAccount().getHost()); + Grantee quoted = grant.getClause().getGranteeItems().get(6); + assertEquals("Mixed Case", quoted.getAccount().getUser().getValue()); + assertEquals("\"", quoted.getAccount().getUser().getQuoteStr()); + assertEquals(Grantee.Kind.CURRENT_ROLE, grant.getClause().getGrantor().getKind()); + + assertEquals(Arrays.asList("app", "GROUP staff", "PUBLIC", "CURRENT_USER", "CURRENT_ROLE", + "SESSION_USER", "\"Mixed Case\""), grant.getUsers()); + assertEquals("CURRENT_ROLE", grant.getClause().getGrantedBy()); + assertThat(TablesNamesFinder.findTables(grant.toString())).containsExactly("users"); + } + + @Test + void typesMembershipOptionList() throws JSQLParserException { + Grant grant = (Grant) parse( + "GRANT app_read TO alice WITH INHERIT TRUE, SET FALSE, ADMIN OPTION GRANTED BY postgres"); + + assertNull(grant.getTarget()); + assertThat(grant.getOptions()).extracting(GrantOption::getKind).containsExactly( + GrantOption.Kind.INHERIT, GrantOption.Kind.SET, GrantOption.Kind.ADMIN); + assertThat(grant.getOptions()).extracting(GrantOption::getValue).containsExactly( + GrantOption.Value.TRUE, GrantOption.Value.FALSE, GrantOption.Value.OPTION); + assertSame(grant.getOptions().get(0), grant.getOption()); + assertEquals("postgres", grant.getClause().getGrantor().getAccount().getUser().getValue()); + + grant.setOption(new GrantOption(GrantOption.Kind.ADMIN)); + assertEquals("GRANT app_read TO alice WITH ADMIN OPTION GRANTED BY postgres", + grant.toString()); + grant.setOption(null); + assertEquals("GRANT app_read TO alice GRANTED BY postgres", grant.toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT app_read TO alice WITH INHERIT TRUE, INHERIT FALSE", + "GRANT app_read TO alice WITH ADMIN OPTION, GRANT OPTION", + "GRANT SELECT ON users TO alice WITH GRANT OPTION, INHERIT TRUE", + "GRANT SELECT ON users TO alice GRANTED BY PUBLIC", + "GRANT SELECT ON users TO alice GRANTED BY GROUP staff", + "GRANT SELECT ON users TO GROUP" + }) + void rejectsInvalidRoleSpecifications(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, POSTGRESQL)); + } + + @Test + void legacyGranteeStringsWriteThroughTypedState() throws JSQLParserException { + Grant grant = (Grant) CCJSqlParserUtil.parse("GRANT SELECT ON t TO 'app'@'%', app2", + parser -> parser.withDialect(Dialect.MYSQL)); + Grantee typed = grant.getClause().getGranteeItems().get(0); + + grant.getUsers().add("PUBLIC"); + grant.getUsers().set(1, "GROUP staff"); + grant.addUsers("CURRENT_USER()"); + assertSame(typed, grant.getClause().getGranteeItems().get(0)); + assertThat(grant.getClause().getGranteeItems()).extracting(Grantee::getKind) + .containsExactly(Grantee.Kind.ACCOUNT, Grantee.Kind.GROUP, Grantee.Kind.PUBLIC, + Grantee.Kind.CURRENT_USER); + assertEquals("GRANT SELECT ON t TO 'app'@'%', GROUP staff, PUBLIC, CURRENT_USER()", + grant.toString()); + + grant.getUsers().remove(0); + grant.getClause().setGrantedBy("SESSION_USER"); + assertEquals(Grantee.Kind.SESSION_USER, grant.getClause().getGrantor().getKind()); + assertEquals("GRANT SELECT ON t TO GROUP staff, PUBLIC, CURRENT_USER() " + + "GRANTED BY SESSION_USER", grant.toString()); + + grant.setUsers(Collections.singletonList("reader")); + assertEquals(Grantee.Kind.ACCOUNT, grant.getClause().getGranteeItems().get(0).getKind()); + grant.setUsers(null); + assertNull(grant.getUsers()); + assertNull(new Grant().getUsers()); + } + + @Test + void legacyBuildersMatchParsedStatements() throws JSQLParserException { + Grant parsed = + (Grant) CCJSqlParserUtil.parse("GRANT SELECT, CREATE VIEW ON t1 TO u, PUBLIC"); + Grant built = new Grant().addPrivileges("SELECT", "CREATE VIEW").withObjectName("t1") + .addUsers("u", "PUBLIC"); + assertEqualsObjectTree(parsed, built); + assertEquals(Privilege.Kind.CREATE_VIEW, built.getPrivilegeItems().get(1).getKind()); + + Grant role = (Grant) CCJSqlParserUtil.parse("GRANT r1 TO u GRANTED BY CURRENT_USER"); + Grant builtRole = new Grant().withRole("r1").addUsers("u"); + builtRole.getClause().setGrantedBy("CURRENT_USER"); + assertEqualsObjectTree(role, builtRole); + assertNull(builtRole.getTarget()); + + builtRole.getRoles().add("'r2'@'%'"); + assertEquals("GRANT r1, 'r2'@'%' TO u GRANTED BY CURRENT_USER", builtRole.toString()); + assertEquals("r1", builtRole.getRole()); + } + + @Test + void legacyObjectNameReplacesWildcard() throws JSQLParserException { + Grant grant = (Grant) CCJSqlParserUtil.parse("GRANT SELECT ON *.* TO app"); + assertEquals("*.*", grant.getObjectName()); + + grant.setObjectName(Arrays.asList("db1", "t1")); + assertNull(grant.getTarget().getWildcard()); + assertEquals("GRANT SELECT ON db1.t1 TO app", grant.toString()); + + Grant role = (Grant) CCJSqlParserUtil.parse("GRANT reader TO app"); + assertNull(role.getObjectName()); + role.setPrivileges(Collections.singletonList("SELECT")); + role.setObjectName("t1"); + assertEquals("GRANT SELECT ON t1 TO app", role.toString()); + } + + @Test + void legacyPrivilegeNames() { + assertEquals(Privilege.Kind.CREATE_TEMPORARY_TABLES, + new Privilege("CREATE TEMPORARY TABLES").getKind()); + assertEquals(Privilege.Kind.ALTER_SYSTEM, new Privilege("ALTER SYSTEM").getKind()); + Privilege unknown = new Privilege("DYNAMIC"); + assertNull(unknown.getKind()); + assertEquals("DYNAMIC", unknown.toString()); + Privilege text = new Privilege("select (a)"); + assertNull(text.getKind()); + assertEquals("select (a)", text.getName()); + assertFalse(new Privilege(Privilege.Kind.SELECT).isUsePrivileges()); + } + + @Test + void grantedByAndRolesAcceptLegacyText() { + PrivilegeClause clause = new PrivilegeClause(); + clause.setGrantedBy("GROUP admins"); + assertEquals(Grantee.Kind.GROUP, clause.getGrantor().getKind()); + clause.setGrantedBy(null); + assertNull(clause.getGrantor()); + + clause.setRoles(Arrays.asList("a", "b")); + clause.getRoles().set(0, "c"); + assertEquals(Arrays.asList("c", "b"), clause.getRoles()); + clause.setRoles(null); + assertEquals(Collections.emptyList(), clause.getRoleItems()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/grant/MySqlGrantRevokeTest.java b/src/test/java/net/sf/jsqlparser/statement/grant/MySqlGrantRevokeTest.java new file mode 100644 index 0000000000..9e1009ef2c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/grant/MySqlGrantRevokeTest.java @@ -0,0 +1,355 @@ +/*- + * #%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.grant; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.user.RoleSelection; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.validation.feature.DatabaseType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlGrantRevokeTest { + + private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL); + + /** Parses with the MySQL dialect, checks toString and the deparser, and reparses. */ + private static Statement parse(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, MYSQL); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), MYSQL).toString()); + return statement; + } + + private static Grant grant(String sql) throws JSQLParserException { + return (Grant) parse(sql); + } + + private static Revoke revoke(String sql) throws JSQLParserException { + return (Revoke) parse(sql); + } + + /** The value and quote of the user and host, for example {@code app(')@%(')}. */ + private static String account(UserAccount account) { + StringValue host = account.getHost(); + return account.getUser().getValue() + "(" + account.getUser().getQuoteStr() + ")" + + (host == null ? "" : "@" + host.getValue() + "(" + host.getQuoteStr() + ")"); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT SELECT ON db1.* TO 'app'@'%'", + "GRANT SELECT, INSERT, UPDATE, DELETE ON `db1`.`t1` TO 'app'@'%' WITH GRANT OPTION", + "GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION", + "GRANT SELECT (id, name), UPDATE (name) ON db1.t1 TO 'app'@'%'", + "GRANT EXECUTE ON PROCEDURE db1.p1 TO 'app'@'%'", + "GRANT EXECUTE ON FUNCTION db1.f1 TO app@'%'", + "GRANT CREATE, ALTER, DROP, INDEX, REFERENCES, CREATE VIEW, SHOW VIEW, TRIGGER, EVENT, " + + "CREATE ROUTINE, ALTER ROUTINE, CREATE TEMPORARY TABLES, LOCK TABLES " + + "ON db1.* TO 'dev'@'%'", + "GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT " + + "ON *.* TO 'cdc'@'%'", + "GRANT PROCESS, SUPER, FILE, SHUTDOWN, CREATE USER, CREATE TABLESPACE, CREATE ROLE, " + + "DROP ROLE, USAGE, GRANT OPTION ON *.* TO 'admin'@'%'", + "GRANT BACKUP_ADMIN, replication_slave_admin ON *.* TO 'admin'@'%'", + "GRANT 'reader', 'writer'@'%' TO 'app'@'%' WITH ADMIN OPTION", + "GRANT reader TO app", + "GRANT PROXY ON 'a'@'%' TO 'b'@'%', CURRENT_USER WITH GRANT OPTION", + "GRANT SELECT ON TABLE db1.* TO `app`@`%`", + "GRANT ALL ON * TO app@localhost", + "GRANT SELECT ON `my-db`.* TO 'app'@localhost, CURRENT_USER()", + "GRANT SELECT ON db1.* TO 'app'@'%' AS 'admin'@'%' WITH ROLE 'r1', r2@'%'", + "GRANT SELECT ON db1.t1 TO app WITH GRANT OPTION AS CURRENT_USER WITH ROLE DEFAULT", + "REVOKE INSERT ON db1.* FROM 'app'@'%', app2", + "REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%'", + "REVOKE ALL, GRANT OPTION FROM app", + "REVOKE GRANT OPTION ON *.* FROM app", + "REVOKE 'reader', writer FROM 'app'@'%'", + "REVOKE IF EXISTS SELECT ON db1.t1 FROM 'app'@'%' IGNORE UNKNOWN USER", + "REVOKE IF EXISTS PROXY ON 'a'@'%' FROM 'b'@'%'", + "REVOKE REPLICATION SLAVE, REPLICATION CLIENT ON *.* FROM 'repl'@'%'" + }) + void roundTripsMySqlForms(String sql) throws JSQLParserException { + Statement statement = parse(sql); + + Statement withoutDialect = assertSqlCanBeParsedAndDeparsed(sql, false); + assertEquals(statement.toString(), withoutDialect.toString()); + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "* | DEFAULT_DATABASE | [] | *", + "*.* | GLOBAL | [] | *.*", + "db1.* | DATABASE | [db1] | db1.*", + "`my-db`.* | DATABASE | [`my-db`] | `my-db`.*" + }) + void typesWildcardPrivilegeLevels(String level, PrivilegeTarget.Wildcard wildcard, + String names, String objectName) throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON " + level + " TO app"); + PrivilegeTarget target = grant.getTarget(); + + assertEquals(wildcard, target.getWildcard()); + assertEquals(PrivilegeTarget.Kind.TABLE, target.getKind()); + assertFalse(target.isExplicitKind()); + assertEquals(names, target.getNames().stream().map(name -> name.get(0)) + .collect(Collectors.toList()).toString()); + assertEquals(objectName, grant.getObjectName()); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + } + + @Test + void namedPrivilegeLevelsRemainTables() throws JSQLParserException { + Grant qualified = grant("GRANT SELECT ON TABLE db1.t1 TO app"); + assertNull(qualified.getTarget().getWildcard()); + assertTrue(qualified.getTarget().isExplicitKind()); + assertEquals(Arrays.asList("db1", "t1"), qualified.getTarget().getNames().get(0)); + assertThat(TablesNamesFinder.findTables(qualified.toString())).containsExactly("db1.t1"); + + Grant routine = grant("GRANT EXECUTE ON PROCEDURE db1.p1 TO app"); + assertEquals(PrivilegeTarget.Kind.PROCEDURE, routine.getTarget().getKind()); + assertEquals("db1.p1", routine.getTarget().getRoutines().get(0).getName()); + } + + @Test + void typesStaticAndDynamicPrivileges() throws JSQLParserException { + Grant grant = grant("GRANT CREATE TEMPORARY TABLES, LOCK TABLES, SHOW DATABASES, " + + "REPLICATION SLAVE, REPLICATION CLIENT, ALTER ROUTINE, CREATE VIEW, INDEX, " + + "SHUTDOWN, GRANT OPTION, backup_admin ON *.* TO app"); + + assertThat(grant.getPrivilegeItems()).extracting(Privilege::getKind).containsExactly( + Privilege.Kind.CREATE_TEMPORARY_TABLES, Privilege.Kind.LOCK_TABLES, + Privilege.Kind.SHOW_DATABASES, Privilege.Kind.REPLICATION_SLAVE, + Privilege.Kind.REPLICATION_CLIENT, Privilege.Kind.ALTER_ROUTINE, + Privilege.Kind.CREATE_VIEW, Privilege.Kind.INDEX, Privilege.Kind.SHUTDOWN, + Privilege.Kind.GRANT_OPTION, Privilege.Kind.DYNAMIC); + Privilege dynamic = grant.getPrivilegeItems().get(10); + assertEquals("backup_admin", dynamic.getDynamicName()); + assertEquals("backup_admin", dynamic.getName()); + assertEquals("CREATE TEMPORARY TABLES", grant.getPrivilegeItems().get(0).getName()); + assertEquals("CREATE TEMPORARY TABLES", grant.getPrivileges().get(0)); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '"', value = { + "'app'@'%' | app(')@%(')", + "app@'10.0.0.%' | app()@10.0.0.%(')", + "app@localhost | app()@localhost()", + "'app'@localhost | app(')@localhost()", + "`app`@`%` | app(`)@%(`)", + "'app' | app(')", + "app | app()" + }) + void typesAccountGrantees(String account, String expected) throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON db1.t1 TO " + account); + Grantee grantee = grant.getClause().getGranteeItems().get(0); + + assertEquals(Grantee.Kind.ACCOUNT, grantee.getKind()); + assertEquals(expected, account(grantee.getAccount())); + assertEquals(Collections.singletonList(account), grant.getUsers()); + } + + @Test + void typesCurrentUser() throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON db1.* TO CURRENT_USER, CURRENT_USER()"); + List grantees = grant.getClause().getGranteeItems(); + + assertThat(grantees).extracting(Grantee::getKind) + .containsExactly(Grantee.Kind.CURRENT_USER, Grantee.Kind.CURRENT_USER); + assertFalse(grantees.get(0).isCurrentUserParentheses()); + assertTrue(grantees.get(1).isCurrentUserParentheses()); + assertNull(grantees.get(0).getAccount()); + assertEquals(Arrays.asList("CURRENT_USER", "CURRENT_USER()"), grant.getUsers()); + } + + @Test + void roleGrantsHaveTypedRolesAndNoTarget() throws JSQLParserException { + Grant grant = grant("GRANT 'reader', 'writer'@'%' TO 'app'@'%' WITH ADMIN OPTION"); + + assertNull(grant.getTarget()); + assertNull(grant.getPrivilegeItems()); + assertNull(grant.getObjectName()); + assertThat(grant.getClause().getRoleItems()).extracting(MySqlGrantRevokeTest::account) + .containsExactly("reader(')", "writer(')@%(')"); + assertEquals(Arrays.asList("'reader'", "'writer'@'%'"), grant.getRoles()); + assertEquals("'reader'", grant.getRole()); + assertEquals(GrantOption.Kind.ADMIN, grant.getOption().getKind()); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + + Revoke revoke = revoke("REVOKE 'reader' FROM 'app'@'%'"); + assertNull(revoke.getClause().getTarget()); + assertEquals("reader", revoke.getClause().getRoleItems().get(0).getUser().getValue()); + } + + @Test + void proxyTargetsAnAccount() throws JSQLParserException { + Grant grant = grant("GRANT PROXY ON 'a'@'%' TO 'b'@'%' WITH GRANT OPTION"); + + assertThat(grant.getPrivilegeItems()).extracting(Privilege::getKind) + .containsExactly(Privilege.Kind.PROXY); + PrivilegeTarget target = grant.getTarget(); + assertEquals(PrivilegeTarget.Kind.ACCOUNT, target.getKind()); + assertEquals("a(')@%(')", account(target.getAccount().getAccount())); + assertTrue(target.getNames().isEmpty()); + assertNull(grant.getObjectName()); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + + Revoke revoke = revoke("REVOKE PROXY ON CURRENT_USER FROM 'b'@'%'"); + assertEquals(Grantee.Kind.CURRENT_USER, + revoke.getClause().getTarget().getAccount().getKind()); + } + + @Test + void revokesAllPrivilegesAndGrantOptionWithoutTarget() throws JSQLParserException { + Revoke revoke = revoke("REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%', app2"); + PrivilegeClause clause = revoke.getClause(); + + assertThat(clause.getPrivileges()).extracting(Privilege::getKind) + .containsExactly(Privilege.Kind.ALL, Privilege.Kind.GRANT_OPTION); + assertTrue(clause.getPrivileges().get(0).isUsePrivileges()); + assertNull(clause.getTarget()); + assertEquals(Arrays.asList("'app'@'%'", "app2"), clause.getGrantees()); + assertThat(TablesNamesFinder.findTables(revoke.toString())).isEmpty(); + } + + @Test + void revokeIfExistsIgnoreUnknownUser() throws JSQLParserException { + Revoke revoke = + revoke("REVOKE IF EXISTS SELECT ON db1.t1 FROM 'app'@'%' IGNORE UNKNOWN USER"); + assertTrue(revoke.isIfExists()); + assertTrue(revoke.isIgnoreUnknownUser()); + assertThat(TablesNamesFinder.findTables(revoke.toString())).containsExactly("db1.t1"); + + revoke.setIfExists(false); + revoke.setIgnoreUnknownUser(false); + assertEquals("REVOKE SELECT ON db1.t1 FROM 'app'@'%'", revoke.toString()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', quoteCharacter = '"', value = { + "DEFAULT | DEFAULT | \"\"", + "NONE | NONE | \"\"", + "ALL | ALL | \"\"", + "ALL EXCEPT 'r1', r2@'%' | ALL_EXCEPT | 'r1', r2@'%'", + "'r1', r2 | ROLES | 'r1', r2" + }) + void typesAsUserWithRole(String selection, RoleSelection.Kind kind, String roles) + throws JSQLParserException { + Grant grant = grant("GRANT SELECT ON db1.* TO app AS 'admin'@'%' WITH ROLE " + selection); + + assertEquals(Grantee.Kind.ACCOUNT, grant.getAsUser().getKind()); + assertEquals("admin(')@%(')", account(grant.getAsUser().getAccount())); + assertEquals(kind, grant.getWithRole().getKind()); + assertEquals(roles, grant.getWithRole().getRoles().stream().map(Object::toString) + .collect(Collectors.joining(", "))); + } + + @ParameterizedTest + @ValueSource(strings = { + "GRANT SELECT ON db1.t1.* TO app", + "GRANT SELECT ON db1.*, db2.* TO app", + "GRANT USAGE ON SCHEMA * TO app", + "GRANT EXECUTE ON PROCEDURE db1.* TO app", + "GRANT PROXY ON db1.t1 TO app", + "GRANT PROXY, SELECT ON t1 TO app", + "GRANT PROXY ON PUBLIC TO app", + "GRANT PROXY ON 'a'@'%' TO app AS admin", + "GRANT reader TO app AS admin", + "GRANT SELECT ON t1 TO app AS PUBLIC", + "GRANT SELECT ON t1 TO app WITH ROLE r1", + "GRANT SELECT ON t1 TO app WITH GRANT OPTION, GRANT OPTION", + "GRANT SELECT ON t1 TO 'app'@", + "GRANT SELECT ON t1 TO app@", + "GRANT SELECT ON t1 TO PUBLIC()", + "GRANT BACKUP_ADMIN PRIVILEGES ON *.* TO app", + "GRANT ALL PRIVILEGES, GRANT OPTION TO app", + "REVOKE SELECT ON t1 FROM app IGNORE UNKNOWN", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON *.* TO app", + "ALTER DEFAULT PRIVILEGES REVOKE IF EXISTS SELECT ON TABLES FROM app", + "ALTER DEFAULT PRIVILEGES REVOKE SELECT ON TABLES FROM app IGNORE UNKNOWN USER", + "ALTER DEFAULT PRIVILEGES GRANT PROXY ON a TO app", + "ALTER DEFAULT PRIVILEGES REVOKE ALL, GRANT OPTION FROM app" + }) + void rejectsInvalidForms(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + @Test + void buildsTypedGrant() { + PrivilegeTarget target = new PrivilegeTarget(); + target.setWildcard(PrivilegeTarget.Wildcard.DATABASE); + target.getNames().add(Collections.singletonList("db1")); + UserAccount app = new UserAccount().withUser(new StringValue("app")) + .withHost(new StringValue("%")); + Grant grant = new Grant(); + grant.setPrivilegeItems(Arrays.asList(new Privilege(Privilege.Kind.SHOW_VIEW), + Privilege.dynamic("BACKUP_ADMIN"))); + grant.setTarget(target); + grant.getClause().setGranteeItems(Arrays.asList(new Grantee(app), + new Grantee(Grantee.Kind.CURRENT_USER).withCurrentUserParentheses(true))); + grant.setOption(new GrantOption(GrantOption.Kind.GRANT)); + grant.setAsUser(new Grantee(new UserAccount().withUser(new StringValue("admin")))); + grant.setWithRole(new RoleSelection(RoleSelection.Kind.ALL_EXCEPT) + .withRoles(Collections.singletonList( + new UserAccount().withUser(new StringValue("r1"))))); + + assertDeparse(grant, "GRANT SHOW VIEW, BACKUP_ADMIN ON db1.* TO 'app'@'%', " + + "CURRENT_USER() WITH GRANT OPTION AS 'admin' WITH ROLE ALL EXCEPT 'r1'"); + + Revoke revoke = new Revoke(); + revoke.setIfExists(true); + revoke.getClause().setPrivileges(Collections.singletonList( + new Privilege(Privilege.Kind.PROXY))); + PrivilegeTarget proxied = new PrivilegeTarget(); + proxied.setKind(PrivilegeTarget.Kind.ACCOUNT); + proxied.setAccount(new Grantee(app)); + revoke.getClause().setTarget(proxied); + revoke.getClause().setGranteeItems( + Collections.singletonList(new Grantee(Grantee.Kind.CURRENT_USER))); + revoke.setIgnoreUnknownUser(true); + assertDeparse(revoke, + "REVOKE IF EXISTS PROXY ON 'app'@'%' FROM CURRENT_USER IGNORE UNKNOWN USER"); + } + + @Test + void validatesWithMySqlAndMariaDbCapabilities() { + for (String sql : Arrays.asList("GRANT SELECT ON db1.* TO 'app'@'%'", + "GRANT 'reader' TO 'app'@'%'", + "REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app'@'%'", + "REVOKE SELECT ON *.* FROM app")) { + validateNoErrors(sql, 1, DatabaseType.MYSQL, DatabaseType.MARIADB); + } + } +}