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
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ public enum Feature {
*/
oracleBlock,

execute, executeExec, executeCall, executeExecute,
execute, executeExec, executeCall, executeExecute, doStatement,

/**
* SQL "EXECUTE" statement is allowed
Expand Down
78 changes: 78 additions & 0 deletions src/main/java/net/sf/jsqlparser/statement/DoStatement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement;

import java.util.function.Consumer;
import net.sf.jsqlparser.expression.StringValue;

/** A PostgreSQL anonymous routine. The language-specific body remains a string literal. */
public class DoStatement implements Statement {
private StringValue code;
private String language;
private boolean languageBeforeCode;

public StringValue getCode() {
return code;
}

public void setCode(StringValue code) {
this.code = code;
}

public DoStatement withCode(StringValue code) {
setCode(code);
return this;
}

/** Returns the explicit language, or null when omitted. */
public String getLanguage() {
return language;
}

public void setLanguage(String language) {
this.language = language;
}

public DoStatement withLanguage(String language) {
setLanguage(language);
return this;
}

public boolean isLanguageBeforeCode() {
return languageBeforeCode;
}

public void setLanguageBeforeCode(boolean languageBeforeCode) {
this.languageBeforeCode = languageBeforeCode;
}

public StringBuilder appendTo(StringBuilder builder, Consumer<StringValue> codePrinter) {
builder.append("DO ");
if (languageBeforeCode && language != null) {
builder.append("LANGUAGE ").append(language).append(' ');
}
codePrinter.accept(code);
if (!languageBeforeCode && language != null) {
builder.append(" LANGUAGE ").append(language);
}
return builder;
}

@Override
public <T, S> T accept(StatementVisitor<T> visitor, S context) {
return visitor.visit(this, context);
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
return appendTo(builder, value -> builder.append(value)).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,13 @@ public <S> Void visit(Execute execute, S context) {
return null;
}

@Override
public <S> Void visit(DoStatement statement, S context) {
analysis.claimTopLevel();
analysis.opaque("DO");
return null;
}

@Override
public <S> Void visit(CreateFunctionalStatement createFunctionalStatement, S context) {
analysis.claimTopLevel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ default void visit(Statements statements) {

<S> T visit(Execute execute, S context);

default <S> T visit(DoStatement statement, S context) {
return null;
}

default void visit(DoStatement statement) {
this.visit(statement, null);
}

default void visit(Execute execute) {
this.visit(execute, null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ public <S> T visit(Execute execute, S context) {
return null;
}

@Override
public <S> T visit(DoStatement statement, S context) {
expressionVisitor.visitExpression(statement.getCode(), context);
return null;
}

@Override
public <S> T visit(LockStatement lock, S context) {

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
import net.sf.jsqlparser.statement.CreateFunctionalStatement;
import net.sf.jsqlparser.statement.DeclareStatement;
import net.sf.jsqlparser.statement.DescribeStatement;
import net.sf.jsqlparser.statement.DoStatement;
import net.sf.jsqlparser.statement.ExplainStatement;
import net.sf.jsqlparser.statement.IfElseStatement;
import net.sf.jsqlparser.statement.OutputClause;
Expand Down Expand Up @@ -1564,6 +1565,12 @@ public <S> Void visit(CreateIndex createIndex, S context) {
return null;
}

@Override
public <S> Void visit(DoStatement statement, S context) {
throwUnsupported(statement);
return null;
}

@Override
public void visit(CreateIndex createIndex) {
StatementVisitor.super.visit(createIndex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import net.sf.jsqlparser.statement.CreateFunctionalStatement;
import net.sf.jsqlparser.statement.DeclareStatement;
import net.sf.jsqlparser.statement.DescribeStatement;
import net.sf.jsqlparser.statement.DoStatement;
import net.sf.jsqlparser.statement.ExplainStatement;
import net.sf.jsqlparser.statement.IfElseStatement;
import net.sf.jsqlparser.statement.PurgeStatement;
Expand Down Expand Up @@ -151,6 +152,11 @@ public <S> StringBuilder visit(CreateIndex createIndex, S context) {
return builder;
}

@Override
public <S> StringBuilder visit(DoStatement statement, S context) {
return statement.appendTo(builder, code -> code.accept(expressionDeParser, context));
}

@Override
public <S> StringBuilder visit(CreateTable createTable, S context) {
CreateTableDeParser createTableDeParser = new CreateTableDeParser(this, builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum PostgresqlVersion implements Version {
EnumSet.of(// supported if used with jdbc
Feature.jdbcParameter,
Feature.jdbcNamedParameter, // expressions
Feature.doStatement,
Feature.exprLike,
Feature.exprSimilarTo, // https://www.postgresql.org/docs/current/sql-select.html
Feature.select,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import net.sf.jsqlparser.statement.Block;
import net.sf.jsqlparser.statement.Commit;
import net.sf.jsqlparser.statement.CreateFunctionalStatement;
import net.sf.jsqlparser.statement.DoStatement;
import net.sf.jsqlparser.statement.DeclareStatement;
import net.sf.jsqlparser.statement.DescribeStatement;
import net.sf.jsqlparser.statement.ExplainStatement;
Expand Down Expand Up @@ -232,6 +233,12 @@ public <S> Void visit(Execute execute, S context) {
return null;
}

@Override
public <S> Void visit(DoStatement statement, S context) {
validateFeature(Feature.doStatement);
return null;
}

@Override
public <S> Void visit(SetStatement set, S context) {
getValidator(SetStatementValidator.class).validate(set);
Expand Down
23 changes: 23 additions & 0 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -2782,6 +2782,9 @@ Statement SingleStatement() :
|
stm = Execute()
|
LOOKAHEAD({ getToken(1).kind == K_DO && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
stm = DoStatement()
|
stm = Set()
|
stm = Reset()
Expand Down Expand Up @@ -2901,6 +2904,26 @@ OracleBlock.ExceptionHandler OracleExceptionHandler():
<K_THEN> body=OracleBlockStatements() { handler.setStatements(body); return handler; }
}

DoStatement DoStatement():
{
DoStatement statement = new DoStatement();
String language;
Token code;
}
{
<K_DO>
(
LOOKAHEAD({ isKeywordAhead("LANGUAGE") })
AccessKeyword("LANGUAGE") language=RelObjectName() code=<S_CHAR_LITERAL>
{ statement.setLanguage(language); statement.setLanguageBeforeCode(true); }
|
code=<S_CHAR_LITERAL>
[ LOOKAHEAD({ isKeywordAhead("LANGUAGE") }) AccessKeyword("LANGUAGE")
language=RelObjectName() { statement.setLanguage(language); } ]
)
{ statement.setCode(new StringValue(code.image)); return statement; }
}

Block Block() #Block : {
Statements stmts = new Statements();
List<Statement> list = new ArrayList<Statement>();
Expand Down
10 changes: 10 additions & 0 deletions src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,16 @@ uses the existing ``Update`` model's ``fromItem`` and ``joins`` properties.
Table discovery and metadata validation recognize a target alias declared in
that FROM clause. Other dialects retain the existing FROM-after-SET syntax.

``Dialect.POSTGRESQL`` enables ``DO [LANGUAGE name] code [LANGUAGE name]``,
with the language clause allowed once, before or after the body.
``DoStatement.getCode()`` is a ``StringValue`` that preserves the literal's
quotes, dollar tag and body text. The optional language and its position have
separate properties; an omitted language remains unspecified in the AST.
The body is language-specific source, not a parsed PL/pgSQL statement tree.
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.
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
127 changes: 127 additions & 0 deletions src/test/java/net/sf/jsqlparser/statement/DoStatementTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static org.junit.jupiter.api.Assertions.*;

import java.util.ArrayList;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.feature.Feature;
import net.sf.jsqlparser.parser.feature.FeatureConfiguration;
import net.sf.jsqlparser.statement.select.SelectVisitorAdapter;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import net.sf.jsqlparser.util.validation.Validation;
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class DoStatementTest {
@ParameterizedTest
@ValueSource(strings = {"DO $$BEGIN NULL; END$$", "DO LANGUAGE plpgsql $$BEGIN NULL; END$$",
"DO $body$BEGIN NULL; END$body$ LANGUAGE plpgsql", "DO 'BEGIN NULL; END'",
"DO LANGUAGE plpython3u 'print(1)'"})
void roundTripsBodyAndLanguagePosition(String sql) throws Exception {
DoStatement statement = (DoStatement) assertSqlCanBeParsedAndDeparsed(sql, false,
p -> p.withDialect(Dialect.POSTGRESQL));
assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(),
p -> p.withDialect(Dialect.POSTGRESQL)).toString());
assertEquals(sql.startsWith("DO LANGUAGE"), statement.isLanguageBeforeCode());
assertEquals(
sql.contains("LANGUAGE") ? sql.contains("plpython") ? "plpython3u" : "plpgsql"
: null,
statement.getLanguage());
assertTrue(statement.getFeatures().isOpaque());
assertTrue(statement.getFeatures().mayModifyData());
assertThrows(UnsupportedOperationException.class,
() -> new TablesNamesFinder().getTables(statement));
}

@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());
}

@Test
void supportsBodyVisitorsAndAstEdits() throws Exception {
DoStatement statement = (DoStatement) CCJSqlParserUtil.parse("DO $$BEGIN NULL; END$$",
p -> p.withDialect(Dialect.POSTGRESQL));
List<String> bodies = new ArrayList<>();
statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(
new ExpressionVisitorAdapter<Void>() {
@Override
public <S> Void visit(StringValue value, S context) {
assertEquals("context", context);
bodies.add(value.toString());
return null;
}
})), "context");
assertEquals(List.of("$$BEGIN NULL; END$$"), bodies);
statement.withCode(new StringValue("$new$BEGIN PERFORM 1; END$new$"))
.withLanguage("plpgsql");
statement.setLanguageBeforeCode(true);
assertEquals("DO LANGUAGE plpgsql $new$BEGIN PERFORM 1; END$new$", statement.toString());
StringBuilder output = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(StringValue value, S context) {
return getBuilder().append("$$BEGIN NULL; END$$");
}
};
statement.accept(new StatementDeParser(expressions, new SelectDeParser(), output));
assertEquals("DO LANGUAGE plpgsql $$BEGIN NULL; END$$", output.toString());
}

@Test
void gatesDialectAndRejectsMalformedWrappers() throws Exception {
String sql = "DO $$BEGIN NULL; END$$";
assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
for (Dialect dialect : Dialect.values()) {
if (dialect != Dialect.POSTGRESQL) {
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(dialect)));
}
}
for (String malformed : List.of("DO", "DO LANGUAGE plpgsql", "DO 123",
"DO LANGUAGE plpgsql $$x$$ LANGUAGE plpgsql", "DO $tag$unterminated")) {
assertThrows(JSQLParserException.class,
() -> CCJSqlParserUtil.parse(malformed,
p -> p.withDialect(Dialect.POSTGRESQL)));
}
}

@Test
void validatesWrapperCapabilityWithoutClaimingBodyValidation() {
FeatureConfiguration config = new FeatureConfiguration()
.setValue(Feature.dialect, Dialect.POSTGRESQL.name());
assertTrue(new Validation(config, List.of(PostgresqlVersion.V10),
"DO $$arbitrary language body$$")
.validate().isEmpty());
assertFalse(new Validation(config, List.of(new FeaturesAllowed(Feature.select)),
"DO $$arbitrary language body$$").validate().isEmpty());
}
}
Loading