Skip to content

Commit 1078346

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 cf057dd commit 1078346

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
@@ -154,6 +154,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
154154
| **MySQL resource groups** | `CREATE`/`ALTER`/`DROP RESOURCE GROUP` · `SET RESOURCE GROUP` |
155155
| **PostgreSQL session statements** | `DISCARD` · `LISTEN` · `UNLISTEN` · `SHOW`/`RESET TIME ZONE`/`TRANSACTION ISOLATION LEVEL`/`SESSION AUTHORIZATION` · `COPY` options from before 9.0 |
156156
| **MySQL server administration** | `FLUSH` · `KILL` · `RESET PERSIST` · `HANDLER … OPEN`/`READ`/`CLOSE` |
157+
| **PostgreSQL cursors** | `DECLARE … CURSOR` · `FETCH` · `MOVE` · `CLOSE` |
157158
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
158159
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
159160
| **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
@@ -53,6 +53,9 @@
5353
import net.sf.jsqlparser.statement.create.trigger.CreateEventTrigger;
5454
import net.sf.jsqlparser.statement.create.view.AlterView;
5555
import net.sf.jsqlparser.statement.create.view.CreateView;
56+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
57+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
58+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
5659
import net.sf.jsqlparser.statement.delete.Delete;
5760
import net.sf.jsqlparser.statement.drop.Drop;
5861
import net.sf.jsqlparser.statement.execute.Execute;
@@ -1011,6 +1014,24 @@ public enum Feature {
10111014
* @see DeclareStatement
10121015
*/
10131016
declare,
1017+
/**
1018+
* PostgreSQL "DECLARE name CURSOR FOR query" statement is allowed
1019+
*
1020+
* @see DeclareCursorStatement
1021+
*/
1022+
declareCursor,
1023+
/**
1024+
* PostgreSQL "FETCH" and "MOVE" statements of a cursor are allowed
1025+
*
1026+
* @see FetchCursorStatement
1027+
*/
1028+
fetchCursor,
1029+
/**
1030+
* PostgreSQL "CLOSE" statement of a cursor is allowed
1031+
*
1032+
* @see CloseCursorStatement
1033+
*/
1034+
closeCursor,
10141035

10151036
/** SQL Server local table variables in queries and DML targets. */
10161037
tableVariable,

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@
8585
import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping;
8686
import net.sf.jsqlparser.statement.create.view.AlterView;
8787
import net.sf.jsqlparser.statement.create.view.CreateView;
88+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
89+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
90+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
8891
import net.sf.jsqlparser.statement.delete.Delete;
8992
import net.sf.jsqlparser.statement.drop.Drop;
9093
import net.sf.jsqlparser.statement.drop.DropPolicy;
@@ -1083,6 +1086,33 @@ public <S> Void visit(DiscardStatement discard, S context) {
10831086
return null;
10841087
}
10851088

1089+
@Override
1090+
public <S> Void visit(DeclareCursorStatement declareCursor, S context) {
1091+
// opens a cursor of the session over its query, which reads as the cursor is fetched
1092+
analysis.claimTopLevel();
1093+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1094+
if (declareCursor.getQuery() != null) {
1095+
declareCursor.getQuery().accept(analysis.selects, context);
1096+
}
1097+
return null;
1098+
}
1099+
1100+
@Override
1101+
public <S> Void visit(FetchCursorStatement fetchCursor, S context) {
1102+
// moves the cursor and runs its query that far; only FETCH returns the rows
1103+
if (analysis.claimTopLevel() && fetchCursor.getKind() == FetchCursorStatement.Kind.FETCH) {
1104+
analysis.certain(StmtFeature.RETURNS_RESULT_SET);
1105+
}
1106+
analysis.certain(StmtFeature.MODIFIES_SESSION);
1107+
analysis.possible(StmtFeature.READS_DATA);
1108+
return null;
1109+
}
1110+
1111+
@Override
1112+
public <S> Void visit(CloseCursorStatement closeCursor, S context) {
1113+
return sessionOnly();
1114+
}
1115+
10861116
@Override
10871117
public <S> Void visit(AlterSystemStatement alterSystemStatement, S context) {
10881118
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
@@ -119,6 +119,9 @@
119119
import net.sf.jsqlparser.statement.upsert.Upsert;
120120
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
121121
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
122+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
123+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
124+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
122125
import net.sf.jsqlparser.statement.export.ExportDataStatement;
123126
import net.sf.jsqlparser.statement.load.LoadDataStatement;
124127

@@ -808,6 +811,30 @@ default void visit(BackupLockStatement backupLock) {
808811
visit(backupLock, null);
809812
}
810813

814+
default <S> T visit(DeclareCursorStatement declareCursor, S context) {
815+
return null;
816+
}
817+
818+
default void visit(DeclareCursorStatement declareCursor) {
819+
visit(declareCursor, null);
820+
}
821+
822+
default <S> T visit(FetchCursorStatement fetchCursor, S context) {
823+
return null;
824+
}
825+
826+
default void visit(FetchCursorStatement fetchCursor) {
827+
visit(fetchCursor, null);
828+
}
829+
830+
default <S> T visit(CloseCursorStatement closeCursor, S context) {
831+
return null;
832+
}
833+
834+
default void visit(CloseCursorStatement closeCursor) {
835+
visit(closeCursor, null);
836+
}
837+
811838
<S> T visit(CreatePolicy createPolicy, S context);
812839

813840
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
@@ -137,6 +137,9 @@
137137
import net.sf.jsqlparser.util.TableDefinitionTraversal;
138138
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
139139
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
140+
import net.sf.jsqlparser.statement.cursor.CloseCursorStatement;
141+
import net.sf.jsqlparser.statement.cursor.DeclareCursorStatement;
142+
import net.sf.jsqlparser.statement.cursor.FetchCursorStatement;
140143
import net.sf.jsqlparser.statement.export.ExportDataStatement;
141144
import net.sf.jsqlparser.statement.load.LoadDataStatement;
142145

@@ -610,6 +613,24 @@ public <S> T visit(BackupLockStatement backupLock, S context) {
610613
return null;
611614
}
612615

616+
@Override
617+
public <S> T visit(DeclareCursorStatement declareCursor, S context) {
618+
if (declareCursor.getQuery() != null) {
619+
declareCursor.getQuery().accept(selectVisitor, context);
620+
}
621+
return null;
622+
}
623+
624+
@Override
625+
public <S> T visit(FetchCursorStatement fetchCursor, S context) {
626+
return null;
627+
}
628+
629+
@Override
630+
public <S> T visit(CloseCursorStatement closeCursor, S context) {
631+
return null;
632+
}
633+
613634
@Override
614635
public <S> T visit(CreatePolicy createPolicy, S context) {
615636
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)