Skip to content

Commit fc8712b

Browse files
committed
feat: parse PostgreSQL DECLARE CURSOR, FETCH, MOVE and CLOSE
PostgreSQL cursor statements failed to parse: DECLARE took only the SQL Server DECLARE @variable, and FETCH, MOVE and CLOSE started no statement, so scripts that page through a query with a cursor failed. Add the statement.cursor package. DeclareCursorStatement keeps the cursor options in their order (CursorOption), the holdability and the query. FetchCursorStatement covers FETCH and MOVE with an optional FetchDirection keyword, a signed 32-bit count or ALL, and FROM or IN. CloseCursorStatement covers CLOSE name and CLOSE ALL. As in PostgreSQL's grammar, a direction word that ends the statement names the cursor, and reserved words name none. Dialect.POSTGRESQL reads these statements. Without a dialect they parse when they take the PostgreSQL form, so other products' FETCH ... INTO stays an unsupported statement. Other dialects and DECLARE @variable are unchanged. MOVE becomes a non-reserved keyword. Wire the statements into the deparser, visitors, table finder, validation (declareCursor, fetchCursor, closeCursor) and feature analysis. Signed-off-by: minleejae <mmj9808@gmail.com>
1 parent e27929f commit fc8712b

20 files changed

Lines changed: 1521 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
152152
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
153153
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
154154
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
155+
| **PostgreSQL cursors** | `DECLARE … CURSOR` · `FETCH` · `MOVE` · `CLOSE` |
155156
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
156157
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
157158
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |

