diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 78fe30f593..71b7e5b641 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -19,6 +19,7 @@ This is a {{ site.pmd.release_type }} release. * [#5053](https://github.com/pmd/pmd/issues/5053): \[apex] CPD fails to parse string literals with escaped characters * plsql * [#1934](https://github.com/pmd/pmd/issues/1934): \[plsql] ParseException with MERGE statement in anonymous block + * [#2779](https://github.com/pmd/pmd/issues/2779): \[plsql] Error while parsing statement with (Oracle) DML Error Logging ### 🚨 API Changes diff --git a/pmd-plsql/etc/grammar/PLSQL.jjt b/pmd-plsql/etc/grammar/PLSQL.jjt index f7a49d95c9..0bfb7feaac 100644 --- a/pmd-plsql/etc/grammar/PLSQL.jjt +++ b/pmd-plsql/etc/grammar/PLSQL.jjt @@ -28,6 +28,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** * Add support for MERGE (INTO) statement + * Add support Error Logging for INSERT, UPDATE, DELETE * * Andreas Dangel 06/2024 *==================================================================== @@ -2105,7 +2106,7 @@ ASTTableReference TableReference() : { QueryTableExpression() - [ LOOKAHEAD(2) TableAlias() ] + [ LOOKAHEAD(2, {!getToken(1).getImage().equalsIgnoreCase("LOG")}) TableAlias() ] { return jjtThis; } } @@ -2476,7 +2477,7 @@ ASTCursorForLoopStatement CursorForLoopStatement() : } /** - * See https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423 + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423 */ ASTInsertStatement InsertStatement() : {} @@ -2489,6 +2490,7 @@ ASTSingleTableInsert SingleTableInsert() : {} { InsertIntoClause() ( ValuesClause() [ ReturningClause() ] | Subquery() ) + [ ErrorLoggingClause() ] { return jjtThis; } } @@ -2641,7 +2643,8 @@ ASTUpdateSetClause UpdateSetClause() : } /** - * https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__I2126358 + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__I2126358 + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/DELETE.html#GUID-156845A5-B626-412B-9F95-8869B988ABD7__I2122564 */ ASTReturningClause ReturningClause() : {} @@ -2651,7 +2654,9 @@ ASTReturningClause ReturningClause() : } /** - * https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__BCEEAAGC + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423__BGBDIGAH + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__BCEEAAGC + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/DELETE.html#GUID-156845A5-B626-412B-9F95-8869B988ABD7__CEGCHDJF */ ASTErrorLoggingClause ErrorLoggingClause() : {} @@ -2664,12 +2669,17 @@ ASTErrorLoggingClause ErrorLoggingClause() : } +/** + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/DELETE.html#GUID-156845A5-B626-412B-9F95-8869B988ABD7 + */ ASTDeleteStatement DeleteStatement() : {} { [ ] ( TableReference() | "(" TableReference() ")" ) [ WhereClause() ] + [ ReturningClause() ] + [ ErrorLoggingClause() ] { return jjtThis; } } diff --git a/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java b/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java index 5a49292182..0cc7dff036 100644 --- a/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java +++ b/pmd-plsql/src/test/java/net/sourceforge/pmd/lang/plsql/ast/PlsqlTreeDumpTest.java @@ -46,4 +46,9 @@ class PlsqlTreeDumpTest extends BaseTreeDumpTest { void parseMergeStatement() { doTest("MergeStatementIssue1934"); } + + @Test + void errorLoggingClause() { + doTest("ErrorLoggingClause2779"); + } } diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls new file mode 100644 index 0000000000..580a396391 --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.pls @@ -0,0 +1,29 @@ +-- +-- BSD-style license; for more info see http://pmd.sourceforge.net/license.html +-- + +create or replace procedure test as +begin + + -- https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html#GUID-903F8043-0254-4EE9-ACC1-CB8AC0AF3423__BCEGDJDJ + INSERT INTO raises + SELECT employee_id, salary*1.1 FROM employees + WHERE commission_pct > .2 + LOG ERRORS INTO errlog ('my_bad') REJECT LIMIT 10; + + -- https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html#GUID-027A462D-379D-4E35-8611-410F3AC8FDA5__I2135485 + UPDATE people_demo1 p SET VALUE(p) = + (SELECT VALUE(q) FROM people_demo2 q + WHERE p.department_id = q.department_id) + WHERE p.department_id = 10 + LOG ERRORS INTO errlog ('my_bad') REJECT LIMIT 10; + + -- https://github.com/pmd/pmd/issues/2779 + delete from test_table talias + log errors into err$_test_table reject limit unlimited; + + -- without a table alias + delete from test_table + log errors into err$_test_table reject limit unlimited; +end; +/ diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt new file mode 100644 index 0000000000..ce2cf4c5e2 --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/ErrorLoggingClause2779.txt @@ -0,0 +1,149 @@ ++- Input[@CanonicalImage = null, @ExcludedLinesCount = 0, @ExcludedRangesCount = 0] + +- Global[@CanonicalImage = null] + +- ProgramUnit[@CanonicalImage = null, @MethodName = "test", @Name = "test", @ObjectName = null] + +- MethodDeclarator[@CanonicalImage = "TEST", @Image = "test", @ParameterCount = 1] + | +- ObjectNameDeclaration[@CanonicalImage = "TEST", @Image = "test"] + | +- ID[@CanonicalImage = "TEST", @Image = "test"] + +- DeclarativeSection[@CanonicalImage = null] + +- Statement[@CanonicalImage = null] + | +- UnlabelledStatement[@CanonicalImage = null] + | +- InsertStatement[@CanonicalImage = null] + | +- SingleTableInsert[@CanonicalImage = null] + | +- InsertIntoClause[@CanonicalImage = null] + | | +- DMLTableExpressionClause[@CanonicalImage = null] + | | +- TableName[@CanonicalImage = "RAISES", @Image = "raises"] + | | +- ID[@CanonicalImage = "RAISES", @Image = "raises"] + | +- QueryBlock[@All = false, @CanonicalImage = null, @Distinct = false, @Unique = false] + | | +- SelectList[@CanonicalImage = null] + | | | +- SqlExpression[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | | +- PrimaryPrefix[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id", @SelfModifier = false] + | | | | +- SimpleExpression[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | | +- Column[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | | +- ID[@CanonicalImage = "EMPLOYEE_ID", @Image = "employee_id"] + | | | +- SqlExpression[@CanonicalImage = "SALARY * 1.1", @Image = "salary * 1.1"] + | | | +- MultiplicativeExpression[@CanonicalImage = "SALARY * 1.1", @Image = "salary * 1.1"] + | | | +- PrimaryPrefix[@CanonicalImage = "SALARY", @Image = "salary", @SelfModifier = false] + | | | | +- SimpleExpression[@CanonicalImage = "SALARY", @Image = "salary"] + | | | | +- Column[@CanonicalImage = "SALARY", @Image = "salary"] + | | | | +- ID[@CanonicalImage = "SALARY", @Image = "salary"] + | | | +- PrimaryPrefix[@CanonicalImage = "1.1", @Image = "1.1", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "1.1", @Image = "1.1"] + | | | +- NumericLiteral[@CanonicalImage = "1.1", @Image = "1.1"] + | | +- FromClause[@CanonicalImage = null] + | | | +- TableReference[@CanonicalImage = null] + | | | +- TableName[@CanonicalImage = "EMPLOYEES", @Image = "employees"] + | | | +- ID[@CanonicalImage = "EMPLOYEES", @Image = "employees"] + | | +- WhereClause[@CanonicalImage = null] + | | +- Condition[@CanonicalImage = null] + | | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | | +- ComparisonCondition[@CanonicalImage = null, @Operator = ">"] + | | +- SqlExpression[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | | +- PrimaryPrefix[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | | +- Column[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | | +- ID[@CanonicalImage = "COMMISSION_PCT", @Image = "commission_pct"] + | | +- SqlExpression[@CanonicalImage = ".2", @Image = ".2"] + | | +- PrimaryPrefix[@CanonicalImage = ".2", @Image = ".2", @SelfModifier = false] + | | +- Literal[@CanonicalImage = ".2", @Image = ".2"] + | | +- NumericLiteral[@CanonicalImage = ".2", @Image = ".2"] + | +- ErrorLoggingClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | | +- ID[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | +- SqlExpression[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- PrimaryPrefix[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @SelfModifier = false] + | | +- Literal[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- StringLiteral[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @String = "my_bad"] + | +- NumericLiteral[@CanonicalImage = "10", @Image = "10"] + +- Statement[@CanonicalImage = null] + | +- UnlabelledStatement[@CanonicalImage = null] + | +- UpdateStatement[@CanonicalImage = null] + | +- DMLTableExpressionClause[@CanonicalImage = null] + | | +- TableName[@CanonicalImage = "PEOPLE_DEMO1", @Image = "people_demo1"] + | | +- ID[@CanonicalImage = "PEOPLE_DEMO1", @Image = "people_demo1"] + | +- TableAlias[@CanonicalImage = "P", @Image = "p"] + | | +- ID[@CanonicalImage = "P", @Image = "p"] + | +- UpdateSetClause[@CanonicalImage = null] + | | +- TableAlias[@CanonicalImage = "P", @Image = "p"] + | | | +- ID[@CanonicalImage = "P", @Image = "p"] + | | +- QueryBlock[@All = false, @CanonicalImage = null, @Distinct = false, @Unique = false] + | | +- SelectList[@CanonicalImage = null] + | | | +- SqlExpression[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | +- PrimaryPrefix[@CanonicalImage = "VALUE", @Image = "VALUE", @SelfModifier = false] + | | | +- FunctionCall[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | +- FunctionName[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | | +- ID[@CanonicalImage = "VALUE", @Image = "VALUE"] + | | | +- Arguments[@ArgumentCount = 1, @CanonicalImage = null] + | | | +- ArgumentList[@CanonicalImage = null] + | | | +- Argument[@CanonicalImage = null] + | | | +- Expression[@CanonicalImage = "Q", @Image = "q"] + | | | +- PrimaryPrefix[@CanonicalImage = "Q", @Image = "q", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "Q", @Image = "q"] + | | | +- Column[@CanonicalImage = "Q", @Image = "q"] + | | | +- ID[@CanonicalImage = "Q", @Image = "q"] + | | +- FromClause[@CanonicalImage = null] + | | | +- TableReference[@CanonicalImage = null] + | | | +- TableName[@CanonicalImage = "PEOPLE_DEMO2", @Image = "people_demo2"] + | | | | +- ID[@CanonicalImage = "PEOPLE_DEMO2", @Image = "people_demo2"] + | | | +- TableAlias[@CanonicalImage = "Q", @Image = "q"] + | | | +- ID[@CanonicalImage = "Q", @Image = "q"] + | | +- WhereClause[@CanonicalImage = null] + | | +- Condition[@CanonicalImage = null] + | | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | | +- SqlExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- PrimaryPrefix[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- TableName[@CanonicalImage = "P", @Image = "p"] + | | | | +- ID[@CanonicalImage = "P", @Image = "p"] + | | | +- Column[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | | +- ID[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | +- SqlExpression[@CanonicalImage = "Q.DEPARTMENT_ID", @Image = "q.department_id"] + | | +- PrimaryPrefix[@CanonicalImage = "Q.DEPARTMENT_ID", @Image = "q.department_id", @SelfModifier = false] + | | +- SimpleExpression[@CanonicalImage = "Q.DEPARTMENT_ID", @Image = "q.department_id"] + | | +- TableName[@CanonicalImage = "Q", @Image = "q"] + | | | +- ID[@CanonicalImage = "Q", @Image = "q"] + | | +- Column[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | +- ID[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | +- WhereClause[@CanonicalImage = null] + | | +- Condition[@CanonicalImage = null] + | | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | | +- SqlExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- PrimaryPrefix[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "P.DEPARTMENT_ID", @Image = "p.department_id"] + | | | +- TableName[@CanonicalImage = "P", @Image = "p"] + | | | | +- ID[@CanonicalImage = "P", @Image = "p"] + | | | +- Column[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | | +- ID[@CanonicalImage = "DEPARTMENT_ID", @Image = "department_id"] + | | +- SqlExpression[@CanonicalImage = "10", @Image = "10"] + | | +- PrimaryPrefix[@CanonicalImage = "10", @Image = "10", @SelfModifier = false] + | | +- Literal[@CanonicalImage = "10", @Image = "10"] + | | +- NumericLiteral[@CanonicalImage = "10", @Image = "10"] + | +- ErrorLoggingClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | | +- ID[@CanonicalImage = "ERRLOG", @Image = "errlog"] + | +- SqlExpression[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- PrimaryPrefix[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @SelfModifier = false] + | | +- Literal[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'"] + | | +- StringLiteral[@CanonicalImage = "\'MY_BAD\'", @Image = "\'my_bad\'", @String = "my_bad"] + | +- NumericLiteral[@CanonicalImage = "10", @Image = "10"] + +- Statement[@CanonicalImage = null] + | +- UnlabelledStatement[@CanonicalImage = null] + | +- DeleteStatement[@CanonicalImage = null] + | +- TableReference[@CanonicalImage = null] + | | +- TableName[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + | | | +- ID[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + | | +- TableAlias[@CanonicalImage = "TALIAS", @Image = "talias"] + | | +- ID[@CanonicalImage = "TALIAS", @Image = "talias"] + | +- ErrorLoggingClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] + | +- ID[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] + +- Statement[@CanonicalImage = null] + +- UnlabelledStatement[@CanonicalImage = null] + +- DeleteStatement[@CanonicalImage = null] + +- TableReference[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + | +- ID[@CanonicalImage = "TEST_TABLE", @Image = "test_table"] + +- ErrorLoggingClause[@CanonicalImage = null] + +- TableName[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"] + +- ID[@CanonicalImage = "ERR$_TEST_TABLE", @Image = "err$_test_table"]