Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/site/sphinx/unsupported.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ Missing syntax is added on demand — please `open an issue <https://github.com/
Procedural SQL
=======================================

This is the one substantial gap. JSQLParser parses **statements**, not stored programs. Anonymous blocks with declarations, cursors, exception handlers and loops are outside its scope:
Procedural-language support is partial. Accepting a block or routine definition does not necessarily mean that its body is parsed into a statement tree. Select the relevant dialect as described in :ref:`Choose a Dialect`.

.. code-block:: sql
:caption: Oracle PL/SQL — not supported
:caption: Oracle anonymous block — supported with Dialect.ORACLE

DECLARE
num NUMBER;
Expand All @@ -23,21 +23,25 @@ This is the one substantial gap. JSQLParser parses **statements**, not stored pr
dbms_output.put_line('The number is ' || num);
END;

.. code-block:: sql
:caption: PostgreSQL anonymous block — not supported
.. code-block:: postgresql
:caption: PostgreSQL DO — supported with Dialect.POSTGRESQL; body remains opaque

DO $$
BEGIN
RAISE NOTICE 'hello';
END
$$;

Specifically not parsed: typed local variable declarations, ``CURSOR`` declarations and ``OPEN`` / ``FETCH`` / ``CLOSE``, ``EXCEPTION`` handlers, ``WHILE`` and ``FOR`` loops, ``ELSIF``, and assignment (``:=``).
The PostgreSQL example produces a ``DoStatement`` whose ``getCode()`` is a ``StringValue``. The body, quotes and dollar tag are preserved, and statements following the block are parsed separately. PL/pgSQL declarations, conditions and statements inside that literal are not exposed as child AST nodes. Table discovery therefore rejects the opaque body rather than reporting an incomplete table list.

Full procedural-language coverage, including cursors, loops and ``ELSIF``, remains outside the supported subset.

What *is* supported:

- ``BEGIN .. END`` blocks and ``IF .. ELSE`` around ordinary statements (``Block``, ``IfElseStatement``)
- ``DECLARE @variable`` in the T-SQL sense (``DeclareStatement``)
- PostgreSQL ``DO`` wrappers with the body preserved as a string literal (``DoStatement``)
- Selected Oracle anonymous blocks, including variable declarations, assignments and exception handlers (``OracleBlock``); see :ref:`Oracle anonymous blocks`

Routine and trigger definitions
---------------------------------------
Expand Down Expand Up @@ -81,4 +85,4 @@ If you hit one, you do not have to abandon the parse:

The offending statement comes back as an ``UnsupportedStatement`` holding its original text, and the rest of the script parses normally. See :ref:`Handle Parse Errors`.

Before concluding something is unsupported, check the parser features: square brackets, backslash escapes, double-quoted strings and hash comments are all **off by default** and are the most common cause of a "not supported" report that is really a dialect setting. See :ref:`Choose a Dialect`.
Before concluding something is unsupported, check the parser features: square brackets, backslash escapes, double-quoted strings and hash comments are all **off by default** and are the most common cause of a "not supported" report that is really a dialect setting. See :ref:`Choose a Dialect`.
16 changes: 16 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,22 @@ Expression visitors can inspect or replace the body literal. Feature analysis
reports ``OPAQUE``; table discovery rejects this statement because the body's
table accesses are unknown. Validation checks the ``doStatement`` capability,
without validating the procedural language inside the literal.

Enable the PostgreSQL dialect when parsing a script containing a ``DO`` block:

.. code-block:: java

Statements statements = CCJSqlParserUtil.parseStatements(
"DO $$BEGIN RAISE NOTICE 'hello'; END$$; SELECT 1;",
parser -> parser.withDialect(Dialect.POSTGRESQL));
DoStatement block = (DoStatement) statements.get(0);
String body = block.getCode().getValue();
// body: BEGIN RAISE NOTICE 'hello'; END
// statements.get(1) is the following SELECT.

Semicolons and SQL statements inside the body remain part of its string literal;
they do not split the surrounding script into additional statements.

With ``Dialect.POSTGRESQL``, ``#`` terminates an unquoted identifier, so JSON
operators such as ``js#>>'{a}'`` and ``js#>'{a}'`` work without surrounding
spaces. Quote identifiers containing ``#``, for example ``"js#"``. Other
Expand Down
39 changes: 31 additions & 8 deletions src/test/java/net/sf/jsqlparser/statement/DoStatementTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.*;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
Expand Down Expand Up @@ -56,14 +58,35 @@ void roundTripsBodyAndLanguagePosition(String sql) throws Exception {

@Test
void preservesProceduralBodyAndFollowingStatementsIssue1946() throws Exception {
String body = "$$\nBEGIN\n IF NOT EXISTS (SELECT 1 FROM comm.permission_operation) THEN\n"
+ " INSERT INTO comm.permission_operation (permission_operation_id) VALUES (1) "
+ "ON CONFLICT (permission_operation_id) DO NOTHING;\n END IF;\nEND $$";
Statements statements = CCJSqlParserUtil.parseStatements("DO " + body + "; SELECT 1;",
p -> p.withDialect(Dialect.POSTGRESQL));
assertEquals(2, statements.size());
assertEquals(body, ((DoStatement) statements.get(0)).getCode().toString());
assertEquals("SELECT 1", statements.get(1).toString());
String sql;
try (InputStream input = getClass().getResourceAsStream("/postgresql/do-issue1946.sql")) {
assertNotNull(input);
sql = new String(input.readAllBytes(), StandardCharsets.UTF_8).strip();
}
String body = sql.substring(sql.indexOf("$$"), sql.lastIndexOf("$$") + 2);
Statements statements =
CCJSqlParserUtil.parseStatements("SELECT 0;\n" + sql + "\nSELECT 1;",
p -> p.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(false));
assertEquals(3, statements.size());
assertEquals("SELECT 0", statements.get(0).toString());
DoStatement block = assertInstanceOf(DoStatement.class, statements.get(1));
assertEquals(body, block.getCode().toString());
assertEquals(body.substring(2, body.length() - 2), block.getCode().getValue());
assertEquals("SELECT 1", statements.get(2).toString());

StringBuilder output = new StringBuilder();
for (Statement statement : statements) {
statement.accept(new StatementDeParser(output), null);
output.append(";\n");
}
assertEquals("SELECT 0;\nDO " + body + ";\nSELECT 1;\n", output.toString());
Statements reparsed = CCJSqlParserUtil.parseStatements(output.toString(),
p -> p.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(false));
assertEquals(3, reparsed.size());
assertEquals(body,
assertInstanceOf(DoStatement.class, reparsed.get(1)).getCode().toString());
assertEquals("SELECT 0", reparsed.get(0).toString());
assertEquals("SELECT 1", reparsed.get(2).toString());
}

@Test
Expand Down
41 changes: 41 additions & 0 deletions src/test/resources/postgresql/do-issue1946.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
-- #%L
-- JSQLParser library
-- %%
-- Copyright (C) 2004 - 2026 JSQLParser
-- %%
-- Dual licensed under GNU LGPL 2.1 or Apache License 2.0
-- #L%
---
DO $$
BEGIN
IF NOT EXISTS( select 1 from comm.permission_operation where permission_operation_code = 'ecg_report_time_modify') and EXISTS( select 1 from comm.permission where permission_code = 'data_modify')
THEN
INSERT INTO comm.permission_operation
(permission_operation_id,
permission_id,
permission_operation_code,
permission_operation_name,
"type",
"version",
his_org_id,
his_creater_id,
his_creater_name,
his_create_time,
his_updater_id,
his_update_time)
VALUES
((select max(permission_operation_id) + 1 from comm.permission_operation),
(select permission_id from comm.permission where permission_code = 'data_modify' limit 1),
'ecg_report_time_modify',
'心电报告时间修改',
'1',
0,
(select his_org_id from comm.hospital limit 1),
1,
'系统管理员',
now(),
1,
now()) on conflict(permission_operation_id) do nothing;
END IF;
END $$;
Loading