‎src/main/java/net/sf/jsqlparser/parser/feature/Feature.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
5252
import net.sf.jsqlparser.statement.create.view.AlterView;
5353
import net.sf.jsqlparser.statement.create.view.CreateView;
54+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
55+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
56+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
5457
import net.sf.jsqlparser.statement.delete.Delete;
5558
import net.sf.jsqlparser.statement.drop.Drop;
5659
import net.sf.jsqlparser.statement.execute.Execute;
@@ -955,6 +958,24 @@ public enum Feature {
955958
* @see DeclareStatement
956959
*/
957960
declare,
961+
/**
962+
* PostgreSQL "DECLARE name CURSOR FOR query" statement is allowed
963+
*
964+
* @see DeclareCursorStatement
965+
*/
966+
declareCursor,
967+
/**
968+
* PostgreSQL "FETCH" and "MOVE" statements of a cursor are allowed
969+
*
970+
* @see FetchCursorStatement
971+
*/
972+
fetchCursor,
973+
/**
974+
* PostgreSQL "CLOSE" statement of a cursor is allowed
975+
*
976+
* @see CloseCursorStatement
977+
*/
978+
closeCursor,
958979

959980
/** SQL Server local table variables in queries and DML targets. */
960981
tableVariable,

‎src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping;
8585
import net.sf.jsqlparser.statement.create.view.AlterView;
8686
import net.sf.jsqlparser.statement.create.view.CreateView;
87+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
88+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
89+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
8790
import net.sf.jsqlparser.statement.delete.Delete;
8891
import net.sf.jsqlparser.statement.drop.Drop;
8992
import net.sf.jsqlparser.statement.drop.DropPolicy;
@@ -1019,6 +1022,33 @@ public <S> Void visit(DiscardStatement discard, S context) {
10191022
return null;
10201023
}
10211024

1025+
@Override
1026+
public <S> Void visit(DeclareCursorStatement declareCursor, S context) {
1027+
// opens a cursor of the session over its query, which reads as the cursor is fetched
1028+
analysis.claimTopLevel();
1029+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1030+
if (declareCursor.getQuery() != null) {
1031+
declareCursor.getQuery().accept(analysis.selects, context);
1032+
}
1033+
return null;
1034+
}
1035+
1036+
@Override
1037+
public <S> Void visit(FetchCursorStatement fetchCursor, S context) {
1038+
// moves the cursor and runs its query that far; only FETCH returns the rows
1039+
if (analysis.claimTopLevel() && fetchCursor.getKind() == FetchCursorStatement.Kind.FETCH) {
1040+
analysis.certain(StmtFeature.RETURNS_RESULT_SET);
1041+
}
1042+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1043+
analysis.possible(StmtFeature.READS_DATA);
1044+
return null;
1045+
}
1046+
1047+
@Override
1048+
public <S> Void visit(CloseCursorStatement closeCursor, S context) {
1049+
return sessionOnly();
1050+
}
1051+
10221052
@Override
10231053
public <S> Void visit(AlterSystemStatement alterSystemStatement, S context) {
10241054
analysis.claimTopLevel();

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@
112112
import net.sf.jsqlparser.statement.upsert.Upsert;
113113
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
114114
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
115+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
116+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
117+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
115118
import net.sf.jsqlparser.statement.export.ExportDataStatement;
116119
import net.sf.jsqlparser.statement.load.LoadDataStatement;
117120

@@ -753,6 +756,30 @@ default void visit(BackupLockStatement backupLock) {
753756
visit(backupLock, null);
754757
}
755758

759+
default <S> T visit(DeclareCursorStatement declareCursor, S context) {
760+
return null;
761+
}
762+
763+
default void visit(DeclareCursorStatement declareCursor) {
764+
visit(declareCursor, null);
765+
}
766+
767+
default <S> T visit(FetchCursorStatement fetchCursor, S context) {
768+
return null;
769+
}
770+
771+
default void visit(FetchCursorStatement fetchCursor) {
772+
visit(fetchCursor, null);
773+
}
774+
775+
default <S> T visit(CloseCursorStatement closeCursor, S context) {
776+
return null;
777+
}
778+
779+
default void visit(CloseCursorStatement closeCursor) {
780+
visit(closeCursor, null);
781+
}
782+
756783
<S> T visit(CreatePolicy createPolicy, S context);
757784

758785
default void visit(CreatePolicy createPolicy) {

‎src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@
130130
import net.sf.jsqlparser.util.TableDefinitionTraversal;
131131
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
132132
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
133+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
134+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
135+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
133136
import net.sf.jsqlparser.statement.export.ExportDataStatement;
134137
import net.sf.jsqlparser.statement.load.LoadDataStatement;
135138

@@ -587,6 +590,24 @@ public <S> T visit(BackupLockStatement backupLock, S context) {
587590
return null;
588591
}
589592

593+
@Override
594+
public <S> T visit(DeclareCursorStatement declareCursor, S context) {
595+
if (declareCursor.getQuery() != null) {
596+
declareCursor.getQuery().accept(selectVisitor, context);
597+
}
598+
return null;
599+
}
600+
601+
@Override
602+
public <S> T visit(FetchCursorStatement fetchCursor, S context) {
603+
return null;
604+
}
605+
606+
@Override
607+
public <S> T visit(CloseCursorStatement closeCursor, S context) {
608+
return null;
609+
}
610+
590611
@Override
591612
public <S> T visit(CreatePolicy createPolicy, S context) {
592613
fromItemVisitor.visitFromItem(createPolicy.getTable(), context);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.cursor;
11+
12+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
13+
import net.sf.jsqlparser.statement.Statement;
14+
import net.sf.jsqlparser.statement.StatementVisitor;
15+
16+
/**
17+
* PostgreSQL {@code CLOSE {name | ALL}}, which closes one cursor or every open cursor of the
18+
* session.<br>
19+
* Examples:<br>
20+
* CLOSE c<br>
21+
* CLOSE ALL<br>
22+
*
23+
* @see <a href="https://www.postgresql.org/docs/18/sql-close.html">PostgreSQL CLOSE</a>
24+
*/
25+
public class CloseCursorStatement extends ASTNodeAccessImpl implements Statement {
26+
private String cursorName;
27+
28+
/** CLOSE ALL. */
29+
public CloseCursorStatement() {}
30+
31+
public CloseCursorStatement(String cursorName) {
32+
this.cursorName = cursorName;
33+
}
34+
35+
/** Returns null for CLOSE ALL. */
36+
public String getCursorName() {
37+
return cursorName;
38+
}
39+
40+
/** A null name makes the statement CLOSE ALL. */
41+
public void setCursorName(String cursorName) {
42+
this.cursorName = cursorName;
43+
}
44+
45+
/** Whether the statement is CLOSE ALL, which is when it has no cursor name. */
46+
public boolean isAll() {
47+
return cursorName == null;
48+
}
49+
50+
@Override
51+
public StringBuilder appendTo(StringBuilder builder) {
52+
return builder.append("CLOSE ").append(isAll() ? "ALL" : cursorName);
53+
}
54+
55+
@Override
56+
public String toString() {
57+
return appendTo(new StringBuilder()).toString();
58+
}
59+
60+
@Override
61+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
62+
return statementVisitor.visit(this, context);
63+
}
64+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.cursor;
11+
12+
/**
13+
* An option of PostgreSQL's {@code DECLARE name [options] CURSOR}. PostgreSQL's grammar takes them
14+
* in any order and repeated; the server rejects contradicting ones such as SCROLL and NO SCROLL
15+
* when it runs the statement.
16+
*
17+
* @see <a href="https://www.postgresql.org/docs/18/sql-declare.html">PostgreSQL DECLARE</a>
18+
*/
19+
public enum CursorOption {
20+
/** Returns the rows in binary rather than text format. */
21+
BINARY("BINARY"),
22+
23+
/** Leaves the visibility of later changes to the implementation. */
24+
ASENSITIVE("ASENSITIVE"),
25+
26+
/** Hides later changes; PostgreSQL's cursors are always insensitive. */
27+
INSENSITIVE("INSENSITIVE"),
28+
29+
/** Allows fetching backward. */
30+
SCROLL("SCROLL"),
31+
32+
/** Forbids fetching backward. */
33+
NO_SCROLL("NO SCROLL");
34+
35+
private final String keyword;
36+
37+
CursorOption(String keyword) {
38+
this.keyword = keyword;
39+
}
40+
41+
public String getKeyword() {
42+
return keyword;
43+
}
44+
}

0 commit comments

Comments
 (0)