diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 6d0492fe7..7659b2597 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -453,6 +453,8 @@ public enum Feature { * * @see Execute */ + oracleBlock, + execute, executeExec, executeCall, executeExecute, /** diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 5b33177f5..111c79c8d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.grant.Revoke; @@ -986,4 +990,24 @@ public Void visit(AlterSubscription statement, S context) { } return null; } + + @Override + public Void visit(OracleBlock block, S context) { + analysis.claimTopLevel(); + return super.visit(block, context); + } + + @Override + public Void visit(OracleAssignment assignment, S context) { + analysis.claimTopLevel(); + // Assignment to a local variable is not a database write. + return super.visit(assignment, context); + } + + @Override + public Void visit(OracleNullStatement statement, S context) { + analysis.claimTopLevel(); + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 5e6d6bd19..2cf0985e7 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.grant.Revoke; @@ -520,4 +524,29 @@ default T visit(AlterSubscription statement, S context) { default void visit(AlterSubscription statement) { visit(statement, null); } + + default T visit(OracleBlock block, S context) { + return visit((Block) block, context); + } + + default void visit(OracleBlock block) { + visit(block, null); + } + + default T visit(OracleAssignment assignment, S context) { + return null; + } + + default void visit(OracleAssignment assignment) { + visit(assignment, null); + } + + default T visit(OracleNullStatement statement, S context) { + return null; + } + + default void visit(OracleNullStatement statement) { + visit(statement, null); + } + } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index ef5ed487f..a8e143a6c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -9,6 +9,9 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; + import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.role.RoleOption; @@ -421,7 +424,7 @@ public T visit(Statements statements, S context) { @Override public T visit(Execute execute, S context) { - + expressionVisitor.visitExpression(execute.getExprList(), context); return null; } @@ -769,4 +772,19 @@ public T visit(AlterSubscription statement, S context) { }); return null; } + + @Override + public T visit(OracleBlock block, S context) { + block.visitChildren(expression -> expression.accept(expressionVisitor, context), + statement -> statement.accept(this, context)); + return null; + } + + @Override + public T visit(OracleAssignment assignment, S context) { + assignment.getTarget().accept(expressionVisitor, context); + assignment.getValue().accept(expressionVisitor, context); + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java b/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java index 59bc05837..cb2d8439f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java +++ b/src/main/java/net/sf/jsqlparser/statement/execute/Execute.java @@ -75,7 +75,10 @@ public String toString() { } public StringBuilder appendTo(StringBuilder builder, Consumer expressionPrinter) { - builder.append(execType.name()).append(' ').append(name); + if (execType != ExecType.IMPLICIT) { + builder.append(execType.name()).append(' '); + } + builder.append(name); if (exprList != null) { builder.append(' '); boolean brackets = exprList instanceof ParenthesedExpressionList; @@ -111,7 +114,7 @@ public Execute withExprList(ExpressionList exprList) { } public enum ExecType { - EXECUTE, EXEC, CALL; + EXECUTE, EXEC, CALL, IMPLICIT; public static ExecType from(String type) { return Enum.valueOf(ExecType.class, type.toUpperCase(Locale.ROOT)); diff --git a/src/main/java/net/sf/jsqlparser/statement/oracle/OracleAssignment.java b/src/main/java/net/sf/jsqlparser/statement/oracle/OracleAssignment.java new file mode 100644 index 000000000..857df3245 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/oracle/OracleAssignment.java @@ -0,0 +1,60 @@ +/*- + * #%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.oracle; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** Assignment to an Oracle block variable, record field or bind parameter. */ +public class OracleAssignment implements Statement { + private Expression target; + private Expression value; + + public OracleAssignment(Expression target, Expression value) { + this.target = target; + this.value = value; + } + + public Expression getTarget() { + return target; + } + + public void setTarget(Expression target) { + this.target = target; + } + + public Expression getValue() { + return value; + } + + public void setValue(Expression value) { + this.value = value; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + printer.accept(target); + builder.append(" := "); + printer.accept(value); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/oracle/OracleBlock.java b/src/main/java/net/sf/jsqlparser/statement/oracle/OracleBlock.java new file mode 100644 index 000000000..83a669270 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/oracle/OracleBlock.java @@ -0,0 +1,191 @@ +/*- + * #%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.oracle; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.statement.Block; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +/** An Oracle anonymous block with variable declarations and exception handlers. */ +public class OracleBlock extends Block { + private final List declarations = new ArrayList<>(); + private final List exceptionHandlers = new ArrayList<>(); + + public List getDeclarations() { + return declarations; + } + + public List getExceptionHandlers() { + return exceptionHandlers; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public void visitChildren(Consumer expressions, Consumer statements) { + for (VariableDeclaration declaration : declarations) { + if (declaration.getInitializer() != null) { + expressions.accept(declaration.getInitializer()); + } + } + if (getStatements() != null) { + getStatements().forEach(statements); + } + exceptionHandlers.forEach(handler -> handler.getStatements().forEach(statements)); + } + + public StringBuilder appendTo(StringBuilder builder, Consumer expressions, + Consumer statements) { + if (!declarations.isEmpty()) { + builder.append("DECLARE\n"); + declarations.forEach(declaration -> { + declaration.appendTo(builder, expressions); + builder.append(";\n"); + }); + } + builder.append("BEGIN\n"); + appendStatements(builder, getStatements(), statements); + if (!exceptionHandlers.isEmpty()) { + builder.append("EXCEPTION\n"); + for (ExceptionHandler handler : exceptionHandlers) { + builder.append("WHEN ").append(String.join(" OR ", handler.getExceptions())) + .append(" THEN\n"); + appendStatements(builder, handler.getStatements(), statements); + } + } + builder.append("END"); + if (hasSemicolonAfterEnd()) { + builder.append(';'); + } + return builder; + } + + private static void appendStatements(StringBuilder builder, Statements body, + Consumer printer) { + if (body == null) { + return; + } + for (Statement statement : body) { + printer.accept(statement); + if (!(statement instanceof Block) || !((Block) statement).hasSemicolonAfterEnd()) { + builder.append(';'); + } + builder.append('\n'); + } + } + + @Override + public StringBuilder appendTo(StringBuilder builder) { + return appendTo(builder, builder::append, builder::append); + } + + public static class VariableDeclaration implements java.io.Serializable { + private String name; + private ColDataType dataType; + private boolean constant; + private boolean notNull; + private String initializerOperator = ":="; + private Expression initializer; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ColDataType getDataType() { + return dataType; + } + + public void setDataType(ColDataType type) { + dataType = type; + } + + public boolean isConstant() { + return constant; + } + + public void setConstant(boolean value) { + constant = value; + } + + public boolean isNotNull() { + return notNull; + } + + public void setNotNull(boolean value) { + notNull = value; + } + + public String getInitializerOperator() { + return initializerOperator; + } + + public void setInitializerOperator(String operator) { + initializerOperator = operator; + } + + public Expression getInitializer() { + return initializer; + } + + public void setInitializer(Expression expression) { + initializer = expression; + } + + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append(name).append(' '); + if (constant) { + builder.append("CONSTANT "); + } + builder.append(dataType); + if (notNull) { + builder.append(" NOT NULL"); + } + if (initializer != null) { + builder.append(' ').append(initializerOperator).append(' '); + printer.accept(initializer); + } + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + } + public static class ExceptionHandler implements java.io.Serializable { + private final List exceptions = new ArrayList<>(); + private Statements statements; + + public List getExceptions() { + return exceptions; + } + + public Statements getStatements() { + return statements; + } + + public void setStatements(Statements statements) { + this.statements = statements; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/oracle/OracleNullStatement.java b/src/main/java/net/sf/jsqlparser/statement/oracle/OracleNullStatement.java new file mode 100644 index 000000000..9d73405fb --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/oracle/OracleNullStatement.java @@ -0,0 +1,26 @@ +/*- + * #%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.oracle; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** A PL/SQL NULL statement, which deliberately performs no action. */ +public class OracleNullStatement implements Statement { + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + @Override + public String toString() { + return "NULL"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 283856680..47bec8fc0 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.grant.Revoke; @@ -2630,4 +2634,23 @@ public Void visit(AlterSubscription statement, S context) { return null; } + + @Override + public Void visit(OracleBlock block, S context) { + block.visitChildren(expression -> expression.accept(this, context), + statement -> statement.accept(this, context)); + return null; + } + + @Override + public Void visit(OracleAssignment assignment, S context) { + assignment.getValue().accept(this, context); + return null; + } + + @Override + public Void visit(OracleNullStatement statement, S context) { + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 3bd785a8c..d28926802 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.grant.Revoke; @@ -657,4 +661,22 @@ public StringBuilder visit(AlterSubscription statement, S context) { statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); return builder; } + + @Override + public StringBuilder visit(OracleBlock block, S context) { + return block.appendTo(builder, expression -> expression.accept(expressionDeParser, context), + statement -> statement.accept(this, context)); + } + + @Override + public StringBuilder visit(OracleAssignment assignment, S context) { + return assignment.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(OracleNullStatement statement, S context) { + return builder.append("NULL"); + } + } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java index 37b193820..53d9bdf74 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/OracleVersion.java @@ -24,20 +24,14 @@ */ public enum OracleVersion implements Version { V19C("19c", - EnumSet.of( - // supported if used with jdbc + EnumSet.of(// supported if used with jdbc Feature.jdbcParameter, - Feature.jdbcNamedParameter, - // expressions - Feature.exprLike, - // common features + Feature.jdbcNamedParameter, // expressions + Feature.exprLike, // common features // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html - Feature.select, - // https://www.oracletutorial.com/oracle-basics/oracle-group-by/ - Feature.selectGroupBy, Feature.function, - // https://www.oracletutorial.com/oracle-basics/oracle-grouping-sets/ - Feature.selectGroupByGroupingSets, - // https://www.oracletutorial.com/oracle-basics/oracle-having/ + Feature.select, // https://www.oracletutorial.com/oracle-basics/oracle-group-by/ + Feature.selectGroupBy, Feature.function, // https://www.oracletutorial.com/oracle-basics/oracle-grouping-sets/ + Feature.selectGroupByGroupingSets, // https://www.oracletutorial.com/oracle-basics/oracle-having/ Feature.selectHaving, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html @@ -91,8 +85,7 @@ public enum OracleVersion implements Version { // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html Feature.insert, Feature.insertValues, - Feature.values, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html + Feature.values, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/INSERT.html // see "single_table_insert" Feature.insertFromSelect, @@ -108,38 +101,25 @@ public enum OracleVersion implements Version { // https://www.oracletutorial.com/oracle-basics/oracle-truncate-table/ Feature.truncate, - Feature.drop, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-TABLE.html - Feature.dropTable, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-INDEX.html - Feature.dropIndex, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-VIEW.html - Feature.dropView, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-SEQUENCE.html + Feature.drop, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-TABLE.html + Feature.dropTable, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-INDEX.html + Feature.dropIndex, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-VIEW.html + Feature.dropView, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/DROP-SEQUENCE.html Feature.dropSequence, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ALTER-TABLE.html - Feature.alterTable, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ALTER-SEQUENCE.html - Feature.alterSequence, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/EXECUTE-IMMEDIATE-statement.html - Feature.executeStatementImmediate, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-VIEW.html + Feature.alterTable, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/ALTER-SEQUENCE.html + Feature.alterSequence, // https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/EXECUTE-IMMEDIATE-statement.html + Feature.executeStatementImmediate, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-VIEW.html Feature.createView, - Feature.createViewForce, Feature.createOrReplaceView, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW.htm - Feature.createViewMaterialized, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html + Feature.createViewForce, Feature.createOrReplaceView, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-MATERIALIZED-VIEW.htm + Feature.createViewMaterialized, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TABLE.html Feature.createTable, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, - Feature.createTableFromSelect, Feature.createTableRowMovement, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html - Feature.createIndex, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SEQUENCE.html - Feature.createSequence, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TRIGGER.html - Feature.createTrigger, - // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SCHEMA.html + Feature.createTableFromSelect, Feature.createTableRowMovement, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-INDEX.html + Feature.createIndex, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SEQUENCE.html + Feature.createSequence, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-TRIGGER.html + Feature.createTrigger, // https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/CREATE-SCHEMA.html Feature.createSchema, Feature.commit, @@ -158,7 +138,7 @@ public enum OracleVersion implements Version { Feature.merge, Feature.createFunction, Feature.createProcedure, Feature.functionalStatement, - Feature.block, + Feature.block, Feature.oracleBlock, Feature.execute, Feature.declare, // special oracle features diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java index 3c91b9128..968bd13b1 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExecuteValidator.java @@ -25,6 +25,8 @@ public class ExecuteValidator extends AbstractValidator { public void validate(Execute execute) { for (ValidationCapability c : getCapabilities()) { validateFeature(c, Feature.execute); + validateFeature(c, ExecType.IMPLICIT.equals(execute.getExecType()), + Feature.oracleBlock); validateFeature(c, ExecType.EXECUTE.equals(execute.getExecType()), Feature.executeExecute); validateFeature(c, ExecType.EXEC.equals(execute.getExecType()), Feature.executeExec); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index d4709c761..08684191f 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import net.sf.jsqlparser.statement.role.CreateRole; import net.sf.jsqlparser.statement.role.AlterRole; import net.sf.jsqlparser.statement.role.RoleOption; @@ -803,4 +807,28 @@ public Void visit(AlterSubscription statement, S context) { return null; } + + @Override + public Void visit(OracleBlock block, S context) { + validateFeature(Feature.block); + validateFeature(Feature.oracleBlock); + block.visitChildren(this::validateOptionalExpression, + statement -> statement.accept(this, context)); + return null; + } + + @Override + public Void visit(OracleAssignment assignment, S context) { + validateFeature(Feature.oracleBlock); + validateOptionalExpression(assignment.getTarget()); + validateOptionalExpression(assignment.getValue()); + return null; + } + + @Override + public Void visit(OracleNullStatement statement, S context) { + validateFeature(Feature.oracleBlock); + return null; + } + } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e89a86094..ce173079f 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -33,6 +33,10 @@ PARSER_BEGIN(CCJSqlParser) package net.sf.jsqlparser.parser; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; + import java.lang.reflect.Field; import java.lang.Integer; @@ -2669,6 +2673,10 @@ Statement SingleStatement() : } { ( + LOOKAHEAD({ Dialect.ORACLE.name().equals(getAsString(Feature.dialect)) + && (getToken(1).kind == K_DECLARE || getToken(1).kind == K_BEGIN) }) + stm = OracleBlock() + | LOOKAHEAD(3) ( [ LOOKAHEAD(2) with=WithList() ] ( @@ -2744,6 +2752,86 @@ Statement SingleStatement() : { return stm; } } +OracleBlock OracleBlock(): +{ + OracleBlock block = new OracleBlock(); + OracleBlock.VariableDeclaration declaration; + OracleBlock.ExceptionHandler handler; + Statements statements; +} +{ + [ declaration=OracleVariableDeclaration() { block.getDeclarations().add(declaration); } + ( LOOKAHEAD({ getToken(1).kind != K_BEGIN }) + declaration=OracleVariableDeclaration() { block.getDeclarations().add(declaration); } )* ] + statements=OracleBlockStatements() { block.setStatements(statements); } + [ LOOKAHEAD({ isKeywordAhead("EXCEPTION") }) AccessKeyword("EXCEPTION") + handler=OracleExceptionHandler() { block.getExceptionHandlers().add(handler); } + ( handler=OracleExceptionHandler() { block.getExceptionHandlers().add(handler); } )* ] + + { block.setSemicolonAfterEnd(true); return block; } +} + +OracleBlock.VariableDeclaration OracleVariableDeclaration(): +{ + OracleBlock.VariableDeclaration declaration = new OracleBlock.VariableDeclaration(); + String name; ColDataType dataType; Expression initializer; +} +{ + name=RelObjectName() { declaration.setName(name); } + [ LOOKAHEAD({ isKeywordAhead("CONSTANT") }) AccessKeyword("CONSTANT") { declaration.setConstant(true); } ] + dataType=ColDataType() { declaration.setDataType(dataType); } + [ { declaration.setNotNull(true); } ] + [ ( ":=" | { declaration.setInitializerOperator("DEFAULT"); } ) + initializer=Expression() { declaration.setInitializer(initializer); } ] + + { requireDdlSyntax((!declaration.isConstant() && !declaration.isNotNull()) || declaration.getInitializer() != null, + "A CONSTANT or NOT NULL variable requires an initializer"); return declaration; } +} + +Statements OracleBlockStatements(): +{ Statements statements = new Statements(); Statement statement; } +{ + statement=OracleBlockStatement() { statements.add(statement); } + ( LOOKAHEAD({ getToken(1).kind != K_END && getToken(1).kind != K_WHEN && !isKeywordAhead("EXCEPTION") }) + statement=OracleBlockStatement() { statements.add(statement); } )* + { return statements; } +} + +Statement OracleBlockStatement(): +{ Statement statement; Expression target; Expression value; } +{ + ( + LOOKAHEAD({ getToken(1).kind == K_DECLARE || getToken(1).kind == K_BEGIN }) statement=OracleBlock() + | LOOKAHEAD(Column() ":=") target=Column() ":=" value=Expression() + { statement = new OracleAssignment(target, value); } + | LOOKAHEAD(JdbcNamedParameter() ":=") target=JdbcNamedParameter() ":=" value=Expression() + { statement = new OracleAssignment(target, value); } + | { statement = new OracleNullStatement(); } + | LOOKAHEAD(ColumnIdentifier() ("(" | )) + statement=OracleImplicitCall() + | statement=SingleStatement() + ) + { return statement; } +} + +Execute OracleImplicitCall(): +{ Execute call = new Execute(); ObjectNames name; ExpressionList arguments; } +{ + name=ColumnIdentifier() { call.setName(name.getNames()); call.setExecType(Execute.ExecType.IMPLICIT); + requireDdlSyntax(!Arrays.asList("RAISE", "RETURN", "GOTO", "EXIT", "CONTINUE", "LOOP", "WHILE", "FOR") + .contains(call.getName().toUpperCase(java.util.Locale.ROOT)), "Unsupported PL/SQL control statement"); } + [ LOOKAHEAD("(") arguments=ExpressionList() { call.setExprList(arguments); } ] + { return call; } +} + +OracleBlock.ExceptionHandler OracleExceptionHandler(): +{ OracleBlock.ExceptionHandler handler = new OracleBlock.ExceptionHandler(); String name; Statements body; } +{ + name=TypeDdlName() { handler.getExceptions().add(name); } + ( name=TypeDdlName() { handler.getExceptions().add(name); } )* + body=OracleBlockStatements() { handler.setStatements(body); return handler; } +} + Block Block() #Block : { Statements stmts = new Statements(); List list = new ArrayList(); diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 6d3995c0c..122e3229e 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -872,3 +872,19 @@ References: `CREATE ROLE `_, `ALTER DEFAULT PRIVILEGES `_, `CREATE TRIGGER `_. + +Oracle anonymous blocks +----------------------- + +With ``Dialect.ORACLE``, ``OracleBlock`` extends ``Block`` with variable declarations +and exception handlers. Initializers and ``OracleAssignment`` values are expressions; +nested blocks and handler bodies contain statements. Calls without ``CALL`` use +``Execute.ExecType.IMPLICIT``, preserving qualified names, parentheses and bind arguments. +``OracleNullStatement`` represents the PL/SQL ``NULL`` statement. Implicit calls are +recognized inside Oracle blocks, so application procedure names need no keyword registration. + +Shared traversal and rendering include declaration initializers, assignments and exception +handler bodies. Procedure side effects remain unknown; table discovery reports unsupported +procedure calls, and feature analysis remains conservative. This covers anonymous blocks +with variable declarations, SQL statements, assignments, calls, nesting and handlers, not +all PL/SQL declarations, loops, packages or procedure definitions. diff --git a/src/test/java/net/sf/jsqlparser/statement/OracleBlockTest.java b/src/test/java/net/sf/jsqlparser/statement/OracleBlockTest.java new file mode 100644 index 000000000..07ddbd592 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/OracleBlockTest.java @@ -0,0 +1,182 @@ +/*- + * #%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 org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.JdbcNamedParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.execute.Execute; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleBlock; +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.OracleVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class OracleBlockTest { + private static OracleBlock parse(String sql) throws Exception { + OracleBlock block = + (OracleBlock) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.ORACLE)); + StringBuilder output = new StringBuilder(); + block.accept(new StatementDeParser(output)); + assertEquals(block.toString(), output.toString()); + assertEquals(block.toString(), CCJSqlParserUtil + .parse(output.toString(), p -> p.withDialect(Dialect.ORACLE)).toString()); + return block; + } + + @Test + void supportsIssue2007BindCalls() throws Exception { + OracleBlock block = + parse("BEGIN modifyParams(:BP_00000_v, :BP_00001_v, :BP_00012_v); END;"); + Execute call = assertInstanceOf(Execute.class, block.getStatements().get(0)); + assertEquals(Execute.ExecType.IMPLICIT, call.getExecType()); + assertEquals("modifyParams", call.getName()); + assertEquals(3, call.getExprList().size()); + List names = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(JdbcNamedParameter parameter, S context) { + assertEquals("ctx", context); + names.add(parameter.getName()); + return null; + } + }; + block.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), "ctx"); + assertEquals(List.of("BP_00000_v", "BP_00001_v", "BP_00012_v"), names); + assertTrue(block.getFeatures().isOpaque()); + assertTrue(block.getFeatures().getUnresolvedReferences().contains("modifyparams")); + assertThrows(UnsupportedOperationException.class, + () -> new TablesNamesFinder().getTables(block)); + } + + @Test + void supportsIssue1786AndMaintainerNestedExample() throws Exception { + OracleBlock simple = parse( + "DECLARE num NUMBER; BEGIN num := 10; dbms_output.put_line('The number is ' || num); END;"); + assertEquals("num", simple.getDeclarations().get(0).getName()); + assertEquals("NUMBER", simple.getDeclarations().get(0).getDataType().toString()); + OracleAssignment assignment = + assertInstanceOf(OracleAssignment.class, simple.getStatements().get(0)); + assertEquals("num", assignment.getTarget().toString()); + assertEquals("10", assignment.getValue().toString()); + OracleBlock nested = parse( + "DECLARE l_message VARCHAR2(100) := 'Hello'; BEGIN DECLARE l_message2 VARCHAR2(100) := l_message || ' World!'; BEGIN DBMS_OUTPUT.put_line(l_message2); END; EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.put_line(DBMS_UTILITY.format_error_stack); END;"); + assertInstanceOf(OracleBlock.class, nested.getStatements().get(0)); + assertEquals(List.of("OTHERS"), nested.getExceptionHandlers().get(0).getExceptions()); + assertEquals("DBMS_OUTPUT.put_line", + ((Execute) nested.getExceptionHandlers().get(0).getStatements().get(0)).getName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "BEGIN p; p(); schema.pkg.p(1, 'text'); p(arg => 1); NULL; END;", + "DECLARE n CONSTANT NUMBER := 1; s VARCHAR2(10) NOT NULL DEFAULT 'a'; BEGIN n2 := n; END;", + "BEGIN :out_value := 1; rec.field := 2; END;", + "BEGIN NULL; EXCEPTION WHEN NO_DATA_FOUND OR TOO_MANY_ROWS THEN NULL; WHEN OTHERS THEN p; END;", + "BEGIN BEGIN NULL; END; BEGIN NULL; END; END;" + }) + void supportsBlockForms(String sql) throws Exception { + parse(sql); + } + + @Test + void visitsMutationsAcrossDeclarationsBodyAndHandlers() throws Exception { + OracleBlock block = parse( + "DECLARE n NUMBER := 1; BEGIN n := 2; EXCEPTION WHEN OTHERS THEN n := 3; END;"); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S context) { + assertEquals("ctx", context); + values.add(value.getValue()); + return null; + } + }; + block.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), "ctx"); + assertEquals(List.of(1L, 2L, 3L), values); + assertFalse(block.getFeatures().modifiesData()); + block.getDeclarations().get(0).setInitializer(new LongValue(10)); + ((OracleAssignment) block.getExceptionHandlers().get(0).getStatements().get(0)) + .setValue(new LongValue(30)); + StringBuilder output = new StringBuilder(); + ExpressionDeParser deparser = new ExpressionDeParser() { + @Override + public StringBuilder visit(LongValue value, S context) { + assertEquals("ctx", context); + return getBuilder().append(value.getValue() + 1); + } + }; + block.accept(new StatementDeParser(deparser, new SelectDeParser(), output), "ctx"); + assertTrue(output.toString().contains("NUMBER := 11")); + assertTrue(output.toString().contains("n := 31")); + parse(output.toString()); + } + + @Test + void findsSqlTablesInBothMainBodyAndHandlers() throws Exception { + OracleBlock block = parse( + "DECLARE n NUMBER := (SELECT max(id) FROM source); BEGIN INSERT INTO target SELECT id FROM source; EXCEPTION WHEN OTHERS THEN INSERT INTO audit VALUES (1); END;"); + assertEquals(Set.of("source", "target", "audit"), new TablesNamesFinder().getTables(block)); + assertTrue(block.getFeatures().modifiesData()); + assertFalse(block.getFeatures().returnsResultSet()); + } + + @Test + void retainsScriptBoundariesAndExistingDialects() throws Exception { + assertEquals(3, CCJSqlParserUtil.parseStatements("BEGIN p; END; BEGIN q; END; SELECT 1;", + p -> p.withDialect(Dialect.ORACLE)).size()); + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse("BEGIN p(1); END;")); + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("DECLARE n NUMBER; BEGIN n := 1; END;", + p -> p.withDialect(Dialect.SQLSERVER))); + assertInstanceOf(DeclareStatement.class, CCJSqlParserUtil.parse("DECLARE @n int = 1", + p -> p.withDialect(Dialect.SQLSERVER))); + assertInstanceOf(Block.class, CCJSqlParserUtil.parse("BEGIN SELECT 1; END;")); + } + + @ParameterizedTest + @ValueSource(strings = {"BEGIN RAISE; END;", "BEGIN LOOP; END;", "DECLARE BEGIN NULL; END;", + "DECLARE n NUMBER BEGIN NULL; END;", + "BEGIN p(1) END;", "BEGIN NULL;", "BEGIN END;", "BEGIN p(1,); END;", + "DECLARE n CONSTANT NUMBER; BEGIN NULL; END;", + "DECLARE n NUMBER NOT NULL; BEGIN NULL; END;", + "BEGIN NULL; EXCEPTION WHEN OTHERS THEN END;"}) + void rejectsIncompleteBlocks(String sql) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + + @Test + void validatesOracleBlockCapability() { + String sql = "DECLARE n NUMBER := 1; BEGIN n := 2; p(n); END;"; + assertTrue(new Validation(CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.ORACLE) + .getConfiguration(), List.of(OracleVersion.values()[0]), sql).validate().isEmpty()); + assertFalse( + new Validation( + CCJSqlParserUtil.newParser("SELECT 1").withDialect(Dialect.ORACLE) + .getConfiguration(), + List.of(new FeaturesAllowed(Feature.block)), sql).validate().isEmpty()); + } +}