Skip to content

Commit ab31ad4

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 12548b9 commit ab31ad4

20 files changed

Lines changed: 1530 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
150150
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
151151
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
152152
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
153+
| **PostgreSQL cursors** | `DECLARE … CURSOR` · `FETCH` · `MOVE` · `CLOSE` |
153154
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
154155
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
155156
| **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
@@ -47,6 +47,9 @@
4747
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
4848
import net.sf.jsqlparser.statement.create.view.AlterView;
4949
import net.sf.jsqlparser.statement.create.view.CreateView;
50+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
51+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
52+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
5053
import net.sf.jsqlparser.statement.delete.Delete;
5154
import net.sf.jsqlparser.statement.drop.Drop;
5255
import net.sf.jsqlparser.statement.execute.Execute;
@@ -915,6 +918,24 @@ public enum Feature {
915918
* @see DeclareStatement
916919
*/
917920
declare,
921+
/**
922+
* PostgreSQL "DECLARE name CURSOR FOR query" statement is allowed
923+
*
924+
* @see DeclareCursorStatement
925+
*/
926+
declareCursor,
927+
/**
928+
* PostgreSQL "FETCH" and "MOVE" statements of a cursor are allowed
929+
*
930+
* @see FetchCursorStatement
931+
*/
932+
fetchCursor,
933+
/**
934+
* PostgreSQL "CLOSE" statement of a cursor is allowed
935+
*
936+
* @see CloseCursorStatement
937+
*/
938+
closeCursor,
918939

919940
/** SQL Server local table variables in queries and DML targets. */
920941
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;
@@ -981,6 +984,33 @@ public <S> Void visit(DeclareStatement declareStatement, S context) {
981984
return sessionOnly();
982985
}
983986

987+
@Override
988+
public <S> Void visit(DeclareCursorStatement declareCursor, S context) {
989+
// opens a cursor of the session over its query, which reads as the cursor is fetched
990+
analysis.claimTopLevel();
991+
analysis.certain(StmtFeature.MODIFIES_SESSION);
992+
if (declareCursor.getQuery() != null) {
993+
declareCursor.getQuery().accept(analysis.selects, context);
994+
}
995+
return null;
996+
}
997+
998+
@Override
999+
public <S> Void visit(FetchCursorStatement fetchCursor, S context) {
1000+
// moves the cursor and runs its query that far; only FETCH returns the rows
1001+
if (analysis.claimTopLevel() && fetchCursor.getKind() == FetchCursorStatement.Kind.FETCH) {
1002+
analysis.certain(StmtFeature.RETURNS_RESULT_SET);
1003+
}
1004+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1005+
analysis.possible(StmtFeature.READS_DATA);
1006+
return null;
1007+
}
1008+
1009+
@Override
1010+
public <S> Void visit(CloseCursorStatement closeCursor, S context) {
1011+
return sessionOnly();
1012+
}
1013+
9841014
@Override
9851015
public <S> Void visit(AlterSystemStatement alterSystemStatement, S context) {
9861016
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
@@ -111,6 +111,9 @@
111111
import net.sf.jsqlparser.statement.upsert.Upsert;
112112
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
113113
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
114+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
115+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
116+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
114117
import net.sf.jsqlparser.statement.export.ExportDataStatement;
115118
import net.sf.jsqlparser.statement.load.LoadDataStatement;
116119

@@ -720,6 +723,30 @@ default void visit(BackupLockStatement backupLock) {
720723
visit(backupLock, null);
721724
}
722725

726+
default <S> T visit(DeclareCursorStatement declareCursor, S context) {
727+
return null;
728+
}
729+
730+
default void visit(DeclareCursorStatement declareCursor) {
731+
visit(declareCursor, null);
732+
}
733+
734+
default <S> T visit(FetchCursorStatement fetchCursor, S context) {
735+
return null;
736+
}
737+
738+
default void visit(FetchCursorStatement fetchCursor) {
739+
visit(fetchCursor, null);
740+
}
741+
742+
default <S> T visit(CloseCursorStatement closeCursor, S context) {
743+
return null;
744+
}
745+
746+
default void visit(CloseCursorStatement closeCursor) {
747+
visit(closeCursor, null);
748+
}
749+
723750
<S> T visit(CreatePolicy createPolicy, S context);
724751

725752
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
@@ -128,6 +128,9 @@
128128
import net.sf.jsqlparser.util.TableDefinitionTraversal;
129129
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
130130
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
131+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
132+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
133+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
131134
import net.sf.jsqlparser.statement.export.ExportDataStatement;
132135
import net.sf.jsqlparser.statement.load.LoadDataStatement;
133136

@@ -559,6 +562,24 @@ public <S> T visit(BackupLockStatement backupLock, S context) {
559562
return null;
560563
}
561564

565+
@Override
566+
public <S> T visit(DeclareCursorStatement declareCursor, S context) {
567+
if (declareCursor.getQuery() != null) {
568+
declareCursor.getQuery().accept(selectVisitor, context);
569+
}
570+
return null;
571+
}
572+
573+
@Override
574+
public <S> T visit(FetchCursorStatement fetchCursor, S context) {
575+
return null;
576+
}
577+
578+
@Override
579+
public <S> T visit(CloseCursorStatement closeCursor, S context) {
580+
return null;
581+
}
582+
562583
@Override
563584
public <S> T visit(CreatePolicy createPolicy, S context) {
564585
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)