From 72bf5d07b421bef9a38f83ce179dc92e634f9395 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Fri, 7 Jun 2024 11:20:50 +0200 Subject: [PATCH] [plsql] Support MERGE statement Fixes #1934 --- docs/pages/release_notes.md | 2 + pmd-plsql/etc/grammar/PLSQL.jjt | 107 ++++++++++++++--- .../pmd/lang/plsql/ast/PlsqlTreeDumpTest.java | 5 + .../plsql/ast/MergeStatementIssue1934.pls | 17 +++ .../plsql/ast/MergeStatementIssue1934.txt | 108 ++++++++++++++++++ 5 files changed, 220 insertions(+), 19 deletions(-) create mode 100644 pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls create mode 100644 pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index e8d7cdbf90..48fbdf1007 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -15,6 +15,8 @@ This is a {{ site.pmd.release_type }} release. ### 🚀 New and noteworthy ### 🐛 Fixed Issues +* plsql + * [#1934](https://github.com/pmd/pmd/issues/1934): \[plsql] ParseException with MERGE statement in anonymous block ### 🚨 API Changes diff --git a/pmd-plsql/etc/grammar/PLSQL.jjt b/pmd-plsql/etc/grammar/PLSQL.jjt index 5108997fac..f7a49d95c9 100644 --- a/pmd-plsql/etc/grammar/PLSQL.jjt +++ b/pmd-plsql/etc/grammar/PLSQL.jjt @@ -27,6 +27,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** + * Add support for MERGE (INTO) statement + * + * Andreas Dangel 06/2024 + *==================================================================== * Add support for Select statement within OPEN FOR Statements * * Andreas Dangel 09/2021 @@ -293,7 +297,8 @@ ASTInput Input() : | DeleteStatement() [";"] | InsertStatement() [";"] | SelectStatement() [";"] - |(|||||) ReadPastNextOccurrence(";") //Ignore SQL statements in scripts + | MergeStatement() [";"] + |(|||
|) ReadPastNextOccurrence(";") //Ignore SQL statements in scripts ) ("/")* )* @@ -1078,19 +1083,32 @@ void Skip2NextTerminator(String initiator,String terminator) : if(t.getImage().equals(initiator)) count++; while (count > 0 || !t.getImage().equals(terminator)) { - t = getNextToken(); - t = getToken(1); - if(t.getImage().equals(initiator)) count++; - if(t.getImage().equals(terminator)) count--; - if((null != t.specialToken && beginToken.kind != SELECT && beginToken.kind != INSERT && beginToken.kind != UPDATE && beginToken.kind != DELETE - && beginToken.kind != MERGE && beginToken.kind != EXECUTE && beginToken.kind != WITH) || t.kind == EOF) - return; - if (t.specialToken != null && "/".equals(t.getImage())) - return; + t = getNextToken(); + t = getToken(1); + if (t.getImage().equals(initiator)) { + count++; + } + if (t.getImage().equals(terminator)) { + count--; + } + if ((null != t.specialToken + && beginToken.kind != SELECT + && beginToken.kind != INSERT + && beginToken.kind != UPDATE + && beginToken.kind != DELETE + && beginToken.kind != MERGE + && beginToken.kind != EXECUTE + && beginToken.kind != WITH) + || t.kind == EOF) { + return; + } + if (t.specialToken != null && "/".equals(t.getImage())) { + return; + } } } { - { return; } + { return; } } /* @@ -1256,10 +1274,10 @@ ASTSqlStatement SqlStatement(String initiator, String terminator) : |
{jjtThis.setType(ASTSqlStatement.Type.LOCK_TABLE); } |{jjtThis.setType(ASTSqlStatement.Type.MERGE); } |) - Skip2NextTerminator(initiator, terminator) - { - return jjtThis ; - } + Skip2NextTerminator(initiator, terminator) + { + return jjtThis; + } } void AbstractSelectStatement(AbstractSelectStatement node) #void : @@ -2346,6 +2364,7 @@ ASTUnlabelledStatement UnlabelledStatement() : UpdateStatement() ";" | DeleteStatement() ";" | InsertStatement() ";" | + LOOKAHEAD(2) MergeStatement() ";" | LOOKAHEAD(["("]
||) SqlStatement(null,";") [";"] | LOOKAHEAD(3) ContinueStatement() ";" // CONTINUE keyword was added in 11G, so Oracle compilation supports CONTINUE as a variable name | CaseStatement() ";" @@ -2654,6 +2673,55 @@ ASTDeleteStatement DeleteStatement() : { return jjtThis; } } +/** + * https://docs.oracle.com/en/database/oracle/oracle-database/18/sqlrf/MERGE.html#GUID-5692CCB7-24D9-4C0E-81A7-A22436DC968F + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/MERGE.html#GUID-5692CCB7-24D9-4C0E-81A7-A22436DC968F + */ +ASTMergeStatement MergeStatement() : +{} +{ + [ LOOKAHEAD(2) SchemaName() "." ] TableName() [ TableAlias() ] + + ( + LOOKAHEAD(3) "(" ValuesClause() ")" + | "(" Subquery() ")" [ TableAlias() ] + | [ LOOKAHEAD(2) SchemaName() "." ] TableName() [ TableAlias() ] + ) + "(" Condition() ")" + [ LOOKAHEAD(MergeUpdateClausePrefix()) MergeUpdateClause() ] + [ LOOKAHEAD(2) MergeInsertClause() ] + [ ErrorLoggingClause() ] + [ ReturningClause() ] + { return jjtThis; } +} + +void MergeUpdateClausePrefix() #void: +{} +{ + KEYWORD("MATCHED") +} + +ASTMergeUpdateClause MergeUpdateClause() : +{} +{ + MergeUpdateClausePrefix() + [ LOOKAHEAD(2) TableName() "." ] Column() "=" ( LOOKAHEAD(2) "(" Subquery() ")" | Expression() | <_DEFAULT> ) + ( "," [ LOOKAHEAD(2) TableName() "." ] Column() "=" ( LOOKAHEAD(2) "(" Subquery() ")" | Expression() | <_DEFAULT> ) )* + [ WhereClause() ] + [ WhereClause() ] + { return jjtThis; } +} + +ASTMergeInsertClause MergeInsertClause() : +{} +{ + KEYWORD("MATCHED") + [ "(" Column() ( "," Column() )* ")" ] + ValuesClause() + [ WhereClause() ] + { return jjtThis; } +} + /** Scope rule: the loop index only exists within the Loop */ ASTForStatement ForStatement() : {} @@ -5426,14 +5494,15 @@ void RESERVED_WORD() #void: {} void KEYWORD(String id) #void: {} { - { - if (!token.getImage().equalsIgnoreCase(id)) { + if (!isKeyword(id)) { String eol = System.getProperty("line.separator", "\n"); - throw new ParseException("Encountered \"" + token.getImage() + "\" " - + "Was expecting: " + id).withLocation(token); + throw new ParseException("Encountered \"" + getToken(1).getImage() + "\" " + + "Was expecting: \"" + id + "\"").withLocation(token); } } + + } ASTKEYWORD_UNRESERVED KEYWORD_UNRESERVED (): {} 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 438184c677..5a49292182 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 @@ -41,4 +41,9 @@ class PlsqlTreeDumpTest extends BaseTreeDumpTest { void parseSelectIntoAssociativeArrayType() { doTest("SelectIntoArray"); } + + @Test + void parseMergeStatement() { + doTest("MergeStatementIssue1934"); + } } diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls new file mode 100644 index 0000000000..555f439f0b --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.pls @@ -0,0 +1,17 @@ +-- +-- BSD-style license; for more info see http://pmd.sourceforge.net/license.html +-- + +-- See https://github.com/pmd/pmd/issues/1934 + +BEGIN + + MERGE INTO jhs_translations b + USING ( SELECT 'PROM_EDIT_PROM_NR' key1,'Edycja promocji nr' text,123123 lce_id FROM dual ) e + ON (b.key1 = e.key1 and b.lce_id=e.lce_id) + WHEN MATCHED + THEN UPDATE SET b.text = e.text + WHEN NOT MATCHED + THEN INSERT (ID,KEY1, TEXT,LCE_ID) values (JHS_SEQ.NEXTVAL,'PROM_EDIT_PROM_NR','Edycja promocji nr',123123); +END; +/ diff --git a/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt new file mode 100644 index 0000000000..c473d6143c --- /dev/null +++ b/pmd-plsql/src/test/resources/net/sourceforge/pmd/lang/plsql/ast/MergeStatementIssue1934.txt @@ -0,0 +1,108 @@ ++- Input[@CanonicalImage = null, @ExcludedLinesCount = 0, @ExcludedRangesCount = 0] + +- Global[@CanonicalImage = null] + +- Block[@CanonicalImage = null] + +- Statement[@CanonicalImage = null] + +- UnlabelledStatement[@CanonicalImage = null] + +- MergeStatement[@CanonicalImage = null] + +- TableName[@CanonicalImage = "JHS_TRANSLATIONS", @Image = "jhs_translations"] + | +- ID[@CanonicalImage = "JHS_TRANSLATIONS", @Image = "jhs_translations"] + +- TableAlias[@CanonicalImage = "B", @Image = "b"] + | +- ID[@CanonicalImage = "B", @Image = "b"] + +- QueryBlock[@All = false, @CanonicalImage = null, @Distinct = false, @Unique = false] + | +- SelectList[@CanonicalImage = null] + | | +- SqlExpression[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | | | +- PrimaryPrefix[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | | | +- StringLiteral[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @String = "PROM_EDIT_PROM_NR"] + | | +- ColumnAlias[@CanonicalImage = "KEY1", @Image = "key1"] + | | | +- ID[@CanonicalImage = "KEY1", @Image = "key1"] + | | +- SqlExpression[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | | | +- PrimaryPrefix[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | | | +- StringLiteral[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @String = "Edycja promocji nr"] + | | +- ColumnAlias[@CanonicalImage = "TEXT", @Image = "text"] + | | | +- ID[@CanonicalImage = "TEXT", @Image = "text"] + | | +- SqlExpression[@CanonicalImage = "123123", @Image = "123123"] + | | | +- PrimaryPrefix[@CanonicalImage = "123123", @Image = "123123", @SelfModifier = false] + | | | +- Literal[@CanonicalImage = "123123", @Image = "123123"] + | | | +- NumericLiteral[@CanonicalImage = "123123", @Image = "123123"] + | | +- ColumnAlias[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | | +- ID[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | +- FromClause[@CanonicalImage = null] + | +- TableReference[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "DUAL", @Image = "dual"] + | +- ID[@CanonicalImage = "DUAL", @Image = "dual"] + +- TableAlias[@CanonicalImage = "E", @Image = "e"] + | +- ID[@CanonicalImage = "E", @Image = "e"] + +- Condition[@CanonicalImage = null] + | +- CompoundCondition[@CanonicalImage = null, @Type = "AND"] + | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | | +- SqlExpression[@CanonicalImage = "B.KEY1", @Image = "b.key1"] + | | | +- PrimaryPrefix[@CanonicalImage = "B.KEY1", @Image = "b.key1", @SelfModifier = false] + | | | +- SimpleExpression[@CanonicalImage = "B.KEY1", @Image = "b.key1"] + | | | +- TableName[@CanonicalImage = "B", @Image = "b"] + | | | | +- ID[@CanonicalImage = "B", @Image = "b"] + | | | +- Column[@CanonicalImage = "KEY1", @Image = "key1"] + | | | +- ID[@CanonicalImage = "KEY1", @Image = "key1"] + | | +- SqlExpression[@CanonicalImage = "E.KEY1", @Image = "e.key1"] + | | +- PrimaryPrefix[@CanonicalImage = "E.KEY1", @Image = "e.key1", @SelfModifier = false] + | | +- SimpleExpression[@CanonicalImage = "E.KEY1", @Image = "e.key1"] + | | +- TableName[@CanonicalImage = "E", @Image = "e"] + | | | +- ID[@CanonicalImage = "E", @Image = "e"] + | | +- Column[@CanonicalImage = "KEY1", @Image = "key1"] + | | +- ID[@CanonicalImage = "KEY1", @Image = "key1"] + | +- Condition[@CanonicalImage = null] + | +- CompoundCondition[@CanonicalImage = null, @Type = null] + | +- ComparisonCondition[@CanonicalImage = null, @Operator = "="] + | +- SqlExpression[@CanonicalImage = "B.LCE_ID", @Image = "b.lce_id"] + | | +- PrimaryPrefix[@CanonicalImage = "B.LCE_ID", @Image = "b.lce_id", @SelfModifier = false] + | | +- SimpleExpression[@CanonicalImage = "B.LCE_ID", @Image = "b.lce_id"] + | | +- TableName[@CanonicalImage = "B", @Image = "b"] + | | | +- ID[@CanonicalImage = "B", @Image = "b"] + | | +- Column[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | | +- ID[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | +- SqlExpression[@CanonicalImage = "E.LCE_ID", @Image = "e.lce_id"] + | +- PrimaryPrefix[@CanonicalImage = "E.LCE_ID", @Image = "e.lce_id", @SelfModifier = false] + | +- SimpleExpression[@CanonicalImage = "E.LCE_ID", @Image = "e.lce_id"] + | +- TableName[@CanonicalImage = "E", @Image = "e"] + | | +- ID[@CanonicalImage = "E", @Image = "e"] + | +- Column[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + | +- ID[@CanonicalImage = "LCE_ID", @Image = "lce_id"] + +- MergeUpdateClause[@CanonicalImage = null] + | +- TableName[@CanonicalImage = "B", @Image = "b"] + | | +- ID[@CanonicalImage = "B", @Image = "b"] + | +- Column[@CanonicalImage = "TEXT", @Image = "text"] + | | +- ID[@CanonicalImage = "TEXT", @Image = "text"] + | +- Expression[@CanonicalImage = "E.TEXT", @Image = "e.text"] + | +- PrimaryPrefix[@CanonicalImage = "E.TEXT", @Image = "e.text", @SelfModifier = false] + | +- SimpleExpression[@CanonicalImage = "E.TEXT", @Image = "e.text"] + | +- TableName[@CanonicalImage = "E", @Image = "e"] + | | +- ID[@CanonicalImage = "E", @Image = "e"] + | +- Column[@CanonicalImage = "TEXT", @Image = "text"] + | +- ID[@CanonicalImage = "TEXT", @Image = "text"] + +- MergeInsertClause[@CanonicalImage = null] + +- Column[@CanonicalImage = "ID", @Image = "ID"] + | +- ID[@CanonicalImage = "ID", @Image = "ID"] + +- Column[@CanonicalImage = "KEY1", @Image = "KEY1"] + | +- ID[@CanonicalImage = "KEY1", @Image = "KEY1"] + +- Column[@CanonicalImage = "TEXT", @Image = "TEXT"] + | +- ID[@CanonicalImage = "TEXT", @Image = "TEXT"] + +- Column[@CanonicalImage = "LCE_ID", @Image = "LCE_ID"] + | +- ID[@CanonicalImage = "LCE_ID", @Image = "LCE_ID"] + +- ValuesClause[@CanonicalImage = null] + +- Expression[@CanonicalImage = "", @Image = ""] + | +- PrimaryPrefix[@CanonicalImage = "", @Image = "", @SelfModifier = false] + | +- SimpleExpression[@CanonicalImage = "", @Image = ""] + | +- ID[@CanonicalImage = "JHS_SEQ", @Image = "JHS_SEQ"] + +- Expression[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | +- PrimaryPrefix[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @SelfModifier = false] + | +- Literal[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'"] + | +- StringLiteral[@CanonicalImage = "\'PROM_EDIT_PROM_NR\'", @Image = "\'PROM_EDIT_PROM_NR\'", @String = "PROM_EDIT_PROM_NR"] + +- Expression[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | +- PrimaryPrefix[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @SelfModifier = false] + | +- Literal[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'"] + | +- StringLiteral[@CanonicalImage = "\'EDYCJA PROMOCJI NR\'", @Image = "\'Edycja promocji nr\'", @String = "Edycja promocji nr"] + +- Expression[@CanonicalImage = "123123", @Image = "123123"] + +- PrimaryPrefix[@CanonicalImage = "123123", @Image = "123123", @SelfModifier = false] + +- Literal[@CanonicalImage = "123123", @Image = "123123"] + +- NumericLiteral[@CanonicalImage = "123123", @Image = "123123"]