Skip to content

Commit f987ecc

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 a30a679 commit f987ecc

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
@@ -49,6 +49,9 @@
4949
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
5050
import net.sf.jsqlparser.statement.create.view.AlterView;
5151
import net.sf.jsqlparser.statement.create.view.CreateView;
52+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
53+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
54+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
5255
import net.sf.jsqlparser.statement.delete.Delete;
5356
import net.sf.jsqlparser.statement.drop.Drop;
5457
import net.sf.jsqlparser.statement.execute.Execute;
@@ -941,6 +944,24 @@ public enum Feature {
941944
* @see DeclareStatement
942945
*/
943946
declare,
947+
/**
948+
* PostgreSQL "DECLARE name CURSOR FOR query" statement is allowed
949+
*
950+
* @see DeclareCursorStatement
951+
*/
952+
declareCursor,
953+
/**
954+
* PostgreSQL "FETCH" and "MOVE" statements of a cursor are allowed
955+
*
956+
* @see FetchCursorStatement
957+
*/
958+
fetchCursor,
959+
/**
960+
* PostgreSQL "CLOSE" statement of a cursor is allowed
961+
*
962+
* @see CloseCursorStatement
963+
*/
964+
closeCursor,
944965

945966
/** SQL Server local table variables in queries and DML targets. */
946967
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;
@@ -991,6 +994,33 @@ public <S> Void visit(DeclareStatement declareStatement, S context) {
991994
return sessionOnly();
992995
}
993996

997+
@Override
998+
public <S> Void visit(DeclareCursorStatement declareCursor, S context) {
999+
// opens a cursor of the session over its query, which reads as the cursor is fetched
1000+
analysis.claimTopLevel();
1001+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1002+
if (declareCursor.getQuery() != null) {
1003+
declareCursor.getQuery().accept(analysis.selects, context);
1004+
}
1005+
return null;
1006+
}
1007+
1008+
@Override
1009+
public <S> Void visit(FetchCursorStatement fetchCursor, S context) {
1010+
// moves the cursor and runs its query that far; only FETCH returns the rows
1011+
if (analysis.claimTopLevel() && fetchCursor.getKind() == FetchCursorStatement.Kind.FETCH) {
1012+
analysis.certain(StmtFeature.RETURNS_RESULT_SET);
1013+
}
1014+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1015+
analysis.possible(StmtFeature.READS_DATA);
1016+
return null;
1017+
}
1018+
1019+
@Override
1020+
public <S> Void visit(CloseCursorStatement closeCursor, S context) {
1021+
return sessionOnly();
1022+
}
1023+
9941024
@Override
9951025
public <S> Void visit(AlterSystemStatement alterSystemStatement, S context) {
9961026
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

@@ -736,6 +739,30 @@ default void visit(BackupLockStatement backupLock) {
736739
visit(backupLock, null);
737740
}
738741

742+
default <S> T visit(DeclareCursorStatement declareCursor, S context) {
743+
return null;
744+
}
745+
746+
default void visit(DeclareCursorStatement declareCursor) {
747+
visit(declareCursor, null);
748+
}
749+
750+
default <S> T visit(FetchCursorStatement fetchCursor, S context) {
751+
return null;
752+
}
753+
754+
default void visit(FetchCursorStatement fetchCursor) {
755+
visit(fetchCursor, null);
756+
}
757+
758+
default <S> T visit(CloseCursorStatement closeCursor, S context) {
759+
return null;
760+
}
761+
762+
default void visit(CloseCursorStatement closeCursor) {
763+
visit(closeCursor, null);
764+
}
765+
739766
<S> T visit(CreatePolicy createPolicy, S context);
740767

741768
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

@@ -569,6 +572,24 @@ public <S> T visit(BackupLockStatement backupLock, S context) {
569572
return null;
570573
}
571574

575+
@Override
576+
public <S> T visit(DeclareCursorStatement declareCursor, S context) {
577+
if (declareCursor.getQuery() != null) {
578+
declareCursor.getQuery().accept(selectVisitor, context);
579+
}
580+
return null;
581+
}
582+
583+
@Override
584+
public <S> T visit(FetchCursorStatement fetchCursor, S context) {
585+
return null;
586+
}
587+
588+
@Override
589+
public <S> T visit(CloseCursorStatement closeCursor, S context) {
590+
return null;
591+
}
592+
572593
@Override
573594
public <S> T visit(CreatePolicy createPolicy, S context) {
574595
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)