Skip to content

feat: parse PostgreSQL DECLARE CURSOR, FETCH, MOVE and CLOSE - #2899

Open
minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/postgresql-cursors-20261106
Open

minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/postgresql-cursors-20261106

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

PostgreSQL scripts and statement logs that page through a query with a cursor fail to parse. On master (12548b9da) every cursor statement fails with Dialect.POSTGRESQL and without a dialect, for example:

  • DECLARE c CURSOR FOR SELECT * FROM t, DECLARE c BINARY INSENSITIVE NO SCROLL CURSOR WITH HOLD FOR SELECT 1, DECLARE c CURSOR FOR VALUES (1), (2), DECLARE c CURSOR FOR TABLE t
  • FETCH NEXT FROM c, FETCH ABSOLUTE -1 FROM c, FETCH FORWARD 10 IN c, FETCH ALL c, FETCH 5 FROM c
  • MOVE ABSOLUTE 2 IN c, MOVE BACKWARD ALL FROM c
  • CLOSE c, CLOSE ALL

PostgreSQL 18 references: DECLARE, FETCH, MOVE, CLOSE.

Current design (As-is)

  • DECLARE parses only SQL Server's DECLARE @variable … (DeclareStatement, with DeclareType TYPE, TABLE and AS), so DECLARE c CURSOR … fails at the name. With Dialect.ORACLE, DECLARE starts a PL/SQL block.
  • FETCH is a reserved keyword used only by SELECT … FETCH {FIRST | NEXT} n ROWS ONLY. CLOSE is a non-reserved keyword used by Oracle's ALTER SESSION CLOSE DATABASE LINK. MOVE is a plain identifier. None of them starts a statement: they fail, or become an UnsupportedStatement with allowUnsupportedStatements.

Target design (To-be)

