diff --git a/README.md b/README.md index e7a8db452..2044e6835 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` | | **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` | | **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` | +| **PostgreSQL cursors** | `DECLARE … CURSOR` · `FETCH` · `MOVE` · `CLOSE` | | **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` | | **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | 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 dc3d137e3..e5652f2dd 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -47,6 +47,9 @@ import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.cursor.CloseCursorStatement; +import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement; +import net.sf.jsqlparser.statement.cursor.FetchCursorStatement; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.drop.Drop; import net.sf.jsqlparser.statement.execute.Execute; @@ -915,6 +918,24 @@ public enum Feature { * @see DeclareStatement */ declare, + /** + * PostgreSQL "DECLARE name CURSOR FOR query" statement is allowed + * + * @see DeclareCursorStatement + */ + declareCursor, + /** + * PostgreSQL "FETCH" and "MOVE" statements of a cursor are allowed + * + * @see FetchCursorStatement + */ + fetchCursor, + /** + * PostgreSQL "CLOSE" statement of a cursor is allowed + * + * @see CloseCursorStatement + */ + closeCursor, /** SQL Server local table variables in queries and DML targets. */ tableVariable, diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index df67903b0..31514e182 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -84,6 +84,9 @@ import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.create.view.AlterView; import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.statement.cursor.CloseCursorStatement; +import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement; +import net.sf.jsqlparser.statement.cursor.FetchCursorStatement; import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.drop.Drop; import net.sf.jsqlparser.statement.drop.DropPolicy; @@ -981,6 +984,33 @@ public Void visit(DeclareStatement declareStatement, S context) { return sessionOnly(); } + @Override + public Void visit(DeclareCursorStatement declareCursor, S context) { + // opens a cursor of the session over its query, which reads as the cursor is fetched + analysis.claimTopLevel(); + analysis.certain(StmtFeature.MODIFIES_SESSION); + if (declareCursor.getQuery() != null) { + declareCursor.getQuery().accept(analysis.selects, context); + } + return null; + } + + @Override + public Void visit(FetchCursorStatement fetchCursor, S context) { + // moves the cursor and runs its query that far; only FETCH returns the rows + if (analysis.claimTopLevel() && fetchCursor.getKind() == FetchCursorStatement.Kind.FETCH) { + analysis.certain(StmtFeature.RETURNS_RESULT_SET); + } + analysis.certain(StmtFeature.MODIFIES_SESSION); + analysis.possible(StmtFeature.READS_DATA); + return null; + } + + @Override + public Void visit(CloseCursorStatement closeCursor, S context) { + return sessionOnly(); + } + @Override public Void visit(AlterSystemStatement alterSystemStatement, S context) { analysis.claimTopLevel(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 4063637f7..76370678c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -111,6 +111,9 @@ import net.sf.jsqlparser.statement.upsert.Upsert; import net.sf.jsqlparser.statement.create.macro.CreateMacro; import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.cursor.CloseCursorStatement; +import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement; +import net.sf.jsqlparser.statement.cursor.FetchCursorStatement; import net.sf.jsqlparser.statement.export.ExportDataStatement; import net.sf.jsqlparser.statement.load.LoadDataStatement; @@ -720,6 +723,30 @@ default void visit(BackupLockStatement backupLock) { visit(backupLock, null); } + default T visit(DeclareCursorStatement declareCursor, S context) { + return null; + } + + default void visit(DeclareCursorStatement declareCursor) { + visit(declareCursor, null); + } + + default T visit(FetchCursorStatement fetchCursor, S context) { + return null; + } + + default void visit(FetchCursorStatement fetchCursor) { + visit(fetchCursor, null); + } + + default T visit(CloseCursorStatement closeCursor, S context) { + return null; + } + + default void visit(CloseCursorStatement closeCursor) { + visit(closeCursor, null); + } + T visit(CreatePolicy createPolicy, S context); default void visit(CreatePolicy createPolicy) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 9cb08129e..f8b303fa1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -128,6 +128,9 @@ import net.sf.jsqlparser.util.TableDefinitionTraversal; import net.sf.jsqlparser.statement.create.macro.CreateMacro; import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository; +import net.sf.jsqlparser.statement.cursor.CloseCursorStatement; +import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement; +import net.sf.jsqlparser.statement.cursor.FetchCursorStatement; import net.sf.jsqlparser.statement.export.ExportDataStatement; import net.sf.jsqlparser.statement.load.LoadDataStatement; @@ -559,6 +562,24 @@ public T visit(BackupLockStatement backupLock, S context) { return null; } + @Override + public T visit(DeclareCursorStatement declareCursor, S context) { + if (declareCursor.getQuery() != null) { + declareCursor.getQuery().accept(selectVisitor, context); + } + return null; + } + + @Override + public T visit(FetchCursorStatement fetchCursor, S context) { + return null; + } + + @Override + public T visit(CloseCursorStatement closeCursor, S context) { + return null; + } + @Override public T visit(CreatePolicy createPolicy, S context) { fromItemVisitor.visitFromItem(createPolicy.getTable(), context); diff --git a/src/main/java/net/sf/jsqlparser/statement/cursor/CloseCursorStatement.java b/src/main/java/net/sf/jsqlparser/statement/cursor/CloseCursorStatement.java new file mode 100644 index 000000000..06a5cacb1 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/cursor/CloseCursorStatement.java @@ -0,0 +1,64 @@ +/*- + * #%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.cursor; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** + * PostgreSQL {@code CLOSE {name | ALL}}, which closes one cursor or every open cursor of the + * session.
+ * Examples:
+ * CLOSE c
+ * CLOSE ALL
+ * + * @see PostgreSQL CLOSE + */ +public class CloseCursorStatement extends ASTNodeAccessImpl implements Statement { + private String cursorName; + + /** CLOSE ALL. */ + public CloseCursorStatement() {} + + public CloseCursorStatement(String cursorName) { + this.cursorName = cursorName; + } + + /** Returns null for CLOSE ALL. */ + public String getCursorName() { + return cursorName; + } + + /** A null name makes the statement CLOSE ALL. */ + public void setCursorName(String cursorName) { + this.cursorName = cursorName; + } + + /** Whether the statement is CLOSE ALL, which is when it has no cursor name. */ + public boolean isAll() { + return cursorName == null; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + return builder.append("CLOSE ").append(isAll() ? "ALL" : cursorName); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/cursor/CursorOption.java b/src/main/java/net/sf/jsqlparser/statement/cursor/CursorOption.java new file mode 100644 index 000000000..dc36372ba --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/cursor/CursorOption.java @@ -0,0 +1,44 @@ +/*- + * #%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.cursor; + +/** + * An option of PostgreSQL's {@code DECLARE name [options] CURSOR}. PostgreSQL's grammar takes them + * in any order and repeated; the server rejects contradicting ones such as SCROLL and NO SCROLL + * when it runs the statement. + * + * @see PostgreSQL DECLARE + */ +public enum CursorOption { + /** Returns the rows in binary rather than text format. */ + BINARY("BINARY"), + + /** Leaves the visibility of later changes to the implementation. */ + ASENSITIVE("ASENSITIVE"), + + /** Hides later changes; PostgreSQL's cursors are always insensitive. */ + INSENSITIVE("INSENSITIVE"), + + /** Allows fetching backward. */ + SCROLL("SCROLL"), + + /** Forbids fetching backward. */ + NO_SCROLL("NO SCROLL"); + + private final String keyword; + + CursorOption(String keyword) { + this.keyword = keyword; + } + + public String getKeyword() { + return keyword; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/cursor/DeclareCursorStatement.java b/src/main/java/net/sf/jsqlparser/statement/cursor/DeclareCursorStatement.java new file mode 100644 index 000000000..87ca339dc --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/cursor/DeclareCursorStatement.java @@ -0,0 +1,129 @@ +/*- + * #%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.cursor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.select.Select; + +/** + * PostgreSQL {@code DECLARE name [BINARY] [ASENSITIVE | INSENSITIVE] [[NO] SCROLL] CURSOR + * [{WITH | WITHOUT} HOLD] FOR query}, which defines a cursor over a SELECT, VALUES or TABLE query. + *
+ * Example:
+ * DECLARE c SCROLL CURSOR WITH HOLD FOR SELECT * FROM t
+ * It is not the SQL Server {@link net.sf.jsqlparser.statement.DeclareStatement DECLARE @variable}. + * + * @see PostgreSQL DECLARE + */ +public class DeclareCursorStatement extends ASTNodeAccessImpl implements Statement { + private String cursorName; + private final List options = new ArrayList<>(); + private Holdability holdability; + private Select query; + + public enum Holdability { + WITH_HOLD("WITH HOLD"), WITHOUT_HOLD("WITHOUT HOLD"); + + private final String keywords; + + Holdability(String keywords) { + this.keywords = keywords; + } + + public String getKeywords() { + return keywords; + } + } + + public DeclareCursorStatement() {} + + public DeclareCursorStatement(String cursorName, Select query) { + this.cursorName = cursorName; + this.query = query; + } + + public String getCursorName() { + return cursorName; + } + + public void setCursorName(String cursorName) { + this.cursorName = cursorName; + } + + /** The options between the name and CURSOR, in their order; the list is live. */ + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options.clear(); + if (options != null) { + this.options.addAll(options); + } + } + + public DeclareCursorStatement addOptions(CursorOption... options) { + this.options.addAll(Arrays.asList(options)); + return this; + } + + /** Returns null when neither WITH HOLD nor WITHOUT HOLD is written. */ + public Holdability getHoldability() { + return holdability; + } + + public void setHoldability(Holdability holdability) { + this.holdability = holdability; + } + + public Select getQuery() { + return query; + } + + public void setQuery(Select query) { + this.query = query; + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append); + } + + /** Renders the query through the caller's select writer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer