diff --git a/.codacy.yml b/.codacy.yml index d98077f4fe..772fed89dd 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,3 +1,8 @@ --- +engines: + tsqllint: + exclude_paths: + # These fixtures use PostgreSQL grammar, not Transact-SQL. + - "src/test/resources/postgresql/**" exclude_paths: - "site/**" 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 bb68fa387a..022ac08ebe 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -637,7 +637,7 @@ public enum Feature { /** * @see Grant */ - grant, + grant, revoke, createRole, alterRole, alterDefaultPrivileges, /** * @see Function */ diff --git a/src/main/java/net/sf/jsqlparser/statement/RoutineReference.java b/src/main/java/net/sf/jsqlparser/statement/RoutineReference.java new file mode 100644 index 0000000000..178a851550 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/RoutineReference.java @@ -0,0 +1,115 @@ +/*- + * #%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; + +import java.util.List; +import java.util.stream.Collectors; +import net.sf.jsqlparser.statement.create.table.ColDataType; +import java.io.Serializable; + +public class RoutineReference implements Serializable { + private String name; + private List arguments; + private boolean allArguments; + private List orderByArguments; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getArguments() { + return arguments; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + public boolean isAllArguments() { + return allArguments; + } + + public void setAllArguments(boolean allArguments) { + this.allArguments = allArguments; + } + + public List getOrderByArguments() { + return orderByArguments; + } + + public void setOrderByArguments(List orderByArguments) { + this.orderByArguments = orderByArguments; + } + + /** A signature argument is a data type, not an invocation expression. */ + public static class Argument implements Serializable { + public enum Mode { + IN, OUT, INOUT, VARIADIC + } + + private Mode mode; + private String name; + private ColDataType dataType; + + public Mode getMode() { + return mode; + } + + public void setMode(Mode mode) { + this.mode = mode; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ColDataType getDataType() { + return dataType; + } + + public void setDataType(ColDataType dataType) { + this.dataType = dataType; + } + + @Override + public String toString() { + return (mode == null ? "" : mode + " ") + (name == null ? "" : name + " ") + dataType; + } + } + + @Override + public String toString() { + if (arguments == null && !allArguments && orderByArguments == null) { + return name; + } + StringBuilder sql = new StringBuilder(name).append('('); + if (allArguments) { + sql.append('*'); + } else if (arguments != null) { + sql.append(arguments.stream().map(Object::toString).collect(Collectors.joining(", "))); + } + if (orderByArguments != null) { + if (arguments != null && !arguments.isEmpty()) { + sql.append(' '); + } + sql.append("ORDER BY ").append(orderByArguments.stream().map(Object::toString) + .collect(Collectors.joining(", "))); + } + return sql.append(')').toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index b083cca6bb..9c9aa9331d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; + import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; @@ -846,4 +852,29 @@ public Void visit(TableFunction tableFunction, S context) { return super.visit(tableFunction, context); } } + + @Override + public Void visit(CreateRole statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterRole statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(Revoke statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterDefaultPrivileges statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateTrigger statement, S context) { + return schemaOnly(); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index fd787910d5..df73392ac0 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,11 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; + import net.sf.jsqlparser.statement.alter.Alter; import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterSystemStatement; @@ -393,4 +398,36 @@ default void visit(CreatePolicy createPolicy) { this.visit(createPolicy, null); } + + default T visit(CreateRole statement, S context) { + return null; + } + + default void visit(CreateRole statement) { + visit(statement, null); + } + + default T visit(AlterRole statement, S context) { + return null; + } + + default void visit(AlterRole statement) { + visit(statement, null); + } + + default T visit(Revoke statement, S context) { + return null; + } + + default void visit(Revoke statement) { + visit(statement, null); + } + + default T visit(AlterDefaultPrivileges statement, S context) { + return null; + } + + default void visit(AlterDefaultPrivileges statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 706fd94665..118537408d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.RoleOption; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; + import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.schema.Column; @@ -351,6 +357,11 @@ public T visit(CreateTable createTable, S context) { @Override public T visit(CreateTrigger createTrigger, S context) { + createTrigger.visit(t -> t.accept(fromItemVisitor, context), + e -> e.accept(expressionVisitor, context)); + if (createTrigger.getBody() != null) { + createTrigger.getBody().accept(this, context); + } return null; } @@ -495,6 +506,8 @@ public T visit(DeclareStatement declareStatement, S context) { @Override public T visit(Grant grant, S context) { + grant.getClause().visit(t -> t.accept(fromItemVisitor, context), + e -> e.accept(expressionVisitor, context)); return null; } @@ -596,4 +609,45 @@ public T visit(Import imprt, S context) { public T visit(Export export, S context) { return null; } + + @Override + public T visit(CreateRole statement, S context) { + for (RoleOption option : statement.getOptions()) { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + } + return null; + } + + @Override + public T visit(AlterRole statement, S context) { + for (RoleOption option : statement.getOptions()) { + if (option.getValue() != null) { + option.getValue().accept(expressionVisitor, context); + } + } + if (statement.getValues() != null) { + statement.getValues().forEach(e -> e.accept(expressionVisitor, context)); + } + return null; + } + + @Override + public T visit(Revoke statement, S context) { + statement.getClause().visit(t -> t.accept(fromItemVisitor, context), + e -> e.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(AlterDefaultPrivileges statement, S context) { + if (statement.getGrant() != null) { + statement.getGrant().accept(this, context); + } + if (statement.getRevoke() != null) { + statement.getRevoke().accept(this, context); + } + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java index 22d7dc0ef4..105115683b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/CreateTrigger.java @@ -9,33 +9,144 @@ */ package net.sf.jsqlparser.statement.create.trigger; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; -/** MySQL {@code CREATE TRIGGER} statement with a structured trigger definition. */ +/** MySQL and PostgreSQL trigger definitions with distinct body and routine-invocation forms. */ public class CreateTrigger implements Statement { public enum Timing { - BEFORE, AFTER + BEFORE, AFTER, INSTEAD_OF } public enum Event { - INSERT, UPDATE, DELETE + INSERT, UPDATE, DELETE, TRUNCATE } public enum Order { FOLLOWS, PRECEDES } + public enum Orientation { + ROW, STATEMENT + } + public enum ExecuteKeyword { + FUNCTION, PROCEDURE + } + + private List events = new ArrayList<>(); private TriggerDefiner definer; private Table trigger; private Timing timing; - private Event event; + private boolean orReplace; + private boolean constraint; + private Table referencedTable; + private Orientation orientation; + private boolean useEach; + private Expression whenExpression; + private ExecuteKeyword executeKeyword = ExecuteKeyword.FUNCTION; + private Function routine; private Table table; private Order order; private Table otherTrigger; private Statement body; + private ConstraintAttributes constraintAttributes = new ConstraintAttributes(); + private List transitionRelations = new ArrayList<>(); + + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean orReplace) { + this.orReplace = orReplace; + } + + public boolean isConstraint() { + return constraint; + } + + public void setConstraint(boolean constraint) { + this.constraint = constraint; + } + + public Table getReferencedTable() { + return referencedTable; + } + + public void setReferencedTable(Table referencedTable) { + this.referencedTable = referencedTable; + } + + public ConstraintAttributes getConstraintAttributes() { + return constraintAttributes; + } + + public void setConstraintAttributes(ConstraintAttributes constraintAttributes) { + this.constraintAttributes = constraintAttributes; + } + + public List getTransitionRelations() { + return transitionRelations; + } + + public void setTransitionRelations(List transitionRelations) { + this.transitionRelations = transitionRelations; + } + + public Orientation getOrientation() { + return orientation; + } + + public void setOrientation(Orientation orientation) { + this.orientation = orientation; + } + + public boolean isUseEach() { + return useEach; + } + + public void setUseEach(boolean useEach) { + this.useEach = useEach; + } + + public Expression getWhenExpression() { + return whenExpression; + } + + public void setWhenExpression(Expression whenExpression) { + this.whenExpression = whenExpression; + } + + public ExecuteKeyword getExecuteKeyword() { + return executeKeyword; + } + + public void setExecuteKeyword(ExecuteKeyword executeKeyword) { + this.executeKeyword = executeKeyword; + } + + public Function getRoutine() { + return routine; + } + + public void setRoutine(Function routine) { + this.routine = routine; + } public TriggerDefiner getDefiner() { return definer; @@ -62,11 +173,14 @@ public void setTiming(Timing timing) { } public Event getEvent() { - return event; + return events.isEmpty() ? null : events.get(0).getEvent(); } public void setEvent(Event event) { - this.event = event; + events = new ArrayList<>(); + if (event != null) { + events.add(new TriggerEvent(event)); + } } public Table getTable() { @@ -106,14 +220,91 @@ public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } + public void visit(Consumer tables, Consumer expressions) { + if (table != null) { + tables.accept(table); + } + if (referencedTable != null) { + tables.accept(referencedTable); + } + for (TriggerEvent triggerEvent : events) { + if (triggerEvent.getColumns() != null) { + triggerEvent.getColumns().forEach(expressions); + } + } + if (whenExpression != null) { + expressions.accept(whenExpression); + } + if (routine != null) { + expressions.accept(routine); + } + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + if (routine == null) { + sql.append(mysqlSql()); + return; + } + appendPostgreSqlHeader(sql, visitor); + appendPostgreSqlTarget(sql); + if (whenExpression != null) { + sql.append(" WHEN ("); + visitor.accept(whenExpression); + sql.append(')'); + } + sql.append(" EXECUTE ").append(executeKeyword).append(' '); + visitor.accept(routine); + } + + private void appendPostgreSqlHeader(StringBuilder sql, Consumer visitor) { + sql.append("CREATE "); + if (orReplace) { + sql.append("OR REPLACE "); + } + if (constraint) { + sql.append("CONSTRAINT "); + } + sql.append("TRIGGER ").append(trigger).append(' ').append(timing.name().replace('_', ' ')) + .append(' '); + for (int i = 0; i < events.size(); i++) { + if (i > 0) { + sql.append(" OR "); + } + events.get(i).appendTo(sql, visitor); + } + } + + private void appendPostgreSqlTarget(StringBuilder sql) { + sql.append(" ON ").append(table); + if (referencedTable != null) { + sql.append(" FROM ").append(referencedTable); + } + constraintAttributes.appendTo(sql); + if (!transitionRelations.isEmpty()) { + sql.append(" REFERENCING"); + for (TransitionRelation relation : transitionRelations) { + sql.append(' ').append(relation); + } + } + if (orientation != null) { + sql.append(" FOR ").append(useEach ? "EACH " : "").append(orientation); + } + } + @Override public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } + + private String mysqlSql() { StringBuilder builder = new StringBuilder("CREATE "); if (definer != null) { builder.append("DEFINER = ").append(definer).append(" "); } builder.append("TRIGGER ").append(trigger).append(" ").append(timing).append(" ") - .append(event).append(" ON ").append(table).append(" FOR EACH ROW "); + .append(getEvent()).append(" ON ").append(table).append(" FOR EACH ROW "); if (order != null) { builder.append(order).append(" ").append(otherTrigger).append(" "); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TransitionRelation.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TransitionRelation.java new file mode 100644 index 0000000000..6f5eb77c5e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TransitionRelation.java @@ -0,0 +1,51 @@ +/*- + * #%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.trigger; + +import java.io.Serializable; + +public class TransitionRelation implements Serializable { + public enum Image { + OLD, NEW + } + + private Image image; + private String name; + private boolean useAs; + + public Image getImage() { + return image; + } + + public void setImage(Image image) { + this.image = image; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isUseAs() { + return useAs; + } + + public void setUseAs(boolean useAs) { + this.useAs = useAs; + } + + @Override + public String toString() { + return image + " TABLE " + (useAs ? "AS " : "") + name; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerEvent.java b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerEvent.java new file mode 100644 index 0000000000..da7c35bff9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/trigger/TriggerEvent.java @@ -0,0 +1,61 @@ +/*- + * #%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.trigger; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; + +public class TriggerEvent implements Serializable { + private CreateTrigger.Event event; + private ExpressionList columns; + + public CreateTrigger.Event getEvent() { + return event; + } + + public void setEvent(CreateTrigger.Event event) { + this.event = event; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + this.columns = columns; + } + + public TriggerEvent(CreateTrigger.Event event) { + this.event = event; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append(event); + if (columns != null) { + sql.append(" OF "); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + sql.append(", "); + } + visitor.accept(columns.get(i)); + } + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/AlterDefaultPrivileges.java b/src/main/java/net/sf/jsqlparser/statement/grant/AlterDefaultPrivileges.java new file mode 100644 index 0000000000..42003ceee8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/AlterDefaultPrivileges.java @@ -0,0 +1,99 @@ +/*- + * #%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.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class AlterDefaultPrivileges implements Statement { + public enum RoleKeyword { + ROLE, USER + } + + private RoleKeyword roleKeyword; + private List roles; + private List schemas; + private Grant grant; + private Revoke revoke; + + public RoleKeyword getRoleKeyword() { + return roleKeyword; + } + + public void setRoleKeyword(RoleKeyword roleKeyword) { + this.roleKeyword = roleKeyword; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getSchemas() { + return schemas; + } + + public void setSchemas(List schemas) { + this.schemas = schemas; + } + + public Grant getGrant() { + return grant; + } + + public void setGrant(Grant grant) { + this.grant = grant; + } + + public Revoke getRevoke() { + return revoke; + } + + public void setRevoke(Revoke revoke) { + this.revoke = revoke; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append("ALTER DEFAULT PRIVILEGES"); + if (roles != null) { + sql.append(" FOR ").append(roleKeyword).append(' ').append(String.join(", ", roles)); + } + if (schemas != null) { + sql.append(" IN SCHEMA ").append(String.join(", ", schemas)); + } + sql.append(' '); + if ((grant == null) == (revoke == null)) { + throw new IllegalStateException("Exactly one GRANT or REVOKE is required"); + } + if (grant != null) { + grant.appendTo(sql, visitor); + } else { + revoke.appendTo(sql, visitor); + } + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} 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 191e66c0c4..a9c43dc267 100644 --- a/src/main/java/net/sf/jsqlparser/statement/grant/Grant.java +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Grant.java @@ -2,106 +2,183 @@ * #%L * JSQLParser library * %% - * Copyright (C) 2004 - 2020 JSQLParser + * 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.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; - -import static java.util.stream.Collectors.joining; - +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; public class Grant implements Statement { + private PrivilegeClause clause = new PrivilegeClause(); + private GrantOption option; - private String role; - private List privileges; - private final List objectName = new ArrayList<>(); - private List users; + public PrivilegeClause getClause() { + return clause; + } - @Override - public T accept(StatementVisitor statementVisitor, S context) { - return statementVisitor.visit(this, context); + public void setClause(PrivilegeClause clause) { + this.clause = clause; + } + + public GrantOption getOption() { + return option; + } + + public void setOption(GrantOption option) { + this.option = option; + } + + public List getPrivilegeItems() { + return clause.getPrivileges(); + } + + public void setPrivilegeItems(List privileges) { + clause.setPrivileges(privileges); + } + + public PrivilegeTarget getTarget() { + return clause.getTarget(); + } + + public void setTarget(PrivilegeTarget target) { + clause.setTarget(target); + } + + public List getRoles() { + return clause.getRoles(); + } + + public void setRoles(List roles) { + clause.setRoles(roles); } public String getRole() { - return role; + return clause.getRoles().isEmpty() ? null : clause.getRoles().get(0); } public void setRole(String role) { - this.role = role; + clause.setRoles(new ArrayList<>()); + if (role != null) { + clause.getRoles().add(role); + clause.setPrivileges(null); + } } + /** Mutable legacy string view over the typed privileges. */ public List getPrivileges() { - return privileges; + 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(); + } + }; } public void setPrivileges(List privileges) { - this.privileges = privileges; + List items = null; + if (privileges != null) { + items = new ArrayList<>(); + for (String privilege : privileges) { + items.add(new Privilege(privilege)); + } + } + clause.setPrivileges(items); } public String getObjectName() { - return objectName.isEmpty() ? null - : objectName.stream() - .map(part -> part == null ? "" : part) - .collect(joining(".")); + if (clause.getTarget().getNames().isEmpty() + || clause.getTarget().getNames().get(0).isEmpty()) { + return null; + } + List parts = clause.getTarget().getNames().get(0); + return parts.stream().map(part -> part == null ? "" : part) + .collect(Collectors.joining(".")); } public void setObjectName(String objectName) { - this.objectName.clear(); - this.objectName.add(objectName); + setObjectName(Collections.singletonList(objectName)); } public void setObjectName(List objectName) { - this.objectName.clear(); - this.objectName.addAll(objectName); + List parts = new ArrayList<>(objectName); + getObjectNameParts().clear(); + getObjectNameParts().addAll(parts); } + /** Mutable parts of the first named target, retained for compatibility. */ public List getObjectNameParts() { - return objectName; + if (clause.getTarget().getNames().isEmpty()) { + clause.getTarget().getNames().add(new ArrayList<>()); + } + return clause.getTarget().getNames().get(0); } public List getUsers() { - return users; + return clause.getGrantees(); } public void setUsers(List users) { - this.users = users; + clause.setGrantees(users); } - @Override - public String toString() { - StringBuilder buffer = new StringBuilder(); - - buffer.append("GRANT "); - if (role != null) { - buffer.append(role); - } else { - for (int i = 0; i < getPrivileges().size(); i++) { - if (i != 0) { - buffer.append(", "); - } - buffer.append(privileges.get(i)); - } - buffer.append(" ON "); - buffer.append(getObjectName()); + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append("GRANT "); + clause.appendTo(sql, false, visitor); + if (option != null) { + sql.append(" WITH ").append(option); } - buffer.append(" TO "); - for (int i = 0; i < getUsers().size(); i++) { - if (i != 0) { - buffer.append(", "); - } - buffer.append(users.get(i)); + if (clause.getGrantedBy() != null) { + sql.append(" GRANTED BY ").append(clause.getGrantedBy()); } - return buffer.toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); } public Grant withRole(String role) { @@ -130,15 +207,18 @@ public Grant withUsers(List users) { } public Grant addPrivileges(String... privileges) { - List collection = Optional.ofNullable(getPrivileges()).orElseGet(ArrayList::new); - Collections.addAll(collection, privileges); - return this.withPrivileges(collection); + return addPrivileges(Arrays.asList(privileges)); } public Grant addPrivileges(Collection privileges) { - List collection = Optional.ofNullable(getPrivileges()).orElseGet(ArrayList::new); - collection.addAll(privileges); - return this.withPrivileges(collection); + List additions = new ArrayList<>(privileges); + if (clause.getPrivileges() == null) { + clause.setPrivileges(new ArrayList<>()); + } + for (String privilege : additions) { + clause.getPrivileges().add(new Privilege(privilege)); + } + return this; } public Grant addUsers(String... users) { diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/GrantOption.java b/src/main/java/net/sf/jsqlparser/statement/grant/GrantOption.java new file mode 100644 index 0000000000..712524378a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/GrantOption.java @@ -0,0 +1,49 @@ +/*- + * #%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; + +public class GrantOption implements Serializable { + public enum Kind { + GRANT, ADMIN, INHERIT, SET + } + public enum Value { + OPTION, TRUE, FALSE + } + + private Kind kind; + private Value value = Value.OPTION; + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public Value getValue() { + return value; + } + + public void setValue(Value value) { + this.value = value; + } + + public GrantOption(Kind kind) { + this.kind = kind; + } + + @Override + public String toString() { + return kind + " " + value; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java b/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java new file mode 100644 index 0000000000..b1f3b461aa --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Privilege.java @@ -0,0 +1,88 @@ +/*- + * #%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.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; + +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 + } + + private String legacyName; + private Kind kind; + private boolean usePrivileges; + private ExpressionList columns; + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public boolean isUsePrivileges() { + return usePrivileges; + } + + public void setUsePrivileges(boolean usePrivileges) { + this.usePrivileges = usePrivileges; + } + + public ExpressionList getColumns() { + return columns; + } + + public void setColumns(ExpressionList columns) { + this.columns = columns; + } + + public Privilege(Kind kind) { + this.kind = kind; + } + + /** Preserves values supplied through the pre-existing string-based Grant API. */ + public Privilege(String name) { + try { + this.kind = Kind.valueOf(name); + } catch (IllegalArgumentException ex) { + this.legacyName = name; + } + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append(kind == null ? legacyName : kind.name().replace('_', ' ')); + if (usePrivileges) { + sql.append(" PRIVILEGES"); + } + if (columns != null) { + sql.append(" ("); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) { + sql.append(", "); + } + visitor.accept(columns.get(i)); + } + sql.append(')'); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java new file mode 100644 index 0000000000..4f517d1229 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeClause.java @@ -0,0 +1,92 @@ +/*- + * #%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.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; + +/** Shared, editable GRANT/REVOKE payload. Null privileges denote role membership. */ +public class PrivilegeClause implements Serializable { + private List privileges; + private List grantees; + private String grantedBy; + private List roles = new ArrayList<>(); + private PrivilegeTarget target = new PrivilegeTarget(); + + public List getPrivileges() { + return privileges; + } + + public void setPrivileges(List privileges) { + this.privileges = privileges; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public PrivilegeTarget getTarget() { + return target; + } + + public void setTarget(PrivilegeTarget target) { + this.target = target; + } + + public List getGrantees() { + return grantees; + } + + public void setGrantees(List grantees) { + this.grantees = grantees; + } + + public String getGrantedBy() { + return grantedBy; + } + + public void setGrantedBy(String grantedBy) { + this.grantedBy = grantedBy; + } + + public void appendTo(StringBuilder sql, boolean revoke, Consumer visitor) { + if (privileges == null) { + sql.append(String.join(", ", roles)); + } else { + for (int i = 0; i < privileges.size(); i++) { + if (i > 0) { + sql.append(", "); + } + privileges.get(i).appendTo(sql, visitor); + } + sql.append(" ON ").append(target); + } + sql.append(revoke ? " FROM " : " TO ").append(String.join(", ", grantees)); + } + + public void visit(Consumer
tables, Consumer expressions) { + if (privileges != 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 new file mode 100644 index 0000000000..a92ac85391 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/PrivilegeTarget.java @@ -0,0 +1,106 @@ +/*- + * #%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.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; + +/** Targets have distinct kinds; a routine signature is not a function invocation. */ +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 + } + + private Kind kind = Kind.TABLE; + private boolean explicitKind; + private boolean allInSchema; + private boolean defaultPrivileges; + private List> names = new ArrayList<>(); + private List routines = new ArrayList<>(); + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public boolean isExplicitKind() { + return explicitKind; + } + + public void setExplicitKind(boolean explicitKind) { + this.explicitKind = explicitKind; + } + + public boolean isAllInSchema() { + return allInSchema; + } + + public void setAllInSchema(boolean allInSchema) { + this.allInSchema = allInSchema; + } + + public boolean isDefaultPrivileges() { + return defaultPrivileges; + } + + public void setDefaultPrivileges(boolean defaultPrivileges) { + this.defaultPrivileges = defaultPrivileges; + } + + public List> getNames() { + return names; + } + + public void setNames(List> names) { + this.names = names; + } + + public List getRoutines() { + return routines; + } + + public void setRoutines(List routines) { + this.routines = routines; + } + + public void visitTables(Consumer
visitor) { + if (kind == Kind.TABLE && !allInSchema && !defaultPrivileges) { + for (List name : names) { + if (!name.isEmpty()) { + visitor.accept(new Table(name)); + } + } + } + } + + @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 (explicitKind ? type + " " : "") + targets; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java b/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java new file mode 100644 index 0000000000..157eccc8a0 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/grant/Revoke.java @@ -0,0 +1,75 @@ +/*- + * #%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.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class Revoke implements Statement { + public enum Behavior { + CASCADE, RESTRICT + } + + private PrivilegeClause clause = new PrivilegeClause(); + private GrantOption.Kind optionFor; + private Behavior behavior; + + public PrivilegeClause getClause() { + return clause; + } + + public void setClause(PrivilegeClause clause) { + this.clause = clause; + } + + public GrantOption.Kind getOptionFor() { + return optionFor; + } + + public void setOptionFor(GrantOption.Kind optionFor) { + this.optionFor = optionFor; + } + + public Behavior getBehavior() { + return behavior; + } + + public void setBehavior(Behavior behavior) { + this.behavior = behavior; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append("REVOKE "); + 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 (behavior != null) { + sql.append(' ').append(behavior); + } + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/role/AlterRole.java b/src/main/java/net/sf/jsqlparser/statement/role/AlterRole.java new file mode 100644 index 0000000000..e2e472b7ae --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/role/AlterRole.java @@ -0,0 +1,203 @@ +/*- + * #%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.role; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class AlterRole implements Statement { + public enum Action { + OPTIONS, RENAME, SET, RESET, ADD_USER, DROP_USER + } + + private CreateRole.Command command = CreateRole.Command.ROLE; + private String name; + private Action action; + private boolean useWith; + private String newName; + private String database; + private String parameter; + private boolean useEquals; + private boolean fromCurrent; + private boolean useDefault; + private ExpressionList values; + private List users; + private List options = new ArrayList<>(); + + public CreateRole.Command getCommand() { + return command; + } + + public void setCommand(CreateRole.Command command) { + this.command = command; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public boolean isUseWith() { + return useWith; + } + + public void setUseWith(boolean useWith) { + this.useWith = useWith; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public String getParameter() { + return parameter; + } + + public void setParameter(String parameter) { + this.parameter = parameter; + } + + public boolean isUseEquals() { + return useEquals; + } + + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + public boolean isFromCurrent() { + return fromCurrent; + } + + public void setFromCurrent(boolean fromCurrent) { + this.fromCurrent = fromCurrent; + } + + public boolean isUseDefault() { + return useDefault; + } + + public void setUseDefault(boolean useDefault) { + this.useDefault = useDefault; + } + + public ExpressionList getValues() { + return values; + } + + public void setValues(ExpressionList values) { + this.values = values; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append("ALTER ").append(command).append(' ').append(name); + if (database != null) { + sql.append(" IN DATABASE ").append(database); + } + switch (action) { + case OPTIONS: + if (useWith) { + sql.append(" WITH"); + } + for (RoleOption option : options) { + sql.append(' '); + option.appendTo(sql, visitor); + } + break; + case RENAME: + sql.append(" RENAME TO ").append(newName); + break; + case RESET: + sql.append(" RESET ").append(parameter); + break; + case SET: + sql.append(" SET ").append(parameter); + if (fromCurrent) { + sql.append(" FROM CURRENT"); + } else { + sql.append(useEquals ? " = " : " TO "); + if (useDefault) { + sql.append("DEFAULT"); + } else { + for (int i = 0; i < values.size(); i++) { + if (i > 0) { + sql.append(", "); + } + visitor.accept(values.get(i)); + } + } + } + break; + case ADD_USER: + case DROP_USER: + sql.append(action == Action.ADD_USER ? " ADD USER " : " DROP USER ") + .append(String.join(", ", users)); + break; + default: + throw new IllegalStateException("Unknown role action: " + action); + } + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/role/CreateRole.java b/src/main/java/net/sf/jsqlparser/statement/role/CreateRole.java new file mode 100644 index 0000000000..79cfa668b7 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/role/CreateRole.java @@ -0,0 +1,83 @@ +/*- + * #%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.role; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class CreateRole implements Statement { + public enum Command { + ROLE, USER, GROUP + } + + private Command command = Command.ROLE; + private String name; + private boolean useWith; + private List options = new ArrayList<>(); + + public Command getCommand() { + return command; + } + + public void setCommand(Command command) { + this.command = command; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isUseWith() { + return useWith; + } + + public void setUseWith(boolean useWith) { + this.useWith = useWith; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + sql.append("CREATE ").append(command).append(' ').append(name); + if (useWith) { + sql.append(" WITH"); + } + for (RoleOption option : options) { + sql.append(' '); + option.appendTo(sql, visitor); + } + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/role/RoleOption.java b/src/main/java/net/sf/jsqlparser/statement/role/RoleOption.java new file mode 100644 index 0000000000..14edc1e80c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/role/RoleOption.java @@ -0,0 +1,84 @@ +/*- + * #%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.role; + +import java.io.Serializable; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** Ordered role attributes; absent attributes are not replaced by server defaults. */ +public class RoleOption implements Serializable { + public enum Kind { + SUPERUSER, NOSUPERUSER, CREATEDB, NOCREATEDB, CREATEROLE, NOCREATEROLE, INHERIT, NOINHERIT, LOGIN, NOLOGIN, REPLICATION, NOREPLICATION, BYPASSRLS, NOBYPASSRLS, CONNECTION_LIMIT, PASSWORD, VALID_UNTIL, IN_ROLE, IN_GROUP, ROLE, USER, ADMIN, SYSID + } + + private Kind kind; + private Expression value; + private boolean encrypted; + private List roles; + + public Kind getKind() { + return kind; + } + + public void setKind(Kind kind) { + this.kind = kind; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + public boolean isEncrypted() { + return encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public RoleOption(Kind kind) { + this.kind = kind; + } + + public void appendTo(StringBuilder sql, Consumer visitor) { + if (encrypted) { + sql.append("ENCRYPTED "); + } + sql.append(kind.name().replace('_', ' ')); + if (value != null) { + sql.append(' '); + visitor.accept(value); + } + if (roles != null) { + sql.append(' ').append(String.join(", ", roles)); + } + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + appendTo(sql, sql::append); + return sql.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 167c54e155..9ad99b6387 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,11 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -1600,7 +1605,7 @@ public void visit(CreateTable createTable) { @Override public Void visit(CreateTrigger createTrigger, S context) { - createTrigger.getTable().accept(this, context); + createTrigger.visit(t -> t.accept(this, context), e -> e.accept(this, context)); if (createTrigger.getBody() != null) { createTrigger.getBody().accept(this, context); } @@ -2049,6 +2054,7 @@ public void visit(DeclareStatement declareStatement) { @Override public Void visit(Grant grant, S context) { + grant.getClause().visit(t -> t.accept(this, context), e -> e.accept(this, context)); return null; } @@ -2497,4 +2503,25 @@ public Void visit(CreatePolicy createPolicy, S context) { public void visit(CreatePolicy createPolicy) { StatementVisitor.super.visit(createPolicy); } + + @Override + public Void visit(CreateRole statement, S context) { + return null; + } + + @Override + public Void visit(AlterRole statement, S context) { + return null; + } + + @Override + public Void visit(Revoke statement, S context) { + statement.getClause().visit(t -> t.accept(this, context), e -> e.accept(this, context)); + return null; + } + + @Override + public Void visit(AlterDefaultPrivileges statement, S context) { + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java index 4a0f997f66..97eda71431 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/GrantDeParser.java @@ -2,47 +2,22 @@ * #%L * JSQLParser library * %% - * Copyright (C) 2004 - 2020 JSQLParser + * Copyright (C) 2004 - 2026 JSQLParser * %% * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 * #L% */ package net.sf.jsqlparser.util.deparser; -import java.util.Iterator; - import net.sf.jsqlparser.statement.grant.Grant; public class GrantDeParser extends AbstractDeParser { - public GrantDeParser(StringBuilder buffer) { super(buffer); } @Override public void deParse(Grant grant) { - builder.append("GRANT "); - if (grant.getRole() != null) { - builder.append(grant.getRole()); - } else { - for (Iterator iter = grant.getPrivileges().iterator(); iter.hasNext();) { - String privilege = iter.next(); - builder.append(privilege); - if (iter.hasNext()) { - builder.append(", "); - } - } - builder.append(" ON "); - builder.append(grant.getObjectName()); - } - builder.append(" TO "); - for (Iterator iter = grant.getUsers().iterator(); iter.hasNext();) { - String user = iter.next(); - builder.append(user); - if (iter.hasNext()) { - builder.append(", "); - } - } + grant.appendTo(builder, builder::append); } - } 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 3069e7d3c5..8ead5c4baa 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,11 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; + import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.stream.Collectors; @@ -140,7 +145,7 @@ public StringBuilder visit(CreateTable createTable, S context) { @Override public StringBuilder visit(CreateTrigger createTrigger, S context) { - builder.append(createTrigger); + createTrigger.appendTo(builder, e -> e.accept(expressionDeParser, context)); return builder; } @@ -436,8 +441,7 @@ public StringBuilder visit(DeclareStatement declareStatement, S context) { @Override public StringBuilder visit(Grant grant, S context) { - GrantDeParser grantDeParser = new GrantDeParser(builder); - grantDeParser.deParse(grant); + grant.appendTo(builder, e -> e.accept(expressionDeParser, context)); return builder; } @@ -561,4 +565,28 @@ public StringBuilder visit(CreatePolicy createPolicy, S context) { new CreatePolicyDeParser(expressionDeParser, builder).deParse(createPolicy); return builder; } + + @Override + public StringBuilder visit(CreateRole statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterRole statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(Revoke statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } + + @Override + public StringBuilder visit(AlterDefaultPrivileges statement, S context) { + statement.appendTo(builder, e -> e.accept(expressionDeParser, context)); + return builder; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index a45dfc6973..3b972ee814 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -135,7 +135,9 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-altertable.html Feature.alterTable, // https://www.postgresql.org/docs/current/using-explain.html Feature.explain, // https://www.postgresql.org/docs/current/sql-grant.html - Feature.grant, // https://www.postgresql.org/docs/current/sql-set.html + Feature.grant, + Feature.revoke, Feature.createRole, Feature.alterRole, + Feature.alterDefaultPrivileges, Feature.createTrigger, // https://www.postgresql.org/docs/current/sql-set.html Feature.set, // https://www.postgresql.org/docs/current/sql-reset.html Feature.reset, // https://www.postgresql.org/docs/current/sql-commit.html Feature.commit)), V11("11", V10.copy().getFeatures()), V12("12", 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 ceb7bd9daa..c37f214ad1 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 @@ -26,11 +26,10 @@ public void validate(Grant grant) { if (isNotEmpty(grant.getUsers())) { grant.getUsers().forEach(u -> validateName(NamedObject.user, u)); } - if (grant.getRole() != null) { - validateName(NamedObject.role, grant.getRole()); - } + grant.getRoles().forEach(role -> validateName(NamedObject.role, role)); - // can't validate grant.getObjectName() - don't know the kind of this object. + grant.getClause().visit(this::validateOptionalFromItem, + this::validateOptionalExpression); } } 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 1857609628..aca9ccb324 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 @@ -9,6 +9,12 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.RoleOption; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; + import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.statement.Block; import net.sf.jsqlparser.statement.Commit; @@ -94,6 +100,11 @@ public Void visit(CreateTable createTable, S context) { @Override public Void visit(CreateTrigger createTrigger, S context) { + if (createTrigger.getRoutine() != null) { + validateFeature(Feature.createTrigger); + } + createTrigger.visit(this::validateOptionalFromItem, + this::validateOptionalExpression); if (createTrigger.getBody() != null) { createTrigger.getBody().accept(this, context); } @@ -659,4 +670,45 @@ public Void visit(CreatePolicy createPolicy, S context) { public void visit(CreatePolicy createPolicy) { visit(createPolicy, null); } + + @Override + public Void visit(CreateRole statement, S context) { + validateFeature(Feature.createRole); + for (RoleOption option : statement.getOptions()) { + validateOptionalExpression(option.getValue()); + } + return null; + } + + @Override + public Void visit(AlterRole statement, S context) { + validateFeature(Feature.alterRole); + for (RoleOption option : statement.getOptions()) { + validateOptionalExpression(option.getValue()); + } + if (statement.getValues() != null) { + statement.getValues().forEach(this::validateOptionalExpression); + } + return null; + } + + @Override + public Void visit(Revoke statement, S context) { + validateFeature(Feature.revoke); + statement.getClause().visit(this::validateOptionalFromItem, + this::validateOptionalExpression); + return null; + } + + @Override + public Void visit(AlterDefaultPrivileges statement, S context) { + validateFeature(Feature.alterDefaultPrivileges); + if (statement.getGrant() != null) { + statement.getGrant().accept(this, context); + } + if (statement.getRevoke() != null) { + statement.getRevoke().accept(this, context); + } + return null; + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index ebb26f4929..48ffa1ebd2 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -73,6 +73,7 @@ import net.sf.jsqlparser.statement.update.*; import net.sf.jsqlparser.statement.upsert.*; import net.sf.jsqlparser.statement.merge.*; import net.sf.jsqlparser.statement.grant.*; +import net.sf.jsqlparser.statement.role.*; import net.sf.jsqlparser.statement.imprt.*; import net.sf.jsqlparser.statement.export.*; import net.sf.jsqlparser.statement.lock.*; @@ -1300,6 +1301,71 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + private static void requireAccessSyntax(boolean condition, String message) throws ParseException { + if (!condition) { throw new ParseException(message); } + } + + private static > E accessEnum(Class type, String value) throws ParseException { + try { return Enum.valueOf(type, value.toUpperCase(Locale.ROOT)); } + catch (IllegalArgumentException ex) { throw new ParseException("Invalid " + type.getSimpleName() + ": " + value); } + } + + private boolean isRoleOptionAhead(int offset) { + if (getToken(offset).kind == EOF) { return false; } + String word = getToken(offset).image.toUpperCase(Locale.ROOT); + return Arrays.asList("SUPERUSER", "NOSUPERUSER", "CREATEDB", "NOCREATEDB", "CREATEROLE", "NOCREATEROLE", + "INHERIT", "NOINHERIT", "LOGIN", "NOLOGIN", "REPLICATION", "NOREPLICATION", "BYPASSRLS", "NOBYPASSRLS", + "CONNECTION", "ENCRYPTED", "PASSWORD", "VALID", "IN", "ROLE", "USER", "ADMIN", "SYSID").contains(word); + } + + private boolean isPostgreSqlRoleAhead() { + if (isAccessKeywordAhead("ROLE") || isAccessKeywordAhead("GROUP")) { return true; } + return isAccessKeywordAhead("USER") && !"MAPPING".equalsIgnoreCase(getToken(2).image) + && (Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) + || "WITH".equalsIgnoreCase(getToken(3).image) || isRoleOptionAhead(3)); + } + + private boolean isAccessKeywordAhead(String word) { + return getToken(1).kind != EOF && isKeywordAhead(word); + } + + private boolean isAccessTargetKindAhead() { + Token next = getToken(2); + return next.kind != EOF && next.kind != K_TO && next.kind != K_FROM + && !",".equals(next.image) && !".".equals(next.image) + && isAccessTargetKindAhead(1); + } + + private boolean isAccessTargetKindAhead(int offset) { + return Arrays.asList("TABLE", "SEQUENCE", "DATABASE", "DOMAIN", "FOREIGN", "FUNCTION", "PROCEDURE", + "ROUTINE", "LANGUAGE", "LARGE", "PARAMETER", "SCHEMA", "TABLESPACE", "TYPE") + .contains(getToken(offset).image.toUpperCase(Locale.ROOT)); + } + + private boolean isObjectPrivilegeAhead() { + int depth = 0; + for (int i = 1; ; i++) { + Token token = getToken(i); + if (token.kind == EOF || ";".equals(token.image)) { return false; } + if ("(".equals(token.image)) { depth++; } + else if (")".equals(token.image)) { depth--; } + else if (depth == 0) { + if (token.kind == K_ON) { return true; } + if (token.kind == K_TO || token.kind == K_FROM) { return false; } + } + } + } + + private boolean isPostgreSqlTriggerAhead() { + if (getToken(1).kind == K_CONSTRAINT) { return true; } + if (getToken(1).kind != K_TRIGGER) { return false; } + for (int i = 2; ; i++) { + Token token = getToken(i); + if (token.kind == EOF || ";".equals(token.image) || token.kind == K_BEGIN) { return false; } + if (token.kind == K_EXECUTE && (getToken(i + 1).kind == K_FUNCTION || getToken(i + 1).kind == K_PROCEDURE)) { return true; } + } + } + private boolean isMySqlDialect() { String dialect = getAsString(Feature.dialect); return Dialect.MYSQL.name().equals(dialect) || Dialect.MARIADB.name().equals(dialect); @@ -1898,6 +1964,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -2561,6 +2628,8 @@ Statement SingleStatement() : | stm = Grant() | + LOOKAHEAD({ isKeywordAhead("REVOKE") }) stm = Revoke(false) + | stm = PurgeStatement() | stm = SessionStatement() @@ -14858,6 +14927,11 @@ Statement Alter(): ( ( + LOOKAHEAD({ isKeywordAhead("ROLE") || isKeywordAhead("USER") || isKeywordAhead("GROUP") }) + statement = AlterRole() + | + statement = AlterDefaultPrivileges() + | statement = AlterEvent() | statement = AlterTable() @@ -15164,36 +15238,363 @@ Comment Comment(): } } -Grant Grant(): +void AccessKeyword(String expected): +{ Token token; } +{ (token= | token=) { requireAccessSyntax(expected.equalsIgnoreCase(token.image), "Expected " + expected); } } + +String AccessQualifiedName(): +{ String name; String part; } +{ name=RelObjectName() ( "." part=RelObjectNameExt() { name += "." + part; } )* { return name; } } + +List AccessNames(): +{ List names = new ArrayList(); String name; } +{ name=RelObjectName() { names.add(name); } ( "," name=RelObjectName() { names.add(name); } )* { return names; } } + +RoleOption RoleOption(boolean create): +{ RoleOption option = null; Token token; Token value; String sign = ""; Expression expression; List names; boolean encrypted = false; } { - Grant grant = new Grant(); - ArrayList privileges = new ArrayList(); - List users; - Token tk = null; - ObjectNames objName; + ( + LOOKAHEAD({ isAccessKeywordAhead("CONNECTION") }) + AccessKeyword("CONNECTION") [ ( "+" | "-" { sign = "-"; } ) ] value= + { option = new RoleOption(RoleOption.Kind.CONNECTION_LIMIT); option.setValue(new LongValue(sign + value.image)); } + | + LOOKAHEAD({ isAccessKeywordAhead("PASSWORD") || isAccessKeywordAhead("ENCRYPTED") }) + [ AccessKeyword("ENCRYPTED") { encrypted = true; } ] + ( value= { expression = new StringValue(value.image); } | { expression = new NullValue(); } ) + { option = new RoleOption(RoleOption.Kind.PASSWORD); option.setEncrypted(encrypted); option.setValue(expression); } + | + LOOKAHEAD({ isAccessKeywordAhead("VALID") }) + AccessKeyword("VALID") AccessKeyword("UNTIL") value= + { option = new RoleOption(RoleOption.Kind.VALID_UNTIL); option.setValue(new StringValue(value.image)); } + | + + ( AccessKeyword("ROLE") { option = new RoleOption(RoleOption.Kind.IN_ROLE); } + | { option = new RoleOption(RoleOption.Kind.IN_GROUP); } ) + names=AccessNames() + { requireAccessSyntax(create, "Membership attributes require CREATE ROLE"); option.setRoles(names); } + | + LOOKAHEAD({ isAccessKeywordAhead("ROLE") || isAccessKeywordAhead("USER") || isAccessKeywordAhead("ADMIN") }) + ( token= | token= ) + names=AccessNames() + { requireAccessSyntax(create, "Membership attributes require CREATE ROLE"); option = new RoleOption(accessEnum(RoleOption.Kind.class, token.image)); option.setRoles(names); } + | + LOOKAHEAD({ isAccessKeywordAhead("SYSID") }) + AccessKeyword("SYSID") value= + { requireAccessSyntax(create, "SYSID requires CREATE ROLE"); option = new RoleOption(RoleOption.Kind.SYSID); option.setValue(new LongValue(value.image)); } + | + token= { option = new RoleOption(accessEnum(RoleOption.Kind.class, token.image)); } + ) + { return option; } } + +CreateRole CreateRole(): +{ CreateRole role = new CreateRole(); String name; RoleOption option; Token token; } { - - ( - ( - [readGrantTypes(privileges) ( readGrantTypes(privileges))*] - objName=RelObjectNames() { grant.setObjectName(objName.getNames()); } - ) + ( token= | token= | token= ) { role.setCommand(accessEnum(CreateRole.Command.class, token.image)); } + name=RelObjectName() { role.setName(name); } + [ LOOKAHEAD(1) { role.setUseWith(true); } ] + ( LOOKAHEAD({ isRoleOptionAhead(1) }) option=RoleOption(true) { role.getOptions().add(option); } )* + { requireAccessSyntax(!role.isUseWith() || !role.getOptions().isEmpty(), "WITH requires role attributes"); return role; } +} + +AlterRole AlterRole(): +{ + AlterRole role = new AlterRole(); String name; RoleOption option; Token token; Expression value; + ExpressionList values = new ExpressionList(); List names; +} +{ + ( token= | token= | token= ) { role.setCommand(accessEnum(CreateRole.Command.class, token.image)); } + ( LOOKAHEAD(1) { role.setName("ALL"); } | name=RelObjectName() { role.setName(name); } ) + [ LOOKAHEAD(2) name=RelObjectName() { role.setDatabase(name); } ] + ( + name=RelObjectName() { role.setAction(AlterRole.Action.RENAME); role.setNewName(name); } + | + ( { role.setAction(AlterRole.Action.ADD_USER); } | { role.setAction(AlterRole.Action.DROP_USER); } ) + names=AccessNames() { requireAccessSyntax(role.getCommand() == CreateRole.Command.GROUP, "ADD/DROP USER requires ALTER GROUP"); role.setUsers(names); } | + name=AccessQualifiedName() { role.setAction(AlterRole.Action.SET); role.setParameter(name); } ( - tk= { grant.setRole(tk.image); } + { role.setFromCurrent(true); } + | + ( | "=" { role.setUseEquals(true); } ) + ( + LOOKAHEAD(1) { role.setUseDefault(true); } + | + value=AccessSettingValue() { values.add(value); } + ( "," value=AccessSettingValue() { values.add(value); } )* + { role.setValues(values); } + ) ) + | + ( LOOKAHEAD(1) { role.setParameter("ALL"); } | name=AccessQualifiedName() { role.setParameter(name); } ) + { role.setAction(AlterRole.Action.RESET); } + | + [ { role.setUseWith(true); } ] + option=RoleOption(false) { role.getOptions().add(option); role.setAction(AlterRole.Action.OPTIONS); } + ( LOOKAHEAD({ isRoleOptionAhead(1) }) option=RoleOption(false) { role.getOptions().add(option); } )* ) - users = UsersList() - { - grant.setUsers(users); - if(privileges.size() > 0) { - grant.setPrivileges(privileges); + { + requireAccessSyntax(role.getDatabase() == null && !"ALL".equals(role.getName()) + || role.getAction() == AlterRole.Action.SET || role.getAction() == AlterRole.Action.RESET, + "ALL and IN DATABASE require SET or RESET"); + requireAccessSyntax(role.getCommand() != CreateRole.Command.GROUP || role.getAction() == AlterRole.Action.RENAME + || role.getAction() == AlterRole.Action.ADD_USER || role.getAction() == AlterRole.Action.DROP_USER, "Invalid ALTER GROUP action"); + return role; + } +} + +Expression AccessSettingValue(): +{ Expression value; Token token; } +{ ( LOOKAHEAD(1) token= { value = new Column(token.image); } | value=PrimaryExpression() ) { return value; } } + +Privilege AccessPrivilege(): +{ Privilege result; Token token; boolean all = false; 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); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("PRIVILEGES") }) + AccessKeyword("PRIVILEGES") { requireAccessSyntax(result.getKind() == Privilege.Kind.ALL, "PRIVILEGES requires ALL"); result.setUsePrivileges(true); } ] + [ "(" { columns = new ExpressionList(); } + column=RelObjectName() { columns.add(new Column(column)); } + ( "," column=RelObjectName() { columns.add(new Column(column)); } )* + ")" { result.setColumns(columns); } ] + { return result; } +} + +PrivilegeTarget.Kind AccessObjectKind(boolean plural): +{ Token token; String name; } +{ + ( + + ( AccessKeyword("WRAPPER") { name = "FOREIGN_DATA_WRAPPER"; } + | AccessKeyword("SERVER") { name = "FOREIGN_SERVER"; } ) + | + LOOKAHEAD({ isAccessKeywordAhead("LARGE") }) + AccessKeyword("LARGE") ( token= ) { name = "LARGE_" + token.image.toUpperCase(Locale.ROOT); } + | + ( token= | token= | token= | token= | token= | token= | token= | token= | token= | token= | token= ) + { name = token.image.toUpperCase(Locale.ROOT); } + ) + { + if (plural) { + requireAccessSyntax(name.endsWith("S"), "Expected plural privilege target"); + name = name.substring(0, name.length() - 1); } - return grant; + return accessEnum(PrivilegeTarget.Kind.class, name); + } +} + +RoutineReference.Argument AccessRoutineArgument(): +{ RoutineReference.Argument result = new RoutineReference.Argument(); String name; Token mode; ColDataType type; } +{ + [ + LOOKAHEAD({ isAccessKeywordAhead("IN") || isAccessKeywordAhead("OUT") || isAccessKeywordAhead("INOUT") || isAccessKeywordAhead("VARIADIC") }) + ( { result.setMode(RoutineReference.Argument.Mode.IN); } + | mode= { result.setMode(accessEnum(RoutineReference.Argument.Mode.class, mode.image)); } ) + ] + ( + LOOKAHEAD(ColDataType() ("," | ")")) type=ColDataType() + | name=RelObjectName() { result.setName(name); } type=ColDataType() + ) + { result.setDataType(type); return result; } +} + +RoutineReference AccessRoutine(): +{ RoutineReference result = new RoutineReference(); String name; RoutineReference.Argument argument; List arguments; } +{ + name=AccessQualifiedName() { result.setName(name); } + [ LOOKAHEAD(1) "(" { arguments = new ArrayList(); result.setArguments(arguments); } + [ argument=AccessRoutineArgument() { arguments.add(argument); } + ( "," argument=AccessRoutineArgument() { arguments.add(argument); } )* ] ")" ] + { return result; } +} + +PrivilegeTarget AccessTarget(boolean defaults): +{ PrivilegeTarget target = new PrivilegeTarget(); PrivilegeTarget.Kind kind; ObjectNames name; RoutineReference routine; Token number; } +{ + ( + LOOKAHEAD({ defaults }) + kind=AccessObjectKind(true) + { requireAccessSyntax(defaults && EnumSet.of(PrivilegeTarget.Kind.TABLE, PrivilegeTarget.Kind.SEQUENCE, PrivilegeTarget.Kind.FUNCTION, + PrivilegeTarget.Kind.ROUTINE, PrivilegeTarget.Kind.TYPE, PrivilegeTarget.Kind.SCHEMA, PrivilegeTarget.Kind.LARGE_OBJECT).contains(kind), + "Invalid default-privilege target"); + target.setKind(kind); target.setDefaultPrivileges(true); } + | + LOOKAHEAD(1) kind=AccessObjectKind(true) + { requireAccessSyntax(!defaults && EnumSet.of(PrivilegeTarget.Kind.TABLE, PrivilegeTarget.Kind.SEQUENCE, + PrivilegeTarget.Kind.FUNCTION, PrivilegeTarget.Kind.PROCEDURE, PrivilegeTarget.Kind.ROUTINE).contains(kind), + "Invalid ALL IN SCHEMA target"); target.setKind(kind); target.setAllInSchema(true); } + name=RelObjectNames() { target.getNames().add(name.getNames()); } + ( "," name=RelObjectNames() { target.getNames().add(name.getNames()); } )* + | + LOOKAHEAD({ isAccessTargetKindAhead() && (isAccessKeywordAhead("FUNCTION") || isAccessKeywordAhead("PROCEDURE") || isAccessKeywordAhead("ROUTINE")) }) + kind=AccessObjectKind(false) { target.setKind(kind); target.setExplicitKind(true); } + routine=AccessRoutine() { target.getRoutines().add(routine); } + ( "," routine=AccessRoutine() { target.getRoutines().add(routine); } )* + | + LOOKAHEAD({ isAccessTargetKindAhead() && isAccessKeywordAhead("LARGE") }) + kind=AccessObjectKind(false) { target.setKind(kind); target.setExplicitKind(true); } + number= { target.getNames().add(Collections.singletonList(number.image)); } + ( "," 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()); } )* + ) + { 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; } } + +List AccessGrantees(): +{ List names = new ArrayList(); String name; } +{ name=AccessGrantee() { names.add(name); } ( "," name=AccessGrantee() { names.add(name); } )* { return names; } } + +PrivilegeClause AccessClause(boolean revoke, boolean defaults): +{ PrivilegeClause clause = new PrivilegeClause(); Privilege privilege; PrivilegeTarget target; List names; } +{ + ( + 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); } + ) + ( { requireAccessSyntax(!revoke, "REVOKE requires FROM"); } | { requireAccessSyntax(revoke, "GRANT requires TO"); } ) + names=AccessGrantees() { clause.setGrantees(names); return clause; } +} + +Grant Grant(): +{ Grant grant; } +{ grant=AccessGrant(false) { return grant; } } + +Grant AccessGrant(boolean defaults): +{ Grant grant = new Grant(); PrivilegeClause clause; Token token; GrantOption option; String name; } +{ + 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"); } + ] + [ LOOKAHEAD({ isAccessKeywordAhead("GRANTED") }) AccessKeyword("GRANTED") name=RelObjectName() + { requireAccessSyntax(!defaults, "GRANTED BY is not part of default privileges"); clause.setGrantedBy(name); } ] + { return grant; } +} + +Revoke Revoke(boolean defaults): +{ Revoke revoke = new Revoke(); PrivilegeClause clause; Token token; String name; } +{ + + [ 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); } ] + [ ( { revoke.setBehavior(Revoke.Behavior.CASCADE); } | { revoke.setBehavior(Revoke.Behavior.RESTRICT); } ) ] + { requireAccessSyntax(revoke.getOptionFor() == null || (clause.getPrivileges() == null + ? revoke.getOptionFor() != GrantOption.Kind.GRANT : revoke.getOptionFor() == GrantOption.Kind.GRANT), "Invalid REVOKE option"); + return revoke; } +} + +AlterDefaultPrivileges AlterDefaultPrivileges(): +{ AlterDefaultPrivileges result = new AlterDefaultPrivileges(); Token token; List names; Grant grant; Revoke revoke; } +{ + AccessKeyword("PRIVILEGES") + [ ( token= | token= ) { result.setRoleKeyword(accessEnum(AlterDefaultPrivileges.RoleKeyword.class, token.image)); } + names=AccessNames() { result.setRoles(names); } ] + [ names=AccessNames() { result.setSchemas(names); } ] + ( grant=AccessGrant(true) { result.setGrant(grant); } + | revoke=Revoke(true) { result.setRevoke(revoke); } ) + { return result; } +} + +TriggerEvent PostgreSqlTriggerEvent(): +{ TriggerEvent result = null; String name; ExpressionList columns; } +{ + ( + { result = new TriggerEvent(CreateTrigger.Event.INSERT); } + | { result = new TriggerEvent(CreateTrigger.Event.DELETE); } + | { result = new TriggerEvent(CreateTrigger.Event.TRUNCATE); } + | { result = new TriggerEvent(CreateTrigger.Event.UPDATE); } + [ { columns = new ExpressionList(); } + name=RelObjectName() { columns.add(new Column(name)); } + ( "," name=RelObjectName() { columns.add(new Column(name)); } )* + { result.setColumns(columns); } ] + ) + { return result; } +} + +TransitionRelation PostgreSqlTransitionRelation(): +{ TransitionRelation result = new TransitionRelation(); Token token; String name; } +{ + ( token= ) { result.setImage(accessEnum(TransitionRelation.Image.class, token.image)); } + [ { result.setUseAs(true); } ] name=RelObjectName() { result.setName(name); return result; } +} + +CreateTrigger PostgreSqlTrigger(boolean replace): +{ + CreateTrigger result = new CreateTrigger(); String name; Table table; CreateTrigger.Timing timing; + TriggerEvent event; TransitionRelation transition; Expression expression; Token token; boolean negative = false; + Function routine = new Function(); ExpressionList arguments = new ExpressionList(); +} +{ + [ { result.setConstraint(true); } ] + name=RelObjectName() { result.setTrigger(new Table(name)); result.setOrReplace(replace); } + ( + LOOKAHEAD({ isAccessKeywordAhead("INSTEAD") }) AccessKeyword("INSTEAD") { result.setTiming(CreateTrigger.Timing.INSTEAD_OF); } + | timing=MySqlTriggerTiming() { result.setTiming(timing); } + ) + event=PostgreSqlTriggerEvent() { result.getEvents().add(event); } + ( event=PostgreSqlTriggerEvent() { result.getEvents().add(event); } )* + table=Table() { result.setTable(table); } + [ table=Table() { result.setReferencedTable(table); } ] + [ [ { negative = true; } ] { result.getConstraintAttributes().setDeferrable(!negative); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("INITIALLY") }) + AccessKeyword("INITIALLY") ( token= ) + { result.getConstraintAttributes().setInitially(accessEnum(ConstraintAttributes.Initially.class, token.image)); } ] + [ LOOKAHEAD({ isAccessKeywordAhead("REFERENCING") }) AccessKeyword("REFERENCING") + transition=PostgreSqlTransitionRelation() { result.getTransitionRelations().add(transition); } + ( LOOKAHEAD({ isAccessKeywordAhead("OLD") || isAccessKeywordAhead("NEW") }) + transition=PostgreSqlTransitionRelation() { result.getTransitionRelations().add(transition); } )* ] + [ [ LOOKAHEAD({ isAccessKeywordAhead("EACH") }) AccessKeyword("EACH") { result.setUseEach(true); } ] + ( { result.setOrientation(CreateTrigger.Orientation.ROW); } + | { result.setOrientation(CreateTrigger.Orientation.STATEMENT); } ) ] + [ "(" expression=Expression() ")" { result.setWhenExpression(expression); } ] + ( | { result.setExecuteKeyword(CreateTrigger.ExecuteKeyword.PROCEDURE); } ) + name=AccessQualifiedName() { routine.setName(name); } + "(" [ expression=PostgreSqlTriggerArgument() { arguments.add(expression); } + ( "," expression=PostgreSqlTriggerArgument() { arguments.add(expression); } )* ] ")" + { + routine.setParameters(arguments); result.setRoutine(routine); + requireAccessSyntax(!result.isConstraint() || !replace && result.getTiming() == CreateTrigger.Timing.AFTER + && result.getOrientation() == CreateTrigger.Orientation.ROW, "A constraint trigger requires AFTER FOR EACH ROW without OR REPLACE"); + requireAccessSyntax(result.isConstraint() || result.getReferencedTable() == null + && result.getConstraintAttributes().getDeferrable() == null && result.getConstraintAttributes().getInitially() == null, + "FROM and deferrability require a constraint trigger"); + return result; } } +Expression PostgreSqlTriggerArgument(): +{ Expression result; Token token; String name; } +{ + ( + token= { result = new StringValue(token.image); } + | token= { result = new LongValue(token.image); } + | token= { result = new DoubleValue(token.image); } + | name=RelObjectName() { result = new Column(name); } + ) + { return result; } +} + List UsersList(): { List users = new ArrayList(); @@ -15348,6 +15749,11 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() + { requireAccessSyntax(!isUsingOrReplace, "CREATE ROLE does not accept OR REPLACE"); } + | + LOOKAHEAD({ isPostgreSqlTriggerAhead() }) statement = PostgreSqlTrigger(isUsingOrReplace) + | statement = CreateUser() | statement = CreateEvent() diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 3d45609df8..1502c612ef 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -748,3 +748,51 @@ You need ``JDK 8`` or ``JDK 11``. JSQLParser-4.9 is the last ``JDK 8`` compatibl git clone --depth 1 https://github.com/JSQLParser/JSqlParser.git cd JSqlParser gradle publishToMavenLocal + +PostgreSQL roles, privileges and triggers +----------------------------------------- + +``CreateRole`` and ``AlterRole`` model role attributes and configuration changes, +including PostgreSQL's USER/GROUP aliases. Role options are ordered and typed; +passwords and configuration values are expressions. Omitted options remain +omitted. ``CREATE USER name`` without attributes retains the existing MySQL +``CreateUser`` AST by default. Select ``Dialect.POSTGRESQL`` explicitly to obtain +the PostgreSQL ``CreateRole`` AST for this ambiguous form:: + + CreateRole user = (CreateRole) CCJSqlParserUtil.parse( + "CREATE USER app", + parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)); + +``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. +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 ``()``. + +``Revoke`` shares the privilege payload and adds the revoked option and +CASCADE/RESTRICT behavior. ``AlterDefaultPrivileges`` has separate role/schema +scope and one nested GRANT or REVOKE. Table-name discovery reports explicit +table targets, not schema-wide targets, routine/type names or role names. + +``CreateTrigger`` supports both its existing MySQL statement body and a distinct +PostgreSQL routine invocation. PostgreSQL fields include multiple events, +UPDATE OF columns, constraint attributes, transition relations, row/statement +orientation and a WHEN expression. The absence of FOR ROW/STATEMENT is retained. +Expression visitors and ``StatementDeParser`` traverse privilege columns, +role values, trigger conditions and invocation arguments. Declaring a trigger +is classified as a schema change, not execution of its body. + +Capability validation covers these statement families, not every server-version +or catalog-dependent restriction. EVENT TRIGGER is outside this support. +Serialized role SQL can contain passwords; avoid logging real credentials. + +References: `CREATE ROLE `_, +`ALTER ROLE `_, +`GRANT `_, +`REVOKE `_, +`ALTER DEFAULT PRIVILEGES `_, +`CREATE TRIGGER `_. diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessDdlTest.java new file mode 100644 index 0000000000..6b8b1e8fb0 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessDdlTest.java @@ -0,0 +1,289 @@ +/*- + * #%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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.util.validation.ValidationTestAsserts.validateNoErrors; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.RoutineReference; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.create.table.ConstraintAttributes; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.trigger.TransitionRelation; +import net.sf.jsqlparser.statement.create.trigger.TriggerEvent; +import net.sf.jsqlparser.statement.create.user.CreateUser; +import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges; +import net.sf.jsqlparser.statement.grant.Grant; +import net.sf.jsqlparser.statement.grant.GrantOption; +import net.sf.jsqlparser.statement.grant.Privilege; +import net.sf.jsqlparser.statement.grant.PrivilegeTarget; +import net.sf.jsqlparser.statement.grant.Revoke; +import net.sf.jsqlparser.statement.role.AlterRole; +import net.sf.jsqlparser.statement.role.CreateRole; +import net.sf.jsqlparser.statement.role.RoleOption; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +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.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlAccessDdlTest { + static Stream statements() throws Exception { + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + PostgreSqlAccessDdlTest.class.getResourceAsStream("/postgresql/access-ddl.sql"), + StandardCharsets.UTF_8))) { + return reader.lines() + .filter(line -> !line.trim().isEmpty() && !line.trim().startsWith("--")) + .collect(Collectors.toList()).stream(); + } + } + + @ParameterizedTest + @MethodSource("statements") + void testRoundTripAndValidation(String sql) throws Exception { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql); + assertThat(statement).isInstanceOfAny(CreateRole.class, AlterRole.class, Grant.class, + Revoke.class, AlterDefaultPrivileges.class, CreateTrigger.class); + assertThat(CCJSqlParserUtil.parse(statement.toString()).toString()) + .isEqualTo(statement.toString()); + assertThat(statement.getFeatures().getCertain()) + .containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(statement.getFeatures().getUncertain()) + .doesNotContain(StmtFeature.MODIFIES_DATA); + validateNoErrors(sql, 1, DatabaseType.POSTGRESQL); + } + + @Test + void testRoleOptionsAndAliases() throws Exception { + CreateRole role = (CreateRole) CCJSqlParserUtil + .parse("CREATE ROLE app WITH REPLICATION LOGIN CONNECTION LIMIT -1 PASSWORD NULL"); + assertThat(role.getCommand()).isEqualTo(CreateRole.Command.ROLE); + assertThat(role.isUseWith()).isTrue(); + assertThat(role.getOptions()).extracting(RoleOption::getKind).containsExactly( + RoleOption.Kind.REPLICATION, RoleOption.Kind.LOGIN, + RoleOption.Kind.CONNECTION_LIMIT, RoleOption.Kind.PASSWORD); + assertThat(((LongValue) role.getOptions().get(2).getValue()).getValue()).isEqualTo(-1); + assertThat(((CreateRole) CCJSqlParserUtil.parse("CREATE ROLE app")).getOptions()).isEmpty(); + assertThat(CCJSqlParserUtil.parse("CREATE USER app")).isInstanceOf(CreateUser.class); + CreateRole user = (CreateRole) CCJSqlParserUtil.parse("CREATE USER app", + p -> p.withDialect(Dialect.POSTGRESQL)); + assertThat(user.getCommand()).isEqualTo(CreateRole.Command.USER); + assertThat(user.getOptions()).isEmpty(); + assertThat(CCJSqlParserUtil + .parse("CREATE USER 'app'@'localhost' IDENTIFIED BY 'example-only'")) + .isInstanceOf(CreateUser.class); + assertThat(TablesNamesFinder.findTables(role.toString())).isEmpty(); + } + + @Test + void testAlterRoleSettings() throws Exception { + AlterRole role = (AlterRole) CCJSqlParserUtil + .parse("ALTER ROLE ALL IN DATABASE appdb SET search_path TO app, public"); + assertThat(role.getAction()).isEqualTo(AlterRole.Action.SET); + assertThat(role.getName()).isEqualTo("ALL"); + assertThat(role.getDatabase()).isEqualTo("appdb"); + assertThat(role.getValues()).hasSize(2); + role.getValues().set(0, new Column("new_schema")); + assertThat(role.toString()).endsWith("TO new_schema, public"); + assertThat(TablesNamesFinder.findTables(role.toString())).isEmpty(); + } + + @Test + void testTypedPrivilegesAndLegacyMutation() throws Exception { + Grant grant = (Grant) CCJSqlParserUtil.parse( + "GRANT SELECT (id), UPDATE (name) ON TABLE app.users, app.orders TO app WITH GRANT OPTION"); + assertThat(grant.getPrivilegeItems().get(0).getKind()).isEqualTo(Privilege.Kind.SELECT); + assertThat(grant.getPrivilegeItems().get(0).getColumns().get(0).getColumnName()) + .isEqualTo("id"); + assertThat(grant.getTarget().getKind()).isEqualTo(PrivilegeTarget.Kind.TABLE); + assertThat(grant.getTarget().getNames()).hasSize(2); + assertThat(grant.getOption().getKind()).isEqualTo(GrantOption.Kind.GRANT); + grant.addPrivileges("INSERT"); + assertThat(grant.getPrivilegeItems().get(0).getColumns()).hasSize(1); + grant.getPrivileges().set(1, "DELETE"); + grant.getUsers().add("reader"); + grant.getObjectNameParts().set(1, "new_users"); + assertThat(grant.getPrivilegeItems().get(1).getKind()).isEqualTo(Privilege.Kind.DELETE); + assertThat(grant.toString()).contains("app.new_users, app.orders TO app, reader"); + assertThat(TablesNamesFinder.findTables(grant.toString())) + .containsExactlyInAnyOrder("app.new_users", "app.orders"); + Grant created = new Grant().addPrivileges("SELECT") + .withObjectName(Arrays.asList("app", "users")).addUsers("app"); + assertThat(created.toString()).isEqualTo("GRANT SELECT ON app.users TO app"); + assertThat(created.getTarget().getNames()).containsExactly(Arrays.asList("app", "users")); + created.setObjectName(Arrays.asList("catalog", null, "users")); + assertThat(created.toString()).isEqualTo("GRANT SELECT ON catalog..users TO app"); + } + + @Test + void testRoutineSignaturesAndNonRelationTargets() throws Exception { + Grant grant = (Grant) CCJSqlParserUtil + .parse("GRANT EXECUTE ON FUNCTION app.f(IN arg integer), app.g(), app.h TO app"); + List routines = grant.getTarget().getRoutines(); + assertThat(routines).hasSize(3); + assertThat(routines.get(0).getArguments().get(0).getMode()) + .isEqualTo(RoutineReference.Argument.Mode.IN); + assertThat(routines.get(0).getArguments().get(0).getName()).isEqualTo("arg"); + assertThat(routines.get(1).getArguments()).isEmpty(); + assertThat(routines.get(2).getArguments()).isNull(); + assertThat(TablesNamesFinder.findTables(grant.toString())).isEmpty(); + assertThat( + TablesNamesFinder.findTables("GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader")) + .isEmpty(); + assertThat(TablesNamesFinder.findTables("GRANT USAGE ON SCHEMA app TO reader")).isEmpty(); + } + + @Test + void testRevokeAndDefaults() throws Exception { + Revoke revoke = (Revoke) CCJSqlParserUtil + .parse("REVOKE ADMIN OPTION FOR reader, writer FROM app GRANTED BY owner CASCADE"); + assertThat(revoke.getOptionFor()).isEqualTo(GrantOption.Kind.ADMIN); + assertThat(revoke.getClause().getRoles()).containsExactly("reader", "writer"); + assertThat(revoke.getClause().getGrantedBy()).isEqualTo("owner"); + assertThat(revoke.getBehavior()).isEqualTo(Revoke.Behavior.CASCADE); + AlterDefaultPrivileges defaults = (AlterDefaultPrivileges) CCJSqlParserUtil.parse( + "ALTER DEFAULT PRIVILEGES FOR USER owner IN SCHEMA app GRANT SELECT ON TABLES TO reader"); + assertThat(defaults.getRoleKeyword()).isEqualTo(AlterDefaultPrivileges.RoleKeyword.USER); + assertThat(defaults.getGrant().getTarget().isDefaultPrivileges()).isTrue(); + assertThat(defaults.getSchemas()).containsExactly("app"); + assertThat(TablesNamesFinder.findTables(defaults.toString())).isEmpty(); + } + + @Test + void testTriggerFields() throws Exception { + CreateTrigger trigger = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE CONSTRAINT TRIGGER check_parent AFTER INSERT OR UPDATE ON child FROM parent DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN (NEW.id > 0) EXECUTE PROCEDURE app.check_parent('id')"); + assertThat(trigger.getEvent()).isEqualTo(CreateTrigger.Event.INSERT); + assertThat(trigger.getEvents()).extracting(TriggerEvent::getEvent) + .containsExactly(CreateTrigger.Event.INSERT, CreateTrigger.Event.UPDATE); + assertThat(trigger.getConstraintAttributes().getInitially()) + .isEqualTo(ConstraintAttributes.Initially.DEFERRED); + assertThat(trigger.getBody()).isNull(); + assertThat(trigger.getRoutine().getName()).isEqualTo("app.check_parent"); + assertThat(trigger.getExecuteKeyword()).isEqualTo(CreateTrigger.ExecuteKeyword.PROCEDURE); + assertThat(TablesNamesFinder.findTables(trigger.toString())) + .containsExactlyInAnyOrder("child", "parent"); + CreateTrigger transition = (CreateTrigger) CCJSqlParserUtil.parse( + "CREATE TRIGGER audit AFTER INSERT ON users REFERENCING NEW TABLE AS inserted EXECUTE FUNCTION audit_all()"); + assertThat(transition.getOrientation()).isNull(); + assertThat(transition.getTransitionRelations().get(0).getImage()) + .isEqualTo(TransitionRelation.Image.NEW); + assertThat(transition.getTransitionRelations().get(0).isUseAs()).isTrue(); + assertThat(TablesNamesFinder.findTables(transition.toString())).containsExactly("users"); + } + + @Test + void testVisitorsAndDeparser() throws Exception { + Statement trigger = CCJSqlParserUtil.parse( + "CREATE TRIGGER audit BEFORE UPDATE OF id ON users FOR EACH ROW WHEN (NEW.id > 0) EXECUTE FUNCTION audit_row('example')"); + List columns = new ArrayList<>(); + List strings = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(Column column, S context) { + assertThat(context).isEqualTo("access"); + columns.add(column.getColumnName()); + return null; + } + + @Override + public Void visit(StringValue value, S context) { + assertThat(context).isEqualTo("access"); + strings.add(value.getValue()); + return null; + } + }; + StatementVisitorAdapter visitor = + new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)); + trigger.accept(visitor, "access"); + CCJSqlParserUtil.parse("GRANT SELECT (name) ON users TO app").accept(visitor, "access"); + CCJSqlParserUtil.parse("CREATE ROLE app PASSWORD 'example-only'").accept(visitor, "access"); + assertThat(columns).containsExactly("id", "id", "name"); + assertThat(strings).containsExactly("example", "example-only"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'changed'"); + } + }; + trigger.accept(new StatementDeParser(deparser, new SelectDeParser(), output)); + assertThat(output.toString()).endsWith("audit_row('changed')"); + assertThat(trigger.toString()).endsWith("audit_row('example')"); + } + + @Test + void testProgrammaticTriggerAndRole() throws Exception { + CreateTrigger trigger = new CreateTrigger(); + trigger.setTrigger(new Table("audit")); + trigger.setTable(new Table("users")); + trigger.setTiming(CreateTrigger.Timing.AFTER); + trigger.setEvent(CreateTrigger.Event.INSERT); + Function routine = new Function(); + routine.setName("audit_row"); + routine.setParameters(new ExpressionList<>()); + trigger.setRoutine(routine); + assertThat(CCJSqlParserUtil.parse(trigger.toString()).toString()) + .isEqualTo(trigger.toString()); + CreateRole role = new CreateRole(); + role.setName("reader"); + role.getOptions().add(new RoleOption(RoleOption.Kind.NOLOGIN)); + assertThat(CCJSqlParserUtil.parse(role.toString()).toString()) + .isEqualTo("CREATE ROLE reader NOLOGIN"); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE OR REPLACE ROLE app", "CREATE ROLE app WITH", "CREATE ROLE app CONNECTION 10", + "CREATE ROLE app PASSWORD", "ALTER ROLE ALL LOGIN", + "ALTER ROLE app IN DATABASE appdb LOGIN", + "ALTER ROLE app IN ROLE reader", "ALTER GROUP staff LOGIN", + "GRANT SELECT ON users FROM app", "REVOKE SELECT ON users TO app", + "GRANT SELECT () ON users TO app", "GRANT SELECT ON users TO app WITH ADMIN OPTION", + "GRANT reader TO app WITH GRANT OPTION", + "REVOKE ADMIN OPTION FOR SELECT ON users FROM app", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLE users TO app", + "ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO app GRANTED BY owner", + "CREATE TRIGGER audit BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_row(id + 1)", + "CREATE CONSTRAINT TRIGGER audit BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_row()", + "CREATE TRIGGER audit AFTER INSERT ON users DEFERRABLE EXECUTE FUNCTION audit_row()", + "CREATE OR REPLACE CONSTRAINT TRIGGER audit AFTER INSERT ON users FOR EACH ROW EXECUTE FUNCTION audit_row()" + }) + void testInvalidSyntax(String sql) { + assertThatThrownBy(() -> CCJSqlParserUtil.parse(sql)) + .isInstanceOf(JSQLParserException.class); + } +} diff --git a/src/test/resources/postgresql/access-ddl.sql b/src/test/resources/postgresql/access-ddl.sql new file mode 100644 index 0000000000..29de42e431 --- /dev/null +++ b/src/test/resources/postgresql/access-ddl.sql @@ -0,0 +1,104 @@ +--- +-- #%L +-- JSQLParser library +-- %% +-- Copyright (C) 2004 - 2026 JSQLParser +-- %% +-- Dual licensed under GNU LGPL 2.1 or Apache License 2.0 +-- #L% +--- +CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'example-only' +CREATE ROLE app +CREATE ROLE login +CREATE ROLE admin WITH SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN BYPASSRLS +CREATE ROLE guest NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOLOGIN NOREPLICATION NOBYPASSRLS +CREATE ROLE reader WITH CONNECTION LIMIT -1 PASSWORD NULL +CREATE ROLE analyst ENCRYPTED PASSWORD 'example-only' VALID UNTIL '2030-01-01' +CREATE ROLE member IN ROLE reader, writer +CREATE ROLE staff IN GROUP reader ROLE app, analyst ADMIN owner USER member +CREATE ROLE legacy SYSID 100 +CREATE ROLE "Mixed Role" LOGIN +CREATE USER app_user WITH LOGIN PASSWORD 'example-only' +CREATE USER app_user NOLOGIN +CREATE GROUP app_group +CREATE GROUP staff WITH USER app, analyst +ALTER ROLE replicator CONNECTION LIMIT 10 +ALTER ROLE app WITH LOGIN NOSUPERUSER PASSWORD NULL +ALTER ROLE app ENCRYPTED PASSWORD 'example-only' VALID UNTIL 'infinity' +ALTER ROLE app RENAME TO new_app +ALTER ROLE CURRENT_USER SET search_path TO app, public +ALTER ROLE CURRENT_ROLE IN DATABASE appdb SET statement_timeout = '5s' +ALTER ROLE ALL SET work_mem TO DEFAULT +ALTER ROLE app SET work_mem FROM CURRENT +ALTER ROLE app RESET work_mem +ALTER ROLE ALL IN DATABASE appdb RESET ALL +ALTER USER app_user WITH NOLOGIN +ALTER USER SESSION_USER SET enable_seqscan TO on +ALTER GROUP staff ADD USER app, analyst +ALTER GROUP staff DROP USER analyst +ALTER GROUP staff RENAME TO team +GRANT SELECT ON app.users TO reader +GRANT SELECT ON type TO app +GRANT SELECT ON tables TO app +GRANT SELECT ON function TO granted +GRANT SELECT ON tables, types TO app +GRANT SELECT ON type.items TO app +GRANT SELECT, INSERT, UPDATE ON TABLE app.users, app.orders TO reader, writer +GRANT SELECT (id, name), UPDATE (name) ON app.users TO app +GRANT ALL PRIVILEGES ON TABLE app.users TO PUBLIC WITH GRANT OPTION +GRANT REFERENCES (id) ON app.users TO GROUP app +GRANT TRUNCATE, TRIGGER, MAINTAIN ON app.users TO owner +GRANT USAGE, SELECT, UPDATE ON SEQUENCE app.user_id_seq TO app +GRANT ALL ON ALL SEQUENCES IN SCHEMA app, public TO app +GRANT SELECT ON ALL TABLES IN SCHEMA app TO reader +GRANT CREATE, CONNECT, TEMPORARY ON DATABASE appdb TO app +GRANT TEMP ON DATABASE appdb TO app +GRANT USAGE ON DOMAIN app.positive_int TO app +GRANT USAGE ON FOREIGN DATA WRAPPER postgres_fdw TO app +GRANT USAGE ON FOREIGN SERVER foreign_server TO app +GRANT EXECUTE ON FUNCTION add(integer, integer) TO app +GRANT EXECUTE ON FUNCTION app.f(IN value integer, VARIADIC args text[]) TO app +GRANT EXECUTE ON FUNCTION app.no_args(), app.unspecified TO app +GRANT EXECUTE ON PROCEDURE app.process(integer) TO app +GRANT EXECUTE ON ROUTINE app.process(integer) TO CURRENT_USER +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA app TO PUBLIC +GRANT EXECUTE ON ALL PROCEDURES IN SCHEMA app TO app +GRANT EXECUTE ON ALL ROUTINES IN SCHEMA app TO app +GRANT USAGE ON LANGUAGE plpgsql TO app +GRANT SELECT, UPDATE ON LARGE OBJECT 123, 456 TO app +GRANT SET, ALTER SYSTEM ON PARAMETER work_mem, maintenance_work_mem TO app +GRANT USAGE, CREATE ON SCHEMA public TO app GRANTED BY CURRENT_ROLE +GRANT CREATE ON TABLESPACE appspace TO app +GRANT USAGE ON TYPE app.status TO app +GRANT reader, writer TO app WITH ADMIN OPTION +GRANT reader TO app WITH INHERIT FALSE +GRANT reader TO app WITH SET TRUE GRANTED BY owner +REVOKE EXECUTE ON FUNCTION add(integer, integer) FROM app +REVOKE GRANT OPTION FOR SELECT ON TABLE app.users FROM app CASCADE +REVOKE SELECT (id) ON app.users FROM app RESTRICT +REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA app FROM PUBLIC +REVOKE USAGE ON SCHEMA public FROM app GRANTED BY CURRENT_USER CASCADE +REVOKE reader, writer FROM app +REVOKE ADMIN OPTION FOR reader FROM app CASCADE +REVOKE INHERIT OPTION FOR reader FROM app RESTRICT +REVOKE SET OPTION FOR reader FROM app GRANTED BY owner +ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app +ALTER DEFAULT PRIVILEGES FOR ROLE owner, admin IN SCHEMA app, public GRANT USAGE ON SEQUENCES TO app WITH GRANT OPTION +ALTER DEFAULT PRIVILEGES FOR USER owner REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC +ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON ROUTINES TO app +ALTER DEFAULT PRIVILEGES GRANT USAGE ON TYPES TO app +ALTER DEFAULT PRIVILEGES GRANT CREATE ON SCHEMAS TO app +ALTER DEFAULT PRIVILEGES GRANT SELECT ON LARGE OBJECTS TO app +ALTER DEFAULT PRIVILEGES REVOKE GRANT OPTION FOR SELECT ON TABLES FROM app CASCADE +CREATE TRIGGER audit_trigger AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION audit_row() +CREATE CONSTRAINT TRIGGER fk_trigger AFTER INSERT ON child DEFERRABLE INITIALLY DEFERRED FOR EACH ROW EXECUTE FUNCTION check_parent() +CREATE CONSTRAINT TRIGGER fk_trigger AFTER UPDATE ON child FROM parent NOT DEFERRABLE FOR EACH ROW EXECUTE FUNCTION check_parent() +CREATE CONSTRAINT TRIGGER fk_trigger AFTER INSERT ON child INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE check_parent() +CREATE OR REPLACE TRIGGER audit_trigger BEFORE UPDATE OF balance, owner ON accounts FOR EACH ROW WHEN (OLD.balance IS DISTINCT FROM NEW.balance) EXECUTE FUNCTION audit_row('balance', 1, flag) +CREATE TRIGGER insert_view INSTEAD OF INSERT ON app.users_view FOR EACH ROW EXECUTE FUNCTION app.insert_row() +CREATE TRIGGER truncate_audit AFTER TRUNCATE ON users EXECUTE FUNCTION audit_all() +CREATE TRIGGER statement_audit AFTER DELETE ON users FOR STATEMENT EXECUTE FUNCTION audit_all() +CREATE TRIGGER update_audit AFTER UPDATE ON users REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows FOR EACH STATEMENT EXECUTE FUNCTION audit_all() +CREATE TRIGGER insert_audit AFTER INSERT ON users REFERENCING NEW TABLE inserted FOR EACH STATEMENT EXECUTE PROCEDURE audit_all() +CREATE TRIGGER row_audit AFTER UPDATE ON users FOR ROW WHEN (OLD.id <> NEW.id) EXECUTE FUNCTION audit_row() +CREATE TRIGGER "Mixed Trigger" BEFORE DELETE ON "App"."Users" FOR EACH ROW EXECUTE FUNCTION "App"."audit_row"('it''s an example')