A new package net.sf.jsqlparser.statement.cursor holds the statements, following PostgreSQL 18's DeclareCursorStmt, FetchStmt and ClosePortalStmt. DeclareStatement keeps its API and its reading.

  • DECLARE name [BINARY] [ASENSITIVE | INSENSITIVE] [[NO] SCROLL] CURSOR [{WITH | WITHOUT} HOLD] FOR query parses to DeclareCursorStatement.
    • getOptions() lists the CursorOption values (BINARY, ASENSITIVE, INSENSITIVE, SCROLL, NO_SCROLL) in their order. PostgreSQL's cursor_options takes them in any order and repeated, and the server rejects contradicting ones such as SCROLL NO SCROLL when it runs the statement, so the parser accepts them as the grammar does.
    • getHoldability() is WITH_HOLD, WITHOUT_HOLD or null.
    • getQuery() is the Select read by the shared Select() production: a SELECT, VALUES, TABLE t, a set operation, a parenthesized query or a query with WITH. INSERT, UPDATE and DELETE are rejected, as in PostgreSQL.
  • {FETCH | MOVE} [direction] [FROM | IN] cursor parses to FetchCursorStatement, whose getKind() is FETCH or MOVE (PostgreSQL's FetchStmt covers both).
    • The direction is an optional FetchDirection keyword (NEXT, PRIOR, FIRST, LAST, ABSOLUTE, RELATIVE, FORWARD, BACKWARD) with getCount() or isAll(). ABSOLUTE and RELATIVE take a count, FORWARD and BACKWARD an optional count or ALL, and a count or ALL can stand alone; FetchDirection.isCountRequired() and isCountOptional() describe this. Every direction may omit FROM/IN, which getFromKeyword() keeps.
    • A count is PostgreSQL's SignedIconst: a signed 32-bit integer. FETCH ABSOLUTE 2147483648 FROM c is rejected, as PostgreSQL reads that number as a numeric constant.
    • As in PostgreSQL's grammar, a direction word that ends the statement names the cursor: FETCH next fetches from a cursor named next, while FETCH NEXT next uses the NEXT direction.
  • CLOSE {name | ALL} parses to CloseCursorStatement; isAll() is true when it has no cursor name.
  • A cursor name is PostgreSQL's ColId: a name, but not one of PostgreSQL's reserved words or type and function name keywords, so CLOSE table and DECLARE binary CURSOR … are rejected. It is not qualified.
  • Dialects:
    • Dialect.POSTGRESQL reads FETCH, MOVE, CLOSE, and DECLARE followed by a name, as cursor statements.
    • Without a dialect the statements parse when they take the PostgreSQL form: DECLARE needs a name, the cursor options and CURSOR, and FETCH, MOVE and CLOSE must end at a semicolon or the end of the input. The forms are the SQL standard's or PostgreSQL's own, and no other reading of them exists, while other products' variants such as SQL Server's FETCH NEXT FROM c INTO @x and CLOSE GLOBAL c remain unsupported statements.
    • Other dialects are unchanged, and DECLARE @x INT still parses to DeclareStatement in every dialect that read it before.
  • Keywords: MOVE becomes a non-reserved keyword, as a statement must start with a token; it remains usable as a name, as do CLOSE, CURSOR and the option and direction words. SELECT … FETCH FIRST, OFFSET … FETCH NEXT and Oracle's ALTER SESSION CLOSE DATABASE LINK keep their readings.
  • Wiring:
    • The statements are dispatched before Declare(), and isStatementStartAhead() lists FETCH, MOVE and CLOSE, so they end at the statement boundary in scripts and after an unsupported statement.
    • toString() and StatementDeParser share appendTo; the query of DECLARE is rendered through the SelectDeParser.
    • StatementVisitor has default methods for the three statements, and StatementVisitorAdapter visits the query of DECLARE.
    • TablesNamesFinder reports the tables of the query of DECLARE, without its WITH names, and none for FETCH, MOVE and CLOSE, whose cursor name is not a table.
    • Validation: new features declareCursor, fetchCursor (FETCH and MOVE) and closeCursor, enabled by the PostgreSQL capabilities; the query of DECLARE is validated like a SELECT.
    • Feature analysis: MODIFIES_SESSION for all of them, READS_DATA for a DECLARE whose query reads a table, RETURNS_RESULT_SET for FETCH, and a possible READS_DATA for FETCH and MOVE, which run the query of the cursor.
    • AST nodes (fix: give every parsed object a node spanning its source #2870): the statements get the node of their tokens, and ASTNodeCoverageTest walks them.
    • The usage guide, README and index list the statements.
  • The public API only gains classes, visitor default methods and Feature constants.

Validation

  • 139 new tests: PostgreSqlCursorTest 135 and CursorValidatorTest 4; ASTNodeCoverageTest walks the new forms. They fail on master.
    • Every form: parse, toString(), StatementDeParser and reparse with Dialect.POSTGRESQL and without a dialect, and the AST fields (options, holdability, query class, kind, direction, count, ALL, FROM/IN, cursor name). Signs, leading zeros, digit separators and keyword case are normalized.
    • Rejected forms: incomplete DECLARE, FETCH, MOVE and CLOSE, SENSITIVE or HOLD as options, options after CURSOR, WITHOUT HOLD WITH HOLD, DECLARE … FOR INSERT/UPDATE/DELETE, NEXT 5, PRIOR ALL, ABSOLUTE without a count, RELATIVE ALL, 1.5, counts outside 32 bits, INTO, two cursors, qualified names, reserved words as names, CLOSE ALL c.
    • Kept readings: DECLARE @x … in SQL Server, PostgreSQL and without a dialect; SQL Server, MySQL, MariaDB, Oracle, H2 and CockroachDB reject the new forms, and FETCH/MOVE/CLOSE there remain unsupported statements; other products' variants remain unsupported statements without a dialect; SELECT … FETCH FIRST, ALTER SESSION CLOSE DATABASE LINK and move, close, cursor and the option and direction words as names.
    • Statement boundaries in scripts (with and without semicolons) and after unsupported statements, builders, visitors, table discovery, validation capabilities and statement features.
  • PostgreSQL 18.6 accepts all 81 statements the tests accept, 77 of which master rejects. Some fail only when run, for example cursor "c" does not exist, DECLARE CURSOR can only be used in transaction blocks or cannot specify both SCROLL and NO SCROLL. It reports a syntax error for all 53 statements the tests reject, including FETCH ABSOLUTE 2147483648 c.
  • Java 17 offline Gradle spotlessApply cleanTest check jar: 12,928 tests, 12,903 passed, 25 skipped, zero failures/errors. The grammar choice conflict check, Checkstyle, PMD and SpotBugs pass.
  • Compared with master (12548b9da) on a corpus of 188 MySQL 8.4 and 166 PostgreSQL 18 statements other than DDL/DCL, each accepted by the server, with the MySQL or PostgreSQL dialect and without a dialect:
    • Fixed with Dialect.POSTGRESQL and without a dialect: DECLARE c BINARY INSENSITIVE NO SCROLL CURSOR WITH HOLD FOR SELECT 1, FETCH NEXT FROM c, FETCH FORWARD 10 IN c, FETCH ALL c, MOVE ABSOLUTE 2 IN c and CLOSE ALL.
    • 0 regressed and 0 changed in status, statement class or rendering.
  • Compared with master (12548b9da) on a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, plus variants, scripts and the keyword sweeps (including move, close and declare as names): 0 regressed, 0 changed.

AI assistance: prepared with Claude Code.

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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant