From f71186f170c2a20b6b97194ecd30aaafef35007e Mon Sep 17 00:00:00 2001 From: Moritz Becker Date: Wed, 14 Aug 2024 13:08:28 +0200 Subject: [PATCH] [#1928] Add parameter support for JSON_GET --- .../CriteriaBuilderConfigurationImpl.java | 25 ++-- .../impl/function/cast/CastFunction.java | 8 ++ .../jsonget/AbstractJsonGetFunction.java | 47 +------ .../function/jsonget/DB2JsonGetFunction.java | 20 +-- .../jsonget/MSSQLJsonGetFunction.java | 36 +++-- ...unction.java => MySQLJsonGetFunction.java} | 23 ++-- .../jsonget/OracleJsonGetFunction.java | 26 ++-- .../jsonget/PostgreSQLJsonGetFunction.java | 35 +++-- .../jsonset/AbstractJsonFunction.java | 126 ++++++++++++++++++ .../jsonset/AbstractJsonSetFunction.java | 7 +- .../jsonset/MSSQLJsonSetFunction.java | 8 +- .../jsonset/MySQL8JsonSetFunction.java | 15 ++- .../jsonset/OracleJsonSetFunction.java | 8 +- .../impl/util/JpqlFunctionUtil.java | 8 +- .../testsuite/entity/JsonDocument.java | 1 + .../testsuite/JsonGetAndSetTest.java | 59 ++++++-- .../core/manual/en_US/jpql_functions.adoc | 25 ++++ 17 files changed, 352 insertions(+), 125 deletions(-) rename core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/{MySQL8JsonGetFunction.java => MySQLJsonGetFunction.java} (53%) create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonFunction.java diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java index 02a9301716..05bd2f28ab 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java @@ -22,8 +22,8 @@ import com.blazebit.persistence.impl.dialect.DB2DbmsDialect; import com.blazebit.persistence.impl.dialect.DefaultDbmsDialect; import com.blazebit.persistence.impl.dialect.H2DbmsDialect; -import com.blazebit.persistence.impl.dialect.MariaDBDbmsDialect; import com.blazebit.persistence.impl.dialect.MSSQLDbmsDialect; +import com.blazebit.persistence.impl.dialect.MariaDBDbmsDialect; import com.blazebit.persistence.impl.dialect.MySQL8DbmsDialect; import com.blazebit.persistence.impl.dialect.MySQLDbmsDialect; import com.blazebit.persistence.impl.dialect.OracleDbmsDialect; @@ -359,7 +359,7 @@ import com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; import com.blazebit.persistence.impl.function.jsonget.DB2JsonGetFunction; import com.blazebit.persistence.impl.function.jsonget.MSSQLJsonGetFunction; -import com.blazebit.persistence.impl.function.jsonget.MySQL8JsonGetFunction; +import com.blazebit.persistence.impl.function.jsonget.MySQLJsonGetFunction; import com.blazebit.persistence.impl.function.jsonget.OracleJsonGetFunction; import com.blazebit.persistence.impl.function.jsonget.PostgreSQLJsonGetFunction; import com.blazebit.persistence.impl.function.jsonset.AbstractJsonSetFunction; @@ -543,8 +543,6 @@ import com.blazebit.persistence.spi.LateralStyle; import com.blazebit.persistence.spi.PackageOpener; import com.blazebit.persistence.spi.SetOperationType; - -import javax.persistence.EntityManagerFactory; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Time; @@ -561,6 +559,7 @@ import java.util.ServiceLoader; import java.util.Set; import java.util.TimeZone; +import javax.persistence.EntityManagerFactory; /** * @@ -2095,11 +2094,19 @@ private void loadJsonFunctions() { jpqlFunctionGroup = new JpqlFunctionGroup(AbstractJsonGetFunction.FUNCTION_NAME, false); jpqlFunctionGroup.add("postgresql", new PostgreSQLJsonGetFunction()); jpqlFunctionGroup.add("cockroach", new PostgreSQLJsonGetFunction()); - jpqlFunctionGroup.add("mariadb", new MySQL8JsonGetFunction()); - jpqlFunctionGroup.add("mysql8", new MySQL8JsonGetFunction()); - jpqlFunctionGroup.add("oracle", new OracleJsonGetFunction()); - jpqlFunctionGroup.add("db2", new DB2JsonGetFunction()); - jpqlFunctionGroup.add("microsoft", new MSSQLJsonGetFunction()); + jpqlFunctionGroup.add("mariadb", new MySQLJsonGetFunction( + (ConcatFunction) findFunction(ConcatFunction.FUNCTION_NAME,"mariadb"))); + jpqlFunctionGroup.add("mysql", new MySQLJsonGetFunction( + (ConcatFunction) findFunction(ConcatFunction.FUNCTION_NAME,"mysql"))); + jpqlFunctionGroup.add("mysql8", new MySQLJsonGetFunction( + (ConcatFunction) findFunction(ConcatFunction.FUNCTION_NAME,"mysql8"))); + jpqlFunctionGroup.add("oracle", new OracleJsonGetFunction( + (ConcatFunction) findFunction(ConcatFunction.FUNCTION_NAME,"oracle"))); + jpqlFunctionGroup.add("db2", new DB2JsonGetFunction( + (ConcatFunction) findFunction(ConcatFunction.FUNCTION_NAME,"db2"))); + jpqlFunctionGroup.add("microsoft", new MSSQLJsonGetFunction( + (ConcatFunction) findFunction(ConcatFunction.FUNCTION_NAME,"microsoft"), + (CastFunction) findFunction("cast_string","microsoft"))); registerFunction(jpqlFunctionGroup); // JSON_SET diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/CastFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/CastFunction.java index 2502a1a6a1..960528c20a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/CastFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/CastFunction.java @@ -73,4 +73,12 @@ public String getCastExpression(String argument) { return "cast(" + argument + " as " + defaultSqlCastType + ")"; } + public String startCastExpression() { + return "cast("; + } + + public String endCastExpression(String castType) { + return " as " + (castType == null ? defaultSqlCastType : castType) + ")"; + } + } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java index 69c0fff14e..0d01911301 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java @@ -15,21 +15,22 @@ */ package com.blazebit.persistence.impl.function.jsonget; -import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.impl.function.concat.ConcatFunction; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonFunction; import com.blazebit.persistence.spi.FunctionRenderContext; -import com.blazebit.persistence.spi.JpqlFunction; - -import java.util.ArrayList; -import java.util.List; /** * @author Moritz Becker * @since 1.5.0 */ -public abstract class AbstractJsonGetFunction implements JpqlFunction { +public abstract class AbstractJsonGetFunction extends AbstractJsonFunction { public static final String FUNCTION_NAME = "JSON_GET"; + protected AbstractJsonGetFunction(ConcatFunction concatFunction) { + super(concatFunction); + } + @Override public boolean hasArguments() { return true; @@ -54,38 +55,4 @@ public void render(FunctionRenderContext context) { } protected abstract void render0(FunctionRenderContext context); - - public static String toJsonPath(List pathElements, int to, boolean quotePathElements) { - StringBuilder jsonPathBuilder = new StringBuilder("$"); - for (int i = 0; i < to; i++) { - Object currentPathElement = pathElements.get(i); - if (currentPathElement instanceof Integer) { - jsonPathBuilder.append('['); - jsonPathBuilder.append((int) currentPathElement); - jsonPathBuilder.append(']'); - } else { - jsonPathBuilder.append('.'); - if (quotePathElements) { - jsonPathBuilder.append("\""); - } - jsonPathBuilder.append((String) currentPathElement); - if (quotePathElements) { - jsonPathBuilder.append("\""); - } - } - } - return jsonPathBuilder.toString(); - } - - public static List retrieveJsonPathElements(FunctionRenderContext context, int pathStartOffset) { - List jsonPathElements = new ArrayList<>(context.getArgumentsSize() - pathStartOffset); - for (int i = pathStartOffset; i < context.getArgumentsSize(); i++) { - try { - jsonPathElements.add(Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)))); - } catch (NumberFormatException e) { - jsonPathElements.add(JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)))); - } - } - return jsonPathElements; - } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.java index 25cc2ca7f0..574ee2bdea 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.java @@ -15,8 +15,9 @@ */ package com.blazebit.persistence.impl.function.jsonget; +import com.blazebit.persistence.impl.function.concat.ConcatFunction; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** @@ -25,17 +26,20 @@ */ public class DB2JsonGetFunction extends AbstractJsonGetFunction { + public DB2JsonGetFunction(ConcatFunction concatFunction) { + super(concatFunction); + } + @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); - jsonPathElements.add(0, "val"); - String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), false); - + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 1); + String jsonPathTemplate = AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), false); + jsonPathTemplate = "$.val" + jsonPathTemplate.substring(1); context.addChunk("json_query(concat('{\"val\":', concat("); context.addArgument(0); context.addChunk(", '}'))"); - context.addChunk(",'"); - context.addChunk(jsonPath); - context.addChunk("' OMIT QUOTES)"); + context.addChunk(","); + renderJsonPathTemplate(context, jsonPathTemplate, jsonPathElements.size() + 1); + context.addChunk(" OMIT QUOTES)"); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.java index d9967f2713..d21411cfd6 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.java @@ -15,8 +15,10 @@ */ package com.blazebit.persistence.impl.function.jsonget; +import com.blazebit.persistence.impl.function.cast.CastFunction; +import com.blazebit.persistence.impl.function.concat.ConcatFunction; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** @@ -25,19 +27,33 @@ */ public class MSSQLJsonGetFunction extends AbstractJsonGetFunction { + private final CastFunction castToStringFunction; + + public MSSQLJsonGetFunction(ConcatFunction concatFunction, CastFunction castToStringFunction) { + super(concatFunction); + this.castToStringFunction = castToStringFunction; + } + @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); - String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), true); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 1); + String jsonPathTemplate = AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), true); - context.addChunk("coalesce(json_value("); + // We need to combine json_value and json_query here because json_value is for querying scalar values while + // json_query is for querying JSON objects and arrays. + context.addChunk("(select coalesce(json_value("); context.addArgument(0); - context.addChunk(",'"); - context.addChunk(jsonPath); - context.addChunk("'),json_query("); + context.addChunk(",temp.val),json_query("); context.addArgument(0); - context.addChunk(",'"); - context.addChunk(jsonPath); - context.addChunk("'))"); + context.addChunk(",temp.val)) from (values("); + renderJsonPathTemplate(context, jsonPathTemplate, jsonPathElements.size() + 1); + context.addChunk(")) temp(val))"); + } + + @Override + protected void renderJsonPathTemplateParameter(FunctionRenderContext context, int parameterIdx) { + context.addChunk(castToStringFunction.startCastExpression()); + context.addArgument(parameterIdx); + context.addChunk(castToStringFunction.endCastExpression(null)); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQL8JsonGetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQLJsonGetFunction.java similarity index 53% rename from core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQL8JsonGetFunction.java rename to core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQLJsonGetFunction.java index 90ec5e1487..93ebf807e0 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQL8JsonGetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQLJsonGetFunction.java @@ -15,27 +15,30 @@ */ package com.blazebit.persistence.impl.function.jsonget; +import com.blazebit.persistence.impl.function.concat.ConcatFunction; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** * @author Moritz Becker * @since 1.5.0 */ -public class MySQL8JsonGetFunction extends AbstractJsonGetFunction { +public class MySQLJsonGetFunction extends AbstractJsonGetFunction { + + public MySQLJsonGetFunction(ConcatFunction concatFunction) { + super(concatFunction); + } @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); - String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), true); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 1); + String jsonPathTemplate = AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), true); - context.addChunk("json_unquote("); - context.addChunk("nullif(json_extract("); + context.addChunk("json_unquote(nullif(json_extract("); context.addArgument(0); - context.addChunk(",'"); - context.addChunk(jsonPath); - context.addChunk("')"); - context.addChunk(",cast('null' as json)))"); + context.addChunk(","); + renderJsonPathTemplate(context, jsonPathTemplate, jsonPathElements.size() + 1); + context.addChunk("),cast('null' as json)))"); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.java index fdbbb6d457..004946a9d9 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.java @@ -15,29 +15,37 @@ */ package com.blazebit.persistence.impl.function.jsonget; +import com.blazebit.persistence.impl.function.concat.ConcatFunction; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** + * This function does not support parameterized JSON path templates because the json_value and json_query + * functions in Oracle only work with literals. + * * @author Moritz Becker * @since 1.5.0 */ public class OracleJsonGetFunction extends AbstractJsonGetFunction { + public OracleJsonGetFunction(ConcatFunction concatFunction) { + super(concatFunction); + } + @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); - String jsonPath = toJsonPath(jsonPathElements, jsonPathElements.size(), true); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 1); + String jsonPathTemplate = AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), true); context.addChunk("coalesce(json_value("); context.addArgument(0); - context.addChunk(" format json,'"); - context.addChunk(jsonPath); - context.addChunk("'),nullif(json_query("); + context.addChunk(" format json,"); + renderJsonPathTemplate(context, jsonPathTemplate, jsonPathElements.size() + 1); + context.addChunk("),nullif(json_query("); context.addArgument(0); - context.addChunk(" format json,'"); - context.addChunk(jsonPath); - context.addChunk("'),'null'))"); + context.addChunk(" format json,"); + renderJsonPathTemplate(context, jsonPathTemplate, jsonPathElements.size() + 1); + context.addChunk("),'null'))"); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.java index 2803fbe6aa..677d903c24 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.java @@ -15,8 +15,10 @@ */ package com.blazebit.persistence.impl.function.jsonget; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonFunction; import com.blazebit.persistence.impl.util.JpqlFunctionUtil; import com.blazebit.persistence.spi.FunctionRenderContext; +import java.util.List; /** * @author Moritz Becker @@ -24,18 +26,33 @@ */ public class PostgreSQLJsonGetFunction extends AbstractJsonGetFunction { + public PostgreSQLJsonGetFunction() { + super(null); + } + @Override protected void render0(FunctionRenderContext context) { - context.addChunk("cast("); - context.addArgument(0); - context.addChunk(" as json)"); - context.addChunk("#>>'{"); - addUnquotedArgument(context, 1); - for (int i = 2; i < context.getArgumentsSize(); i++) { - context.addChunk(","); - addUnquotedArgument(context, i); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 1); + Object firstArgument = jsonPathElements.get(0); + if (firstArgument instanceof String && isJsonPathTemplate((String) firstArgument)) { + String jsonPathTemplate = AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), false); + context.addChunk("jsonb_path_query(cast("); + context.addArgument(0); + context.addChunk(" as jsonb),cast("); + renderJsonPathTemplate(context, jsonPathTemplate, jsonPathElements.size() + 1); + context.addChunk(" as jsonpath))"); + } else { + context.addChunk("cast("); + context.addArgument(0); + context.addChunk(" as json)"); + context.addChunk("#>>'{"); + addUnquotedArgument(context, 1); + for (int i = 2; i < context.getArgumentsSize(); i++) { + context.addChunk(","); + addUnquotedArgument(context, i); + } + context.addChunk("}'"); } - context.addChunk("}'"); } private void addUnquotedArgument(FunctionRenderContext context, int argIndex) { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonFunction.java new file mode 100644 index 0000000000..0e7a6d2b0f --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonFunction.java @@ -0,0 +1,126 @@ +/* + * Copyright 2014 - 2024 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.blazebit.persistence.impl.function.jsonset; + +import static com.blazebit.persistence.impl.util.JpqlFunctionUtil.quoteSingle; +import static com.blazebit.persistence.impl.util.JpqlFunctionUtil.unquoteSingleQuotes; + +import com.blazebit.persistence.impl.function.concat.ConcatFunction; +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; +import com.blazebit.persistence.spi.JpqlFunction; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class AbstractJsonFunction implements JpqlFunction { + + private static final String PARAMETER_PLACEHOLDER = "??"; + + private final ConcatFunction concatFunction; + + protected AbstractJsonFunction(ConcatFunction concatFunction) { + this.concatFunction = concatFunction; + } + + protected static boolean isJsonPathTemplate(String argument) { + return argument.startsWith("$"); + } + + /** + * Introduce JSON path template as optional first parameter + * If first parameter is a literal string, interpret it as JSON path. The remaining arguments are interpreted + * as varargs named parameters that are used as positional parameters for the json path template. + * For postgres we need to parse the json path template and reformulate it to the postgres syntax. + */ + protected static String toJsonPathTemplate(List pathElements, int to, boolean quotePathElements) { + Object firstArgument = pathElements.get(0); + if (firstArgument instanceof String && isJsonPathTemplate((String) firstArgument)) { + return (String) firstArgument; + } else { + StringBuilder jsonPathBuilder = new StringBuilder("$"); + for (int i = 0; i < to; i++) { + Object currentPathElement = pathElements.get(i); + if (currentPathElement instanceof Integer) { + jsonPathBuilder.append('['); + jsonPathBuilder.append((int) currentPathElement); + jsonPathBuilder.append(']'); + } else { + jsonPathBuilder.append('.'); + if (quotePathElements) { + jsonPathBuilder.append("\""); + } + jsonPathBuilder.append((String) currentPathElement); + if (quotePathElements) { + jsonPathBuilder.append("\""); + } + } + } + return jsonPathBuilder.toString(); + } + } + + protected void renderJsonPathTemplate(FunctionRenderContext context, String jsonPathTemplate, + int templateParameterOffset) { + List concatenationParts = splitByParameterPlaceholder(jsonPathTemplate); + if (concatenationParts.size() == 1) { + context.addChunk(quoteSingle(jsonPathTemplate)); + } else { + context.addChunk(concatFunction.startConcat()); + context.addChunk(quoteSingle(concatenationParts.get(0))); + for (int i = 1; i < concatenationParts.size(); i++) { + context.addChunk(concatFunction.concatSeparator()); + renderJsonPathTemplateParameter(context, templateParameterOffset++); + context.addChunk(concatFunction.concatSeparator()); + context.addChunk(quoteSingle(concatenationParts.get(i))); + } + context.addChunk(concatFunction.endConcat()); + } + } + + protected void renderJsonPathTemplateParameter(FunctionRenderContext context, int parameterIdx) { + context.addArgument(parameterIdx); + } + + private static List splitByParameterPlaceholder(String str) { + List parts = new ArrayList<>(); + int previousIdx = 0; + int currentIdx; + while ((currentIdx = str.indexOf(PARAMETER_PLACEHOLDER, previousIdx)) != -1) { + parts.add(str.substring(previousIdx, currentIdx)); + previousIdx = currentIdx + PARAMETER_PLACEHOLDER.length(); + } + parts.add(str.substring(previousIdx)); + return parts; + } + + protected static List retrieveJsonPathElements(FunctionRenderContext context, int pathStartOffset) { + String firstArgument = context.getArgument(pathStartOffset); + if (isJsonPathTemplate(unquoteSingleQuotes(firstArgument))) { + return Collections.singletonList((Object) + JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(firstArgument))); + } + List jsonPathElements = new ArrayList<>(context.getArgumentsSize() - pathStartOffset); + for (int i = pathStartOffset; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements.add(Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)))); + } catch (NumberFormatException e) { + jsonPathElements.add(JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)))); + } + } + return jsonPathElements; + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonSetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonSetFunction.java index 64e508e724..01bbcf1c74 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonSetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonSetFunction.java @@ -17,16 +17,19 @@ import com.blazebit.persistence.impl.util.JpqlFunctionUtil; import com.blazebit.persistence.spi.FunctionRenderContext; -import com.blazebit.persistence.spi.JpqlFunction; /** * @author Moritz Becker * @since 1.5.0 */ -public abstract class AbstractJsonSetFunction implements JpqlFunction { +public abstract class AbstractJsonSetFunction extends AbstractJsonFunction { public static final String FUNCTION_NAME = "JSON_SET"; + protected AbstractJsonSetFunction() { + super(null); + } + @Override public boolean hasArguments() { return true; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MSSQLJsonSetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MSSQLJsonSetFunction.java index 97d7a7de58..345379279f 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MSSQLJsonSetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MSSQLJsonSetFunction.java @@ -15,21 +15,19 @@ */ package com.blazebit.persistence.impl.function.jsonset; -import com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** * @author Moritz Becker * @since 1.5.0 */ -public class MSSQLJsonSetFunction extends AbstractJsonGetFunction { +public class MSSQLJsonSetFunction extends AbstractJsonSetFunction { @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 2); - String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), true); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 2); + String jsonPath = AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), true); context.addChunk("(select case when isjson(temp.val) = 0 then (case "); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java index 84b10b2c73..65d569f35b 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java @@ -15,9 +15,7 @@ */ package com.blazebit.persistence.impl.function.jsonset; -import com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** @@ -28,13 +26,15 @@ public class MySQL8JsonSetFunction extends AbstractJsonSetFunction { @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 2); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 2); + String jsonPath = + AbstractJsonFunction.toJsonPathTemplate(jsonPathElements, jsonPathElements.size(), true); context.addChunk("(select "); context.addChunk("case when lower(temp.val) = 'null' then json_set("); context.addArgument(0); context.addChunk(",'"); - context.addChunk(AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), true)); + context.addChunk(jsonPath); context.addChunk("', null) else "); context.addChunk("json_merge_patch("); context.addArgument(0); @@ -66,7 +66,8 @@ private void startJsonPathElement(FunctionRenderContext context, List pa context.addChunk("json_table("); context.addArgument(0); context.addChunk(",'"); - context.addChunk(AbstractJsonGetFunction.toJsonPath(pathElems, curIndex, true) + "[*]"); + String jsonPathTemplate = AbstractJsonFunction.toJsonPathTemplate(pathElems, curIndex, true); + context.addChunk(jsonPathTemplate + "[*]"); context.addChunk("' COLUMNS ("); context.addChunk("rownumber FOR ORDINALITY,"); context.addChunk("complexvalue JSON PATH '$',"); @@ -83,7 +84,7 @@ private void startJsonPathElement(FunctionRenderContext context, List pa if (curIndex < pathElems.size() - 1) { context.addChunk("coalesce(json_merge_patch("); - renderJsonGet(context, AbstractJsonGetFunction.toJsonPath(pathElems, curIndex + 1, true)); + renderJsonGet(context, AbstractJsonFunction.toJsonPathTemplate(pathElems, curIndex + 1, true)); context.addChunk(", concat('"); } else { context.addChunk("concat('"); @@ -128,4 +129,4 @@ private void renderJsonGet(FunctionRenderContext context, String jsonPath) { context.addChunk(jsonPath); context.addChunk("')"); } -} \ No newline at end of file +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java index 6bf105a659..2a7553e323 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java @@ -15,9 +15,7 @@ */ package com.blazebit.persistence.impl.function.jsonset; -import com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; import com.blazebit.persistence.spi.FunctionRenderContext; - import java.util.List; /** @@ -28,7 +26,7 @@ public class OracleJsonSetFunction extends AbstractJsonSetFunction { @Override protected void render0(FunctionRenderContext context) { - List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 2); + List jsonPathElements = AbstractJsonFunction.retrieveJsonPathElements(context, 2); context.addChunk("(select "); context.addChunk("json_mergepatch("); @@ -61,7 +59,7 @@ private void startJsonPathElement(FunctionRenderContext context, List pa context.addChunk("json_table("); context.addArgument(0); context.addChunk(",'"); - context.addChunk(AbstractJsonGetFunction.toJsonPath(pathElems, curIndex, true) + "[*]"); + context.addChunk(AbstractJsonFunction.toJsonPathTemplate(pathElems, curIndex, true) + "[*]"); context.addChunk("' COLUMNS ("); context.addChunk("row_number FOR ORDINALITY,"); context.addChunk("\"complex_value\" varchar2 FORMAT JSON PATH '$',"); @@ -77,7 +75,7 @@ private void startJsonPathElement(FunctionRenderContext context, List pa if (curIndex < pathElems.size() - 1) { context.addChunk("coalesce(json_mergepatch("); - renderJsonGet(context, AbstractJsonGetFunction.toJsonPath(pathElems, curIndex + 1, true)); + renderJsonGet(context, AbstractJsonFunction.toJsonPathTemplate(pathElems, curIndex + 1, true)); context.addChunk(",'"); } else { context.addChunk("'"); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/util/JpqlFunctionUtil.java b/core/impl/src/main/java/com/blazebit/persistence/impl/util/JpqlFunctionUtil.java index 6d2213b02a..653a809d51 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/util/JpqlFunctionUtil.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/util/JpqlFunctionUtil.java @@ -22,11 +22,13 @@ */ public class JpqlFunctionUtil { + private static final char SINGLE_QUOTE_CHARACTER = '\''; + private JpqlFunctionUtil() { } public static String unquoteSingleQuotes(String s) { - return unquote(s, '\''); + return unquote(s, SINGLE_QUOTE_CHARACTER); } public static String unquoteDoubleQuotes(String s) { @@ -60,4 +62,8 @@ private static String unquote(String s, char quoteCharacter) { } return sb.toString(); } + + public static String quoteSingle(String s) { + return SINGLE_QUOTE_CHARACTER + s + SINGLE_QUOTE_CHARACTER; + } } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/JsonDocument.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/JsonDocument.java index f079060896..3577683eb7 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/JsonDocument.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/JsonDocument.java @@ -28,6 +28,7 @@ @Table(name = "json_document") public class JsonDocument { private Long id; +// @Column(columnDefinition = "jsonb") private String content; public JsonDocument() { diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java index fe4cfb8132..b715ab65cf 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java @@ -15,6 +15,10 @@ */ package com.blazebit.persistence.testsuite; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import com.blazebit.persistence.testsuite.base.jpa.category.NoFirebird; import com.blazebit.persistence.testsuite.base.jpa.category.NoH2; import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQLOld; @@ -25,16 +29,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; -import org.junit.experimental.categories.Category; - +import java.util.List; import javax.persistence.EntityManager; import javax.persistence.Tuple; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.experimental.categories.Category; /** * @author Moritz Becker @@ -68,8 +67,8 @@ public void work(EntityManager em) { } @Test - @Category({ NoH2.class, NoSQLite.class, NoFirebird.class, NoMySQLOld.class }) - public void testJsonGet() throws JsonProcessingException { + @Category({ NoH2.class, NoSQLite.class, NoFirebird.class }) + public void testJsonGetWithLiteralPathElements() throws JsonProcessingException { List objectRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") .select("d.content") .select("cast_integer(json_get(d.content, 'K1', '0', 'K2'))") @@ -113,6 +112,46 @@ public void testJsonGet() throws JsonProcessingException { assertEquals(2, arrayRootResult.get(0).get(2)); } + @Test + @Category({ NoH2.class, NoSQLite.class, NoFirebird.class, NoOracle.class }) + public void testJsonGetWithJsonPath() throws JsonProcessingException { + List objectRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + .select("json_get(d.content, '$.K1[??].K3', :array_index_2)") + .select("json_get(d.content, '$.K1[??]', :array_index_0)") + .select("json_get(d.content, '$.??[0]', :k1)") + .select("json_get(d.content, '$.??[??]', :k1, :array_index_0)") + .select("json_get(d.content, '$.K1[0]')") + .where("id").eq(1L) + .setParameter("array_index_0", 0) + .setParameter("array_index_2", 2) + .setParameter("k1", "K1") + .getResultList(); + List arrayRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + .select("cast_integer(json_get(d.content, '$[??]', :array_index_0))") + .select("cast_integer(json_get(d.content, '$[??].??', :array_index_1, :k2))") + .select("cast_integer(json_get(d.content, '$[1].K2'))") + .where("id").eq(2L) + .setParameter("array_index_0", 0) + .setParameter("array_index_1", 1) + .setParameter("k2", "K2") + .getResultList(); + + assertEquals(1, objectRootResult.size()); + JsonNode jsonTestDocument = objectMapper.readTree((String) objectRootResult.get(0).get(0)); + assertNull(objectRootResult.get(0).get(1)); + assertEquals(jsonTestDocument.get("K1").get(0), objectMapper.readTree((String) objectRootResult.get(0).get(2))); + assertEquals(jsonTestDocument.get("K1").get(0), objectMapper.readTree((String) objectRootResult.get(0).get(3))); + assertEquals(jsonTestDocument.get("K1").get(0), objectMapper.readTree((String) objectRootResult.get(0).get(4))); + assertEquals(jsonTestDocument.get("K1").get(0), objectMapper.readTree((String) objectRootResult.get(0).get(5))); + + assertEquals(1, arrayRootResult.size()); + assertEquals(1, arrayRootResult.get(0).get(1)); + assertEquals(2, arrayRootResult.get(0).get(2)); + assertEquals(2, arrayRootResult.get(0).get(3)); + } + @Test @Category({ NoH2.class, NoSQLite.class, NoFirebird.class, NoMySQLOld.class }) public void testJsonSet() throws JsonProcessingException { diff --git a/documentation/src/main/asciidoc/core/manual/en_US/jpql_functions.adoc b/documentation/src/main/asciidoc/core/manual/en_US/jpql_functions.adoc index 6531e3e138..1332e0ae27 100644 --- a/documentation/src/main/asciidoc/core/manual/en_US/jpql_functions.adoc +++ b/documentation/src/main/asciidoc/core/manual/en_US/jpql_functions.adoc @@ -649,6 +649,10 @@ Returns a Base64 encoded string that represents the passed bytes. ==== JSON_GET +There are 2 overloads of the JSON_GET function. + +===== JSON_GET with literal path segments + Sytax: `JSON_GET(jsonDocument, pathSegment1, ..., pathSegmentN)` Where `pathSegmentN` is a quoted literal json key or array index. @@ -666,6 +670,27 @@ json_get('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "foo --> tennis ---- +===== JSON_GET with JSON path + +Sytax: `JSON_GET(jsonDocument, jsonPathTemplate, param1, ..., paramN)` + +Where `jsonPathTemplate` is a JSON path template that uses the special character sequence of `??` as parameter +placeholder. The `paramN` arguments are the parameters that replace the `??` placeholders in the `jsonPathTemplate`. + +Usage examples: + +[source] +---- +json_get('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "football", "tennis" ] } }', '$.owner.??', 'firstName') +--> John + +// Assuming the presence of a named parameter :hobbies_index with value 1 +json_get('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "football", "tennis" ] } }', '$.owner.hobbies[??]', :hobbies_index) +--> tennis +---- + +NOTE: The use of parameters is not supported for Oracle. Only the `jsonPathTemplate` may be passed. + ==== JSON_SET Sytax: `JSON_SET(jsonDocument, newValue, pathSegment1, ..., pathSegmentN)`