From be972b78195789298cfe443c0ce35b699b2e6c2a Mon Sep 17 00:00:00 2001 From: Felipe Zorzo Date: Mon, 10 Jun 2024 21:03:02 -0300 Subject: [PATCH] feat(grammar): Support JSON_ARRAY (#182) --- .../plugins/plsqlopen/api/PlSqlKeyword.kt | 3 + .../api/SingleRowSqlFunctionsGrammar.kt | 52 +++++++++++ .../api/expressions/JsonArrayTest.kt | 90 +++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 zpa-core/src/test/kotlin/org/sonar/plugins/plsqlopen/api/expressions/JsonArrayTest.kt diff --git a/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/PlSqlKeyword.kt b/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/PlSqlKeyword.kt index fe370562..da3c26d5 100644 --- a/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/PlSqlKeyword.kt +++ b/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/PlSqlKeyword.kt @@ -103,6 +103,7 @@ enum class PlSqlKeyword(override val value: String, val isReserved: Boolean = fa WHERE("where", true), WITH("with", true), A("a"), + ABSENT("absent"), ACCESSIBLE("accessible"), ADD("add"), ADMIN("admin"), @@ -275,6 +276,7 @@ enum class PlSqlKeyword(override val value: String, val isReserved: Boolean = fa JAVA("java"), JOIN("join"), JSON("json"), + JSON_ARRAY("json_array"), JSON_QUERY("json_query"), KEEP("keep"), KEY("key"), @@ -487,6 +489,7 @@ enum class PlSqlKeyword(override val value: String, val isReserved: Boolean = fa TRUE("true"), TRUNCATE("truncate"), TYPE("type"), + TYPENAME("typename"), UDF("udf"), UNBOUNDED("unbounded"), UNCONDITIONAL("unconditional"), diff --git a/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/SingleRowSqlFunctionsGrammar.kt b/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/SingleRowSqlFunctionsGrammar.kt index 98038121..8d317ff5 100644 --- a/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/SingleRowSqlFunctionsGrammar.kt +++ b/zpa-core/src/main/kotlin/org/sonar/plugins/plsqlopen/api/SingleRowSqlFunctionsGrammar.kt @@ -30,6 +30,11 @@ enum class SingleRowSqlFunctionsGrammar : GrammarRuleKey { // internals JSON_PASSING_CLAUSE, + JSON_ON_NULL_CLAUSE, + JSON_RETURNING_CLAUSE, + JSON_ARRAY_ENUMERATION_CONTENT, + JSON_ARRAY_QUERY_CONTENT, + JSON_ARRAY_ELEMENT, JSON_QUERY_RETURNING_CLAUSE, JSON_QUERY_WRAPPER_CLAUSE, JSON_QUERY_QUOTES_CLAUSE, @@ -46,6 +51,7 @@ enum class SingleRowSqlFunctionsGrammar : GrammarRuleKey { // functions EXTRACT_DATETIME_EXPRESSION, JSON_CONSTRUCTOR, + JSON_ARRAY_EXPRESSION, JSON_QUERY_EXPRESSION, XMLATTRIBUTES_EXPRESSION, XMLELEMENT_EXPRESSION, @@ -80,6 +86,7 @@ enum class SingleRowSqlFunctionsGrammar : GrammarRuleKey { b.firstOf( EXTRACT_DATETIME_EXPRESSION, JSON_CONSTRUCTOR, + JSON_ARRAY_EXPRESSION, JSON_QUERY_EXPRESSION, XMLATTRIBUTES_EXPRESSION, XMLELEMENT_EXPRESSION, @@ -260,6 +267,51 @@ enum class SingleRowSqlFunctionsGrammar : GrammarRuleKey { private fun createJsonFunctions(b: PlSqlGrammarBuilder) { b.rule(JSON_CONSTRUCTOR).define(JSON, LPARENTHESIS, EXPRESSION, RPARENTHESIS) + b.rule(JSON_ARRAY_EXPRESSION).define( + JSON_ARRAY, + LPARENTHESIS, + b.firstOf(JSON_ARRAY_ENUMERATION_CONTENT, JSON_ARRAY_QUERY_CONTENT), + RPARENTHESIS + ) + + b.rule(JSON_ARRAY_ENUMERATION_CONTENT).define( + JSON_ARRAY_ELEMENT, + b.zeroOrMore(COMMA, JSON_ARRAY_ELEMENT), + b.optional(JSON_ON_NULL_CLAUSE), + b.optional(JSON_RETURNING_CLAUSE), + b.optional(STRICT) + ) + + b.rule(JSON_ARRAY_ELEMENT).define( + EXPRESSION, + b.optional(FORMAT, JSON) + ) + + b.rule(JSON_ON_NULL_CLAUSE).define(b.firstOf(NULL, ABSENT), ON, NULL) + + b.rule(JSON_RETURNING_CLAUSE).define( + RETURNING, + b.firstOf( + b.sequence( + VARCHAR2, + b.optional(LPARENTHESIS, PlSqlTokenType.INTEGER_LITERAL, b.firstOf(CHAR, BYTE), RPARENTHESIS), + b.optional(WITH, TYPENAME) + ), + b.sequence( + b.firstOf(CLOB, BLOB), + b.optional(b.firstOf(REFERENCE, VALUE)) + ), + JSON + ) + ) + + b.rule(JSON_ARRAY_QUERY_CONTENT).define( + DmlGrammar.SELECT_EXPRESSION, + b.optional(JSON_ON_NULL_CLAUSE), + b.optional(JSON_RETURNING_CLAUSE), + b.optional(STRICT) + ) + b.rule(JSON_QUERY_EXPRESSION).define( JSON_QUERY, LPARENTHESIS, diff --git a/zpa-core/src/test/kotlin/org/sonar/plugins/plsqlopen/api/expressions/JsonArrayTest.kt b/zpa-core/src/test/kotlin/org/sonar/plugins/plsqlopen/api/expressions/JsonArrayTest.kt new file mode 100644 index 00000000..7222510f --- /dev/null +++ b/zpa-core/src/test/kotlin/org/sonar/plugins/plsqlopen/api/expressions/JsonArrayTest.kt @@ -0,0 +1,90 @@ +/** + * Z PL/SQL Analyzer + * Copyright (C) 2015-2024 Felipe Zorzo + * mailto:felipe AT felipezorzo DOT com DOT br + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.plsqlopen.api.expressions + +import com.felipebz.flr.tests.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.sonar.plugins.plsqlopen.api.PlSqlGrammar +import org.sonar.plugins.plsqlopen.api.RuleTest + +class JsonArrayTest : RuleTest() { + + @BeforeEach + fun init() { + setRootRule(PlSqlGrammar.EXPRESSION) + } + + @Test + fun matchesJsonArrayWithOneElement() { + assertThat(p).matches("json_array(1)") + } + + @Test + fun matchesJsonArrayWithManyElement() { + assertThat(p).matches("json_array(1, 'test', sysdate)") + } + + @Test + fun matchesJsonArrayWithFormat() { + assertThat(p).matches("json_array('{}' format json)") + } + + @Test + fun matchesJsonArrayWithNullOnNull() { + assertThat(p).matches("json_array(foo null on null)") + } + + @Test + fun matchesJsonArrayWithAbsentOnNull() { + assertThat(p).matches("json_array(foo absent on null)") + } + + @Test + fun matchesJsonArrayWithReturning() { + assertThat(p).matches("json_array(foo returning json)") + } + + @Test + fun matchesJsonArrayWithStrict() { + assertThat(p).matches("json_array(foo strict)") + } + + @Test + fun matchesLongJsonArray() { + assertThat(p).matches("json_array(json_array(1,2,3), 100, 'test', null null on null returning json strict)") + } + + @Test + fun matchesJsonArrayFromQuery() { + /* + * This syntax is listed on the JSON_ARRAY docs for Oracle 23ai: + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/JSON_ARRAY.html + * + * However, according to the table "Table C-2 Oracle Support for Optional Features of SQL/Foundation" + * it isn't supported yet: + * T811, Basic SQL/JSON constructor functions + * Oracle fully supports this feature, except for the JSON_ARRAY constructor by query. + * https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Oracle-Support-for-Optional-Features-of-SQLFoundation2011.html. + */ + assertThat(p).matches("json_array(select * from tab null on null returning json strict)") + } + +}