From 1d3f9eb10e58923e58518ead45c761f10ef609ef Mon Sep 17 00:00:00 2001 From: Moritz Becker Date: Tue, 14 Jul 2020 16:03:25 +0200 Subject: [PATCH] [#841] Add json functions --- .travis.yml | 2 +- .../CriteriaBuilderConfigurationImpl.java | 43 +++- .../impl/dialect/MySQLDbmsDialect.java | 2 + .../impl/function/cast/CastFunction.java | 8 +- .../impl/function/cast/DB2CastFunction.java | 100 +++++++++ .../impl/function/entity/EntityFunction.java | 8 +- .../jsonget/AbstractJsonGetFunction.java | 74 +++++++ .../function/jsonget/DB2JsonGetFunction.java | 47 ++++ .../jsonget/MSSQLJsonGetFunction.java | 49 +++++ .../jsonget/MySQL8JsonGetFunction.java | 47 ++++ .../jsonget/OracleJsonGetFunction.java | 49 +++++ .../jsonget/PostgreSQLJsonGetFunction.java | 43 ++++ .../jsonset/AbstractJsonSetFunction.java | 58 +++++ .../function/jsonset/DB2JsonSetFunction.java | 44 ++++ .../jsonset/MSSQLJsonSetFunction.java | 66 ++++++ .../jsonset/MySQL8JsonSetFunction.java | 143 +++++++++++++ .../jsonset/OracleJsonSetFunction.java | 147 +++++++++++++ .../jsonset/PostgreSQLJsonSetFunction.java | 41 ++++ .../impl/function/nullfn/NullfnFunction.java | 2 +- .../impl/function/param/ParamFunction.java | 2 +- .../PostgreSQLStringXmlAggFunction.java | 2 +- .../AbstractToStringJsonFunction.java | 2 +- .../AbstractToStringXmlFunction.java | 2 +- .../impl/util/JpqlFunctionUtil.java | 33 ++- .../impl/util/JpqlFunctionUtilTest.java | 34 +++ core/testsuite/pom.xml | 17 +- .../testsuite/entity/JsonDocument.java | 55 +++++ .../main/resources/META-INF/persistence.xml | 2 + .../testsuite/JsonGetAndSetTest.java | 202 ++++++++++++++++++ .../src/test/resources/logging.properties | 2 +- docker_db.sh | 15 +- .../persistence/criteria/UpdateTest.java | 2 +- .../src/test/resources/logging.properties | 2 +- 33 files changed, 1291 insertions(+), 54 deletions(-) create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/DB2CastFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQL8JsonGetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonSetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/DB2JsonSetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MSSQLJsonSetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/PostgreSQLJsonSetFunction.java create mode 100644 core/impl/src/test/java/com/blazebit/persistence/impl/util/JpqlFunctionUtilTest.java create mode 100644 core/testsuite/src/main/hibernate/com/blazebit/persistence/testsuite/entity/JsonDocument.java create mode 100644 core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java diff --git a/.travis.yml b/.travis.yml index a685af92d4..3e16d744b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ before_script: source setup-graalvm.sh fi - bash -c "if [ '$RDBMS' = 'mysql' ]; then MYSQL_VERSION=5.7 bash travis/before_script_mysql.sh; fi" - - bash -c "if [ '$RDBMS' = 'mysql8' ]; then MYSQL_VERSION=8.0 bash travis/before_script_mysql.sh; fi" + - bash -c "if [ '$RDBMS' = 'mysql8' ]; then MYSQL_VERSION=8.0.21 bash travis/before_script_mysql.sh; fi" - bash -c "if [ '$RDBMS' = 'postgresql' ]; then psql -c 'create database test;' -U postgres; fi" - bash -c "if [ '$RDBMS' = 'db2' ]; then bash travis/before_script_db2.sh; fi" - bash -c "if [ '$RDBMS' = 'firebird' ]; then bash travis/before_script_firebird.sh; fi" 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 d71703dfdf..835e21eec8 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 @@ -30,6 +30,7 @@ import com.blazebit.persistence.impl.function.base64.Base64Function; import com.blazebit.persistence.impl.function.base64.PostgreSQLBase64Function; import com.blazebit.persistence.impl.function.cast.CastFunction; +import com.blazebit.persistence.impl.function.cast.DB2CastFunction; import com.blazebit.persistence.impl.function.chr.CharChrFunction; import com.blazebit.persistence.impl.function.chr.ChrFunction; import com.blazebit.persistence.impl.function.colldml.CollectionDmlSupportFunction; @@ -344,6 +345,18 @@ import com.blazebit.persistence.impl.function.groupconcat.MySQLGroupConcatFunction; import com.blazebit.persistence.impl.function.groupconcat.OracleListaggGroupConcatFunction; import com.blazebit.persistence.impl.function.groupconcat.PostgreSQLGroupConcatFunction; +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.OracleJsonGetFunction; +import com.blazebit.persistence.impl.function.jsonget.PostgreSQLJsonGetFunction; +import com.blazebit.persistence.impl.function.jsonset.AbstractJsonSetFunction; +import com.blazebit.persistence.impl.function.jsonset.DB2JsonSetFunction; +import com.blazebit.persistence.impl.function.jsonset.MSSQLJsonSetFunction; +import com.blazebit.persistence.impl.function.jsonset.MySQL8JsonSetFunction; +import com.blazebit.persistence.impl.function.jsonset.OracleJsonSetFunction; +import com.blazebit.persistence.impl.function.jsonset.PostgreSQLJsonSetFunction; import com.blazebit.persistence.impl.function.least.AbstractLeastFunction; import com.blazebit.persistence.impl.function.least.DefaultLeastFunction; import com.blazebit.persistence.impl.function.least.MinLeastFunction; @@ -369,9 +382,9 @@ import com.blazebit.persistence.impl.function.set.SetFunction; import com.blazebit.persistence.impl.function.stringjsonagg.AbstractStringJsonAggFunction; import com.blazebit.persistence.impl.function.stringjsonagg.GroupConcatBasedStringJsonAggFunction; +import com.blazebit.persistence.impl.function.stringjsonagg.MySQLStringJsonAggFunction; import com.blazebit.persistence.impl.function.stringjsonagg.OracleStringJsonAggFunction; import com.blazebit.persistence.impl.function.stringjsonagg.PostgreSQLStringJsonAggFunction; -import com.blazebit.persistence.impl.function.stringjsonagg.MySQLStringJsonAggFunction; import com.blazebit.persistence.impl.function.stringxmlagg.AbstractStringXmlAggFunction; import com.blazebit.persistence.impl.function.stringxmlagg.GroupConcatBasedStringXmlAggFunction; import com.blazebit.persistence.impl.function.stringxmlagg.OracleGroupConcatBasedStringXmlAggFunction; @@ -381,9 +394,9 @@ import com.blazebit.persistence.impl.function.tostringjson.AbstractToStringJsonFunction; import com.blazebit.persistence.impl.function.tostringjson.ForJsonPathToStringJsonFunction; import com.blazebit.persistence.impl.function.tostringjson.GroupConcatBasedToStringJsonFunction; +import com.blazebit.persistence.impl.function.tostringjson.MySQLToStringJsonFunction; import com.blazebit.persistence.impl.function.tostringjson.OracleToStringJsonFunction; import com.blazebit.persistence.impl.function.tostringjson.PostgreSQLToStringJsonFunction; -import com.blazebit.persistence.impl.function.tostringjson.MySQLToStringJsonFunction; import com.blazebit.persistence.impl.function.tostringxml.AbstractToStringXmlFunction; import com.blazebit.persistence.impl.function.tostringxml.ForXmlPathToStringXmlFunction; import com.blazebit.persistence.impl.function.tostringxml.GroupConcatBasedToStringXmlFunction; @@ -703,7 +716,13 @@ private void loadFunctions() { for (Map.Entry dbmsDialectEntry : dbmsDialects.entrySet()) { for (Class type : BasicCastTypes.TYPES) { - functions.get("cast_" + type.getSimpleName().toLowerCase()).add(dbmsDialectEntry.getKey(), new CastFunction(type, dbmsDialectEntry.getValue())); + CastFunction castFunction; + if ("db2".equals(dbmsDialectEntry.getKey())) { + castFunction = new DB2CastFunction(type, dbmsDialectEntry.getValue()); + } else { + castFunction = new CastFunction(type, dbmsDialectEntry.getValue()); + } + functions.get("cast_" + type.getSimpleName().toLowerCase()).add(dbmsDialectEntry.getKey(), castFunction); } } @@ -1758,6 +1777,24 @@ private void loadFunctions() { jpqlFunctionGroup.add(dialectEntry.getKey(), new NthValueFunction(dialectEntry.getValue())); } registerFunction(jpqlFunctionGroup); + + // json_get + jpqlFunctionGroup = new JpqlFunctionGroup(AbstractJsonGetFunction.FUNCTION_NAME, false); + jpqlFunctionGroup.add("postgresql", new PostgreSQLJsonGetFunction()); + jpqlFunctionGroup.add("mysql8", new MySQL8JsonGetFunction()); + jpqlFunctionGroup.add("oracle", new OracleJsonGetFunction()); + jpqlFunctionGroup.add("db2", new DB2JsonGetFunction()); + jpqlFunctionGroup.add("microsoft", new MSSQLJsonGetFunction()); + registerFunction(jpqlFunctionGroup); + + // json_set + jpqlFunctionGroup = new JpqlFunctionGroup(AbstractJsonSetFunction.FUNCTION_NAME, false); + jpqlFunctionGroup.add("postgresql", new PostgreSQLJsonSetFunction()); + jpqlFunctionGroup.add("mysql8", new MySQL8JsonSetFunction()); + jpqlFunctionGroup.add("oracle", new OracleJsonSetFunction()); + jpqlFunctionGroup.add("db2", new DB2JsonSetFunction()); + jpqlFunctionGroup.add("microsoft", new MSSQLJsonSetFunction()); + registerFunction(jpqlFunctionGroup); } private T findFunction(String name, String dbms) { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java index 62595c6b93..4abedaa0f4 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java @@ -44,6 +44,8 @@ protected static Map, String> getSqlTypes() { Map, String> types = new HashMap, String>(); types.put(String.class, "longtext"); + types.put(Integer.class, "signed"); + types.put(Boolean.class, "unsigned"); return types; } 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 21461abaab..f889b6d890 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 @@ -27,9 +27,9 @@ */ public class CastFunction implements JpqlFunction { - private final String functionName; - private final Class castType; - private final String defaultSqlCastType; + protected final String functionName; + protected final Class castType; + protected final String defaultSqlCastType; public CastFunction(Class castType, DbmsDialect dbmsDialect) { this.functionName = "CAST_" + castType.getSimpleName().toUpperCase(); @@ -64,7 +64,7 @@ public void render(FunctionRenderContext context) { if (context.getArgumentsSize() == 1) { context.addChunk(defaultSqlCastType); } else { - context.addChunk(JpqlFunctionUtil.unquote(context.getArgument(1))); + context.addChunk(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(1))); } context.addChunk(")"); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/DB2CastFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/DB2CastFunction.java new file mode 100644 index 0000000000..b66aaa8cf4 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/DB2CastFunction.java @@ -0,0 +1,100 @@ +/* + * Copyright 2014 - 2020 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.cast; + +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.DbmsDialect; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Christian Beikov + * @since 1.2.0 + */ +public class DB2CastFunction extends CastFunction { + + private final String[] clobReturningFunctions = new String[] { + "json_value", + "json_query" + }; + private final String[] clobCompatibleCastTargetTypes = new String[] { + "char", + "varchar", + "graphic", + "vargraphic", + "dbclob", + "blob", + "xml" + }; + + public DB2CastFunction(Class castType, DbmsDialect dbmsDialect) { + super(castType, dbmsDialect); + } + + @Override + public void render(FunctionRenderContext context) { + if (context.getArgumentsSize() != 1 && context.getArgumentsSize() != 2) { + throw new RuntimeException("The " + functionName + " function needs one argument with an optional second argument ! args=" + context); + } + String effectiveCastTargetType; + if (context.getArgumentsSize() == 1) { + effectiveCastTargetType = defaultSqlCastType; + } else { + effectiveCastTargetType = JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(1)); + } + boolean insertVarcharCast = isClobReturningFunction(context.getArgument(0)) && !isClobCompatibleCastTarget(effectiveCastTargetType); + context.addChunk("cast("); + if (insertVarcharCast) { + context.addChunk("cast("); + } + context.addArgument(0); + if (insertVarcharCast) { + context.addChunk(" as varchar(32000))"); + } + context.addChunk(" as "); + context.addChunk(effectiveCastTargetType); + context.addChunk(")"); + } + + @Override + public String getCastExpression(String argument) { + boolean insertVarcharCast = isClobReturningFunction(argument) && !isClobCompatibleCastTarget(defaultSqlCastType); + if (insertVarcharCast) { + return "cast(cast(" + argument + " as varchar(32000)) as " + defaultSqlCastType + ")"; + } else { + return "cast(" + argument + " as " + defaultSqlCastType + ")"; + } + } + + private boolean isClobReturningFunction(String castSource) { + for (int i = 0; i < clobReturningFunctions.length; i++) { + if (castSource.toLowerCase().startsWith(clobReturningFunctions[i] + "(")) { + return true; + } + } + return false; + } + + private boolean isClobCompatibleCastTarget(String castTargetType) { + for (int i = 0; i < clobCompatibleCastTargetTypes.length; i++) { + if (castTargetType.equalsIgnoreCase(clobCompatibleCastTargetTypes[i]) || + castTargetType.toLowerCase().startsWith(clobCompatibleCastTargetTypes[i] + "(")) { + return true; + } + } + return false; + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/entity/EntityFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/entity/EntityFunction.java index 195545b3bd..bc2f61a03a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/entity/EntityFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/entity/EntityFunction.java @@ -68,10 +68,10 @@ public void render(FunctionRenderContext functionRenderContext) { aliasStartIndex--; } - String entityName = JpqlFunctionUtil.unquote(functionRenderContext.getArgument(1)); - String valuesClause = JpqlFunctionUtil.unquote(functionRenderContext.getArgument(2)); - String valuesAliases = JpqlFunctionUtil.unquote(functionRenderContext.getArgument(3)); - String syntheticPredicate = JpqlFunctionUtil.unquote(functionRenderContext.getArgument(4)); + String entityName = JpqlFunctionUtil.unquoteSingleQuotes(functionRenderContext.getArgument(1)); + String valuesClause = JpqlFunctionUtil.unquoteSingleQuotes(functionRenderContext.getArgument(2)); + String valuesAliases = JpqlFunctionUtil.unquoteSingleQuotes(functionRenderContext.getArgument(3)); + String syntheticPredicate = JpqlFunctionUtil.unquoteSingleQuotes(functionRenderContext.getArgument(4)); String valuesTableSqlAlias = subquery.substring(aliasStartIndex, aliasEndIndex); appendSubqueryPart(sb, subquery, 1, subqueryEndIndex, subquery.length() - 1); 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 new file mode 100644 index 0000000000..c67ebad19e --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014 - 2020 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.jsonget; + +import com.blazebit.persistence.spi.FunctionRenderContext; +import com.blazebit.persistence.spi.JpqlFunction; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public abstract class AbstractJsonGetFunction implements JpqlFunction { + + public static final String FUNCTION_NAME = "json_get"; + + @Override + public boolean hasArguments() { + return true; + } + + @Override + public boolean hasParenthesesIfNoArguments() { + return true; + } + + @Override + public Class getReturnType(Class firstArgumentType) { + return String.class; + } + + @Override + public void render(FunctionRenderContext context) { + if (context.getArgumentsSize() < 2) { + throw new RuntimeException("The " + FUNCTION_NAME + " function requires at least two arguments , , ..., ! args=" + context); + } + render0(context); + } + + protected abstract void render0(FunctionRenderContext context); + + public static String toJsonPath(Object[] pathElements, int to, boolean quotePathElements) { + StringBuilder jsonPathBuilder = new StringBuilder("$"); + for (int i = 0; i < to; i++) { + if (pathElements[i] instanceof Integer) { + jsonPathBuilder.append('['); + jsonPathBuilder.append((int) pathElements[i]); + jsonPathBuilder.append(']'); + } else { + jsonPathBuilder.append('.'); + if (quotePathElements) { + jsonPathBuilder.append("\""); + } + jsonPathBuilder.append((String) pathElements[i]); + if (quotePathElements) { + jsonPathBuilder.append("\""); + } + } + } + return jsonPathBuilder.toString(); + } +} 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 new file mode 100644 index 0000000000..17349110e5 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 - 2020 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.jsonget; + +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class DB2JsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize()]; + jsonPathElements[0] = "val"; + for (int i = 1; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i] = JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } + } + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.length, false); + + context.addChunk("json_query(concat('{\"val\":', concat("); + context.addArgument(0); + context.addChunk(", '}'))"); + context.addChunk(",'"); + context.addChunk(jsonPath); + 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 new file mode 100644 index 0000000000..36e2fc7bec --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 - 2020 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.jsonget; + +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MSSQLJsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize() - 1]; + for (int i = 1; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i - 1] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i - 1] = JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } + } + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.length, true); + + context.addChunk("coalesce(json_value("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(jsonPath); + context.addChunk("'),json_query("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(jsonPath); + context.addChunk("'))"); + } +} 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/MySQL8JsonGetFunction.java new file mode 100644 index 0000000000..e74e2552e9 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQL8JsonGetFunction.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014 - 2020 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.jsonget; + +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MySQL8JsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize() - 1]; + for (int i = 1; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i - 1] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i - 1] = JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } + } + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.length, true); + + context.addChunk("json_unquote("); + context.addChunk("nullif(json_extract("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(jsonPath); + context.addChunk("')"); + 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 new file mode 100644 index 0000000000..0dc1f80a86 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 - 2020 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.jsonget; + +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class OracleJsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize() - 1]; + for (int i = 1; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i - 1] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i - 1] = JpqlFunctionUtil.unquoteDoubleQuotes(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } + } + String jsonPath = toJsonPath(jsonPathElements, jsonPathElements.length, true); + + context.addChunk("coalesce(json_value("); + context.addArgument(0); + context.addChunk(" format json,'"); + context.addChunk(jsonPath); + context.addChunk("'),json_query("); + context.addArgument(0); + context.addChunk(" format json,'"); + context.addChunk(jsonPath); + context.addChunk("'))"); + } +} 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 new file mode 100644 index 0000000000..39fec4e81c --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014 - 2020 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.jsonget; + +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class PostgreSQLJsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + context.addArgument(0); + context.addChunk("::json"); + context.addChunk("#>>'{"); + addUnquotedArgument(context, 1); + for (int i = 2; i < context.getArgumentsSize(); i++) { + context.addChunk(","); + addUnquotedArgument(context, i); + } + context.addChunk("}'"); + } + + private void addUnquotedArgument(FunctionRenderContext context, int argIndex) { + context.addChunk(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(argIndex))); + } +} 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 new file mode 100644 index 0000000000..4f48ae728e --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/AbstractJsonSetFunction.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 - 2020 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 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 static final String FUNCTION_NAME = "json_set"; + + @Override + public boolean hasArguments() { + return true; + } + + @Override + public boolean hasParenthesesIfNoArguments() { + return true; + } + + @Override + public Class getReturnType(Class firstArgumentType) { + return String.class; + } + + @Override + public void render(FunctionRenderContext context) { + if (context.getArgumentsSize() < 3) { + throw new RuntimeException("The " + FUNCTION_NAME + " function requires at least 3 arguments , , , ..., ! args=" + context); + } + render0(context); + } + + protected abstract void render0(FunctionRenderContext context); + + protected void addUnquotedArgument(FunctionRenderContext context, int argIndex) { + context.addChunk(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(argIndex))); + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/DB2JsonSetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/DB2JsonSetFunction.java new file mode 100644 index 0000000000..b87aa19f6e --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/DB2JsonSetFunction.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 - 2020 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 com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class DB2JsonSetFunction extends AbstractJsonSetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + context.addChunk("json_query(SYSTOOLS.BSON2JSON(SYSTOOLS.JSON_UPDATE(SYSTOOLS.JSON2BSON("); + context.addChunk("concat('{\"val\":', concat("); + context.addArgument(0); + context.addChunk(", '}'))"); + context.addChunk("),concat('{ $set: {\""); + + context.addChunk("val"); + for (int i = 2; i < context.getArgumentsSize(); i++) { + context.addChunk("."); + addUnquotedArgument(context, i); + } + + context.addChunk("\": ',concat("); + context.addArgument(1); + context.addChunk(",'}}')))), '$.val')"); + } +} 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 new file mode 100644 index 0000000000..fe338c115e --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MSSQLJsonSetFunction.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 - 2020 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 com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MSSQLJsonSetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize() - 2]; + for (int i = 2; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i - 2] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i - 2] = JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)); + } + } + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.length, true); + + context.addChunk("(select case when isjson(temp.val) = 0 then (case "); + + context.addChunk("when TRY_CONVERT(bigint , json_value(concat('{\"val\": ', temp.val, '}'), '$.val')) is not null then json_modify("); + context.addArgument(0); + context.addChunk(", '" + jsonPath + "', CONVERT(bigint, json_value(concat('{\"val\": ', temp.val, '}'), '$.val'))) "); + + context.addChunk("when TRY_CONVERT(float , json_value(concat('{\"val\": ', temp.val, '}'), '$.val')) is not null then json_modify("); + context.addArgument(0); + context.addChunk(", '" + jsonPath + "', CONVERT(float, json_value(concat('{\"val\": ', temp.val, '}'), '$.val'))) "); + + context.addChunk("when TRY_CONVERT(bit, json_value(concat('{\"val\": ', temp.val, '}'), '$.val')) is not null then json_modify("); + context.addArgument(0); + context.addChunk(", '" + jsonPath + "', CONVERT(bit, json_value(concat('{\"val\": ', temp.val, '}'), '$.val'))) "); + + context.addChunk("else json_modify("); + context.addArgument(0); + context.addChunk(", '" + jsonPath + "', json_value(concat('{\"val\": ', temp.val, '}'), '$.val')) end"); + + context.addChunk(") else json_modify("); + context.addArgument(0); + context.addChunk(", '" + jsonPath + "', json_query(concat('{\"val\": ', temp.val, '}'), '$.val')) end "); + + context.addChunk("from (values("); + context.addArgument(1); + context.addChunk(")) temp(val))"); + } +} 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 new file mode 100644 index 0000000000..7deb21ebc7 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java @@ -0,0 +1,143 @@ +/* + * Copyright 2014 - 2020 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 com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MySQL8JsonSetFunction extends AbstractJsonSetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize() - 2]; + for (int i = 2; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i - 2] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i - 2] = JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)); + } + } + + context.addChunk("(select "); + context.addChunk("json_merge_patch("); + context.addArgument(0); + context.addChunk(", concat('"); + + for (int i = 0; i < jsonPathElements.length; i++) { + startJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("', "); + context.addChunk("temp.val"); + context.addChunk(", '"); + for (int i = jsonPathElements.length - 1; i >= 0; i--) { + endJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("'))"); + + context.addChunk(" from (values row("); + context.addArgument(1); + context.addChunk(")) temp(val))"); + } + + private void startJsonPathElement(FunctionRenderContext context, Object[] pathElems, int curIndex) { + Object pathElem = pathElems[curIndex]; + if (pathElem instanceof Integer) { + context.addChunk("[', "); + + if ((int) pathElem > 0) { + context.addChunk("(select GROUP_CONCAT(quoted_array_element.value SEPARATOR ',') from ("); + context.addChunk("select array_element.rownumber, COALESCE(array_element.complexvalue, COALESCE(CASE WHEN array_element.scalarvalue IS NOT NULL AND array_element.numbervalue IS NULL THEN concat('\"', array_element.scalarvalue, '\"') ELSE array_element.scalarvalue END, 'null')) as value from "); + context.addChunk("json_table("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(AbstractJsonGetFunction.toJsonPath(pathElems, curIndex, true) + "[*]"); + context.addChunk("' COLUMNS ("); + context.addChunk("rownumber FOR ORDINALITY,"); + context.addChunk("complexvalue JSON PATH '$',"); + context.addChunk("scalarvalue text PATH '$',"); + context.addChunk("numbervalue numeric PATH '$' null on error"); + context.addChunk(")) array_element) quoted_array_element where quoted_array_element.rownumber < "); + context.addChunk(pathElem.toString()); + context.addChunk("+1"); + context.addChunk(")"); + context.addChunk(", ',', "); + } + if (curIndex < pathElems.length - 1) { + context.addChunk("coalesce(json_merge_patch("); + renderJsonGet(context, AbstractJsonGetFunction.toJsonPath(pathElems, curIndex + 1, true)); + context.addChunk(", concat('"); + } else { + context.addChunk("'"); + } + } else { + context.addChunk("{\""); + context.addChunk((String) pathElem); + context.addChunk("\":"); + } + } + + private void endJsonPathElement(FunctionRenderContext context, Object[] pathElems, int curIndex) { + Object pathElem = pathElems[curIndex]; + if (pathElem instanceof Integer) { + context.addChunk("'"); + if (curIndex < pathElems.length - 1) { + context.addChunk(")), concat('"); + for (int i = curIndex + 1; i < pathElems.length; i++) { + startJsonPathElement(context, pathElems, i); + } + context.addChunk("', temp.val, '"); + for (int i = pathElems.length - 1; i >= curIndex + 1; i--) { + endJsonPathElement(context, pathElems, i); + } + context.addChunk("'))"); + } + context.addChunk(", "); + + context.addChunk("(select coalesce(concat(',', GROUP_CONCAT(quoted_array_element.value SEPARATOR ',')), '') from ("); + context.addChunk("select array_element.rownumber, COALESCE(array_element.complexvalue, COALESCE(CASE WHEN array_element.scalarvalue IS NOT NULL AND array_element.numbervalue IS NULL THEN concat('\"', array_element.scalarvalue, '\"') ELSE array_element.scalarvalue END, 'null')) as value from "); + context.addChunk("json_table("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(AbstractJsonGetFunction.toJsonPath(pathElems, curIndex, true) + "[*]"); + context.addChunk("' COLUMNS ("); + context.addChunk("rownumber FOR ORDINALITY,"); + context.addChunk("complexvalue JSON PATH '$',"); + context.addChunk("scalarvalue text PATH '$',"); + context.addChunk("numbervalue numeric PATH '$' null on error"); + context.addChunk(")) array_element) quoted_array_element where quoted_array_element.rownumber > "); + context.addChunk(pathElem.toString()); + context.addChunk("+1"); + context.addChunk(")"); + + context.addChunk(", ']"); + } else { + context.addChunk("}"); + } + } + + private void renderJsonGet(FunctionRenderContext context, String jsonPath) { + context.addChunk("json_value("); + context.addArgument(0); + context.addChunk(",'"); + 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 new file mode 100644 index 0000000000..9744ea92c4 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java @@ -0,0 +1,147 @@ +/* + * Copyright 2014 - 2020 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 com.blazebit.persistence.impl.function.jsonget.AbstractJsonGetFunction; +import com.blazebit.persistence.impl.util.JpqlFunctionUtil; +import com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class OracleJsonSetFunction extends AbstractJsonSetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + Object[] jsonPathElements = new Object[context.getArgumentsSize() - 2]; + for (int i = 2; i < context.getArgumentsSize(); i++) { + try { + jsonPathElements[i - 2] = Integer.parseInt(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); + } catch (NumberFormatException e) { + jsonPathElements[i - 2] = JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i)); + } + } + + context.addChunk("(select "); + context.addChunk("json_mergepatch("); + context.addArgument(0); + context.addChunk(",'"); + + for (int i = 0; i < jsonPathElements.length; i++) { + startJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("' || "); + context.addChunk("column_value"); + context.addChunk(" || '"); + for (int i = jsonPathElements.length - 1; i >= 0; i--) { + endJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("')"); + + context.addChunk(" from table(sys.ODCIVARCHAR2LIST("); + context.addArgument(1); + context.addChunk(")))"); + } + + private void startJsonPathElement(FunctionRenderContext context, Object[] pathElems, int curIndex) { + Object pathElem = pathElems[curIndex]; + if (pathElem instanceof Integer) { + context.addChunk("[' || "); + + if ((int) pathElem > 0) { + context.addChunk("(select (dbms_xmlgen.convert(substr(xmlagg(xmlelement(e,to_clob(',') || quoted_array_element.value).extract('//text()')).getClobVal(),2),1)) from ("); + context.addChunk("select array_element.row_number, COALESCE(array_element.\"complex_value\", COALESCE(CASE WHEN array_element.\"scalar_value\" IS NOT NULL AND array_element.\"number_value\" IS NULL THEN '\"' || array_element.\"scalar_value\" || '\"' ELSE array_element.\"scalar_value\" END, 'null')) as value from "); + context.addChunk("json_table("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(AbstractJsonGetFunction.toJsonPath(pathElems, curIndex, true) + "[*]"); + context.addChunk("' COLUMNS ("); + context.addChunk("row_number FOR ORDINALITY,"); + context.addChunk("\"complex_value\" varchar2 FORMAT JSON PATH '$',"); + context.addChunk("\"scalar_value\" varchar2 PATH '$',"); + context.addChunk("\"number_value\" number PATH '$' null on error"); + context.addChunk(")) array_element) quoted_array_element where quoted_array_element.row_number < "); + context.addChunk(pathElem.toString()); + context.addChunk("+1"); + context.addChunk(")"); + context.addChunk(" || ',' || "); + } + if (curIndex < pathElems.length - 1) { + context.addChunk("coalesce(json_mergepatch("); + renderJsonGet(context, AbstractJsonGetFunction.toJsonPath(pathElems, curIndex + 1, true)); + context.addChunk(",'"); + } else { + context.addChunk("'"); + } + } else { + context.addChunk("{\""); + context.addChunk((String) pathElem); + context.addChunk("\":"); + } + } + + private void endJsonPathElement(FunctionRenderContext context, Object[] pathElems, int curIndex) { + Object pathElem = pathElems[curIndex]; + if (pathElem instanceof Integer) { + context.addChunk("'"); + if (curIndex < pathElems.length - 1) { + context.addChunk("), '"); + for (int i = curIndex + 1; i < pathElems.length; i++) { + startJsonPathElement(context, pathElems, i); + } + context.addChunk("' || column_value || '"); + for (int i = pathElems.length - 1; i >= curIndex + 1; i--) { + endJsonPathElement(context, pathElems, i); + } + context.addChunk("')"); + } + context.addChunk(" || "); + + context.addChunk("(select ',' || (dbms_xmlgen.convert(substr(xmlagg(xmlelement(e,to_clob(',') || quoted_array_element.value).extract('//text()')).getClobVal(),2),1)) from ("); + context.addChunk("select array_element.row_number, COALESCE(array_element.\"complex_value\", COALESCE(CASE WHEN array_element.\"scalar_value\" IS NOT NULL AND array_element.\"number_value\" IS NULL THEN '\"' || array_element.\"scalar_value\" || '\"' ELSE array_element.\"scalar_value\" END, 'null')) as value from "); + context.addChunk("json_table("); + context.addArgument(0); + context.addChunk(",'"); + context.addChunk(AbstractJsonGetFunction.toJsonPath(pathElems, curIndex, true) + "[*]"); + context.addChunk("' COLUMNS ("); + context.addChunk("row_number FOR ORDINALITY,"); + context.addChunk("\"complex_value\" varchar2 FORMAT JSON PATH '$',"); + context.addChunk("\"scalar_value\" varchar2 PATH '$',"); + context.addChunk("\"number_value\" number PATH '$' null on error"); + context.addChunk(")) array_element) quoted_array_element where quoted_array_element.row_number > "); + context.addChunk(pathElem.toString()); + context.addChunk("+1"); + context.addChunk(")"); + + context.addChunk(" || ']"); + } else { + context.addChunk("}"); + } + } + + private void renderJsonGet(FunctionRenderContext context, String jsonPath) { + context.addChunk("coalesce(json_value("); + context.addArgument(0); + context.addChunk(" format json,'"); + context.addChunk(jsonPath); + context.addChunk("'),json_query("); + context.addArgument(0); + context.addChunk(" format json,'"); + context.addChunk(jsonPath); + context.addChunk("'))"); + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/PostgreSQLJsonSetFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/PostgreSQLJsonSetFunction.java new file mode 100644 index 0000000000..9bd4d1e5d8 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/PostgreSQLJsonSetFunction.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014 - 2020 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 com.blazebit.persistence.spi.FunctionRenderContext; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class PostgreSQLJsonSetFunction extends AbstractJsonSetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + context.addChunk("jsonb_set("); + context.addArgument(0); + context.addChunk("::jsonb,"); + context.addChunk("'{"); + addUnquotedArgument(context, 2); + for (int i = 3; i < context.getArgumentsSize(); i++) { + context.addChunk(","); + addUnquotedArgument(context, i); + } + context.addChunk("}',"); + context.addArgument(1); + context.addChunk(")"); + } +} diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/nullfn/NullfnFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/nullfn/NullfnFunction.java index 78597d7bd6..628f9fa04d 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/nullfn/NullfnFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/nullfn/NullfnFunction.java @@ -55,7 +55,7 @@ public void render(FunctionRenderContext functionRenderContext) { functionRenderContext.addChunk("null"); if (functionRenderContext.getArgumentsSize() > 1) { functionRenderContext.addChunk(" as "); - functionRenderContext.addChunk(JpqlFunctionUtil.unquote(functionRenderContext.getArgument(1))); + functionRenderContext.addChunk(JpqlFunctionUtil.unquoteSingleQuotes(functionRenderContext.getArgument(1))); functionRenderContext.addChunk(")"); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/param/ParamFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/param/ParamFunction.java index 413f8e8e34..6e7ed59a2a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/param/ParamFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/param/ParamFunction.java @@ -48,7 +48,7 @@ public Class getReturnType(Class firstArgumentType) { public void render(FunctionRenderContext functionRenderContext) { if (functionRenderContext.getArgumentsSize() == 2) { functionRenderContext.addChunk("cast(? as "); - functionRenderContext.addChunk(JpqlFunctionUtil.unquote(functionRenderContext.getArgument(1))); + functionRenderContext.addChunk(JpqlFunctionUtil.unquoteSingleQuotes(functionRenderContext.getArgument(1))); functionRenderContext.addChunk(")"); } else { functionRenderContext.addChunk("?"); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/stringxmlagg/PostgreSQLStringXmlAggFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/stringxmlagg/PostgreSQLStringXmlAggFunction.java index 13f78183ef..7ad46c6f37 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/stringxmlagg/PostgreSQLStringXmlAggFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/stringxmlagg/PostgreSQLStringXmlAggFunction.java @@ -38,7 +38,7 @@ public void render(FunctionRenderContext context) { context.addChunk(")"); } else { context.addChunk(", xmlelement(name "); - context.addChunk(JpqlFunctionUtil.unquote(context.getArgument(i))); + context.addChunk(JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i))); } } context.addChunk("))"); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringjson/AbstractToStringJsonFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringjson/AbstractToStringJsonFunction.java index 69366979f3..afc284811b 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringjson/AbstractToStringJsonFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringjson/AbstractToStringJsonFunction.java @@ -59,7 +59,7 @@ public void render(FunctionRenderContext context) { throw new RuntimeException("The to_string_json function argument must have at least as many select items as keys are given! args=" + context); } for (int i = 0; i < fields.length; i++) { - fields[i] = JpqlFunctionUtil.unquote(context.getArgument(i + 1)); + fields[i] = JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i + 1)); } render(context, fields, selectItemExpressions, subquery, fromIndex); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringxml/AbstractToStringXmlFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringxml/AbstractToStringXmlFunction.java index 9649bfe1e1..8764b8a323 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringxml/AbstractToStringXmlFunction.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/tostringxml/AbstractToStringXmlFunction.java @@ -59,7 +59,7 @@ public void render(FunctionRenderContext context) { throw new RuntimeException("The to_string_xml function argument must have at least as many select items as keys are given! args=" + context); } for (int i = 0; i < fields.length; i++) { - fields[i] = JpqlFunctionUtil.unquote(context.getArgument(i + 1)); + fields[i] = JpqlFunctionUtil.unquoteSingleQuotes(context.getArgument(i + 1)); } render(context, fields, selectItemExpressions, subquery, fromIndex); } 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 692a4f3091..1d23bddcbc 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 @@ -25,28 +25,21 @@ public class JpqlFunctionUtil { private JpqlFunctionUtil() { } - public static String unquote(String s) { - StringBuilder sb = new StringBuilder(s.length()); - boolean quote = false; - for (int i = 1; i < s.length() - 1; i++) { - final char c = s.charAt(i); - if (quote) { - quote = false; - if (c != '\'') { - sb.append('\''); - } - sb.append(c); - } else { - if (c == '\'') { - quote = true; - } else { - sb.append(c); - } + public static String unquoteSingleQuotes(String s) { + if (s.length() >= 2) { + if (s.charAt(0) == '\'' && s.charAt(s.length() - 1) == '\'') { + return s.substring(1, s.length() - 1); } } - if (quote) { - sb.append('\''); + return s; + } + + public static String unquoteDoubleQuotes(String s) { + if (s.length() >= 2) { + if (s.charAt(0) == '\"' && s.charAt(s.length() - 1) == '\"') { + return s.substring(1, s.length() - 1); + } } - return sb.toString(); + return s; } } diff --git a/core/impl/src/test/java/com/blazebit/persistence/impl/util/JpqlFunctionUtilTest.java b/core/impl/src/test/java/com/blazebit/persistence/impl/util/JpqlFunctionUtilTest.java new file mode 100644 index 0000000000..ae4ed6af45 --- /dev/null +++ b/core/impl/src/test/java/com/blazebit/persistence/impl/util/JpqlFunctionUtilTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 - 2020 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.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class JpqlFunctionUtilTest { + + @Test + public void testUnquote() { + assertEquals("0", JpqlFunctionUtil.unquoteSingleQuotes("0")); + assertEquals("0", JpqlFunctionUtil.unquoteSingleQuotes("'0'")); + assertEquals("'0'", JpqlFunctionUtil.unquoteSingleQuotes("''0''")); + } +} diff --git a/core/testsuite/pom.xml b/core/testsuite/pom.xml index 96d1eb7a50..f97e493683 100644 --- a/core/testsuite/pom.xml +++ b/core/testsuite/pom.xml @@ -74,6 +74,14 @@ compile + + com.fasterxml.jackson.core + jackson-databind + 2.11.0 + test + + + org.mockito @@ -544,9 +552,9 @@ com.blazebit.persistence.testsuite.base.jpa.category.NoOracle,${jpa.excludedGroups} - jdbc:oracle:thin:@localhost:1521/xe + jdbc:oracle:thin:@localhost:1521:XE SYSTEM - oracle + Oracle18 oracle.jdbc.driver.OracleDriver US @@ -584,8 +592,9 @@ - com.oracle - ojdbc14 + com.oracle.database.jdbc + ojdbc8 + 18.3.0.0 test diff --git a/core/testsuite/src/main/hibernate/com/blazebit/persistence/testsuite/entity/JsonDocument.java b/core/testsuite/src/main/hibernate/com/blazebit/persistence/testsuite/entity/JsonDocument.java new file mode 100644 index 0000000000..3c466b78c4 --- /dev/null +++ b/core/testsuite/src/main/hibernate/com/blazebit/persistence/testsuite/entity/JsonDocument.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014 - 2020 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.testsuite.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +@Entity +@Table(name = "json_document") +public class JsonDocument { + private Long id; + @Column(nullable = false, columnDefinition = "text") + private String content; + + public JsonDocument(Long id, String content) { + this.id = id; + this.content = content; + } + + @Id + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/core/testsuite/src/main/resources/META-INF/persistence.xml b/core/testsuite/src/main/resources/META-INF/persistence.xml index ed4cf690bc..d719210a26 100644 --- a/core/testsuite/src/main/resources/META-INF/persistence.xml +++ b/core/testsuite/src/main/resources/META-INF/persistence.xml @@ -150,6 +150,8 @@ com.blazebit.persistence.testsuite.entity.Sub2Sub1 com.blazebit.persistence.testsuite.entity.Sub2Sub2 + com.blazebit.persistence.testsuite.entity.JsonDocument + true 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 new file mode 100644 index 0000000000..09bbb9eedb --- /dev/null +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2014 - 2020 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.testsuite; + +import com.blazebit.persistence.testsuite.base.jpa.category.NoDB2; +import com.blazebit.persistence.testsuite.base.jpa.category.NoMSSQL; +import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQL; +import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQLOld; +import com.blazebit.persistence.testsuite.base.jpa.category.NoOracle; +import com.blazebit.persistence.testsuite.entity.JsonDocument; +import com.blazebit.persistence.testsuite.tx.TxVoidWork; +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized; + +import javax.persistence.EntityManager; +import javax.persistence.Tuple; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class JsonGetAndSetTest extends AbstractCoreTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected Class[] getEntityClasses() { + return new Class[] { JsonDocument.class }; + } + + @Parameterized.Parameters + public static Collection configurationPermutations() { + return Arrays.asList( + new Object[]{true}, + new Object[]{false} + ); + } + + @Override + public void setUpOnce() { + cleanDatabase(); + String objectRootJsonDocument = "{ \"K1\": [ " + + "{\"K2\": 1}, {\"K2\": \"test\"}, [ 0, 1 ], null, \"null\", true " + + "], \"1\": 4, \"key with blanks\": 3 }"; + String arrayRootJsonDocument = "[ 1, {\"K2\": 2} ]"; + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + JsonDocument d1 = new JsonDocument(1L, objectRootJsonDocument); + JsonDocument d2 = new JsonDocument(2L, arrayRootJsonDocument); + em.persist(d1); + em.persist(d2); + } + }); + } + + @Test + @Category(NoMySQLOld.class) + public void testJsonGet() 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'))") + .select("json_get(d.content, 'K1', '1', 'K2')") + .select("json_get(d.content, 'K1', '0', 'K3')") + .select("json_get(d.content, 'K1', '2', 'K3')") + .select("cast_integer(json_get(d.content, 'K1', '2', '0'))") + .select("json_get(d.content, 'K1', '3')") + .select("json_get(d.content, 'K1', '4')") + .select("CASE WHEN json_get(d.content, 'K1', '5') = 'true' THEN true ELSE false END") + .select("json_get(d.content, 'K1')") + .select("json_get(d.content, 'K1', '0')") + .select("json_get(d.content, '\"1\"')") + .select("json_get(d.content, 'key with blanks')") + .where("id").eq(1L) + .getResultList(); + List arrayRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + .select("cast_integer(json_get(d.content, '0'))") + .select("cast_integer(json_get(d.content, '1', 'K2'))") + .where("id").eq(2L) + .getResultList(); + + assertEquals(1, objectRootResult.size()); + JsonNode jsonTestDocument = objectMapper.readTree((String) objectRootResult.get(0).get(0)); + assertEquals(1, objectRootResult.get(0).get(1)); + assertEquals("test", objectRootResult.get(0).get(2)); + assertNull(objectRootResult.get(0).get(3)); + assertNull(objectRootResult.get(0).get(4)); + assertEquals(0, objectRootResult.get(0).get(5)); + assertNull(objectRootResult.get(0).get(6)); + assertEquals("null", objectRootResult.get(0).get(7)); + assertTrue((boolean) objectRootResult.get(0).get(8)); + assertEquals(jsonTestDocument.get("K1"), objectMapper.readTree((String) objectRootResult.get(0).get(9))); + assertEquals(jsonTestDocument.get("K1").get(0), objectMapper.readTree((String) objectRootResult.get(0).get(10))); + assertEquals(jsonTestDocument.get("1"), objectMapper.readTree((String) objectRootResult.get(0).get(11))); + assertEquals(jsonTestDocument.get("key with blanks"), objectMapper.readTree((String) objectRootResult.get(0).get(12))); + + assertEquals(1, arrayRootResult.size()); + assertEquals(1, arrayRootResult.get(0).get(1)); + assertEquals(2, arrayRootResult.get(0).get(2)); + } + + @Test + @Category(NoMySQLOld.class) + public void testJsonSet() throws JsonProcessingException { + List objectRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + .select("json_set(d.content, '2', 'K1', '0', 'K2')") + .select("json_set(d.content, '2.5', 'K1', '0', 'K2')") + .select("json_set(d.content, '2', 'K1', '0')") + .select("json_set(d.content, '2', 'K1', '2')") + .select("json_set(d.content, '2', 'K1', '5')") + .select("json_set(d.content, '\"test\"', 'K1', '0', 'K2')") + .select("json_set(d.content, '\"test\"', 'K1', '0')") + .select("json_set(d.content, 'true', 'K1', '0', 'K2')") + .select("json_set(d.content, 'true', 'K1', '0')") + .select("json_set(d.content, '4', 'key with blanks')") + .where("id").eq(1L) + .getResultList(); + List arrayRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + .select("json_set(d.content, '{\"K1\": 2}', '0')") + .select("json_set(d.content, '3', '1', 'K2')") + .where("id").eq(2L) + .getResultList(); + assertEquals(1, objectRootResult.size()); + assertEquals(2, objectMapper.readTree((String) objectRootResult.get(0).get(1)).at("/K1/0/K2").intValue()); + assertEquals(2.5, objectMapper.readTree((String) objectRootResult.get(0).get(2)).at("/K1/0/K2").floatValue(), 0.001); + assertEquals(2, objectMapper.readTree((String) objectRootResult.get(0).get(3)).at("/K1/0").intValue()); + + assertEquals(2, objectMapper.readTree((String) objectRootResult.get(0).get(4)).at("/K1/2").intValue()); + + assertEquals(2, objectMapper.readTree((String) objectRootResult.get(0).get(5)).at("/K1/5").intValue()); + + assertEquals("test", objectMapper.readTree((String) objectRootResult.get(0).get(6)).at("/K1/0/K2").textValue()); + assertEquals("test", objectMapper.readTree((String) objectRootResult.get(0).get(7)).at("/K1/0").textValue()); + + assertTrue(objectMapper.readTree((String) objectRootResult.get(0).get(8)).at("/K1/0/K2").booleanValue()); + assertTrue(objectMapper.readTree((String) objectRootResult.get(0).get(9)).at("/K1/0").booleanValue()); + + assertEquals(4, objectMapper.readTree((String) objectRootResult.get(0).get(10)).at("/key with blanks").intValue()); + + assertEquals(1, arrayRootResult.size()); + assertEquals(2, objectMapper.readTree((String) arrayRootResult.get(0).get(1)).at("/0/K1").intValue()); + assertEquals(3, objectMapper.readTree((String) arrayRootResult.get(0).get(2)).at("/1/K2").intValue()); + } + + @Test + @Category({ NoOracle.class, NoMySQL.class }) + public void testJsonSetNull() throws JsonProcessingException { + List objectRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + // json_set with value null not supported for Oracle and MySQL + .select("json_set(d.content, 'null', 'K1', 0, 'K2')") + .select("json_set(d.content, 'null', 'K1', 0)") + .where("id").eq(1L) + .getResultList(); + assertEquals(1, objectRootResult.size()); + assertTrue(objectMapper.readTree((String) objectRootResult.get(0).get(1)).at("/K1/0/K2").isNull()); + assertTrue(objectMapper.readTree((String) objectRootResult.get(0).get(2)).at("/K1/0").isNull()); + } + + + @Test + @Category({ NoMySQLOld.class, NoMSSQL.class, NoDB2.class }) + public void testJsonSetNoMssql() throws JsonProcessingException { + List objectRootResult = cbf.create(em, Tuple.class).from(JsonDocument.class, "d") + .select("d.content") + // json_set with non-existent path not supported for MSSQL and DB2 + .select("json_set(d.content, '2', 'K1', '2', 'K2')") + .select("json_set(d.content, '2', 'K1', '5', 'K2')") + .where("id").eq(1L) + .getResultList(); + assertEquals(1, objectRootResult.size()); + assertEquals(2, objectMapper.readTree((String) objectRootResult.get(0).get(1)).at("/K1/2/K2").intValue()); + assertEquals(2, objectMapper.readTree((String) objectRootResult.get(0).get(2)).at("/K1/5/K2").intValue()); + } +} diff --git a/core/testsuite/src/test/resources/logging.properties b/core/testsuite/src/test/resources/logging.properties index 4504e3764f..749e05191d 100644 --- a/core/testsuite/src/test/resources/logging.properties +++ b/core/testsuite/src/test/resources/logging.properties @@ -21,7 +21,7 @@ handlers = java.util.logging.ConsoleHandler org.hibernate.level = SEVERE org.hibernate.tool.hbm2ddl.level = OFF org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl.level = ALL -#org.hibernate.SQL.level = ALL +org.hibernate.SQL.level = ALL #org.hibernate.type.descriptor.sql.level = ALL #org.hibernate.tool.hbm2ddl.level = ALL #org.hibernate.pretty.level = ALL diff --git a/docker_db.sh b/docker_db.sh index 9eb8950ef3..703a53fe37 100644 --- a/docker_db.sh +++ b/docker_db.sh @@ -7,12 +7,12 @@ mysql_5_7() { mysql_8_0() { docker rm -f mysql || true - docker run --name mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=test -p3306:3306 -d mysql:8.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + docker run --name mysql -e MYSQL_ALLOW_EMPTY_PASSWORD=yes -e MYSQL_DATABASE=test -p3306:3306 -d mysql:8.0.21 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci } -postgresql_9_3() { +postgresql_9_5() { docker rm -f postgres || true - docker run --name postgres -e POSTGRES_DB=test -e POSTGRES_PASSWORD=postgres -p5432:5432 -d postgres:9.3 + docker run --name postgres -e POSTGRES_DB=test -e POSTGRES_PASSWORD=postgres -p5432:5432 -d postgres:9.5 } db2() { @@ -42,12 +42,7 @@ mssql() { oracle() { docker rm -f oracle || true - docker run --shm-size=1536m --name oracle -d -p 1521:1521 wnameless/oracle-xe-11g || echo "Clone and build the docker image from Github first. - git clone https://github.com/wnameless/docker-oracle-xe-11g.git - cd docker-oracle-xe-11g - docker build -t wnameless/oracle-xe-11g . - cd - - docker run --shm-size=1536m --name oracle -d -p 1521:1521 wnameless/oracle-xe-11g" + docker run --shm-size=1536m --name oracle -d -p 1521:1521 quillbuilduser/oracle-18-xe } if [ -z ${1} ]; then @@ -55,7 +50,7 @@ if [ -z ${1} ]; then echo "Provide one of:" echo -e "\tmysql_5_7" echo -e "\tmysql_8_0" - echo -e "\tpostgresql_9_3" + echo -e "\tpostgresql_9_5" echo -e "\tdb2" echo -e "\tmssql" echo -e "\toracle" diff --git a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/UpdateTest.java b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/UpdateTest.java index 30dbafc487..f2a19b8af6 100644 --- a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/UpdateTest.java +++ b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/UpdateTest.java @@ -93,6 +93,6 @@ public void setCorrelatedSubqueryExpression() { UpdateCriteriaBuilder criteriaBuilder = query.createCriteriaBuilder(em); assertEquals("UPDATE Document d SET d.idx = (SELECT " + function("CAST_INTEGER", "COUNT(owner)") + " FROM d.owner owner) + 1", criteriaBuilder.getQueryString()); - criteriaBuilder.getQuery(); + criteriaBuilder.getQuery().executeUpdate(); } } diff --git a/jpa-criteria/testsuite/src/test/resources/logging.properties b/jpa-criteria/testsuite/src/test/resources/logging.properties index 8a9481332d..be91f682cf 100644 --- a/jpa-criteria/testsuite/src/test/resources/logging.properties +++ b/jpa-criteria/testsuite/src/test/resources/logging.properties @@ -21,7 +21,7 @@ handlers = java.util.logging.ConsoleHandler org.hibernate.level = SEVERE org.hibernate.tool.hbm2ddl.level = OFF org.hibernate.tool.schema.internal.ExceptionHandlerLoggedImpl.level = ALL -#org.hibernate.SQL.level = ALL +org.hibernate.SQL.level = ALL #org.hibernate.type.descriptor.sql.level = ALL #org.hibernate.tool.hbm2ddl.level = ALL #org.hibernate.pretty.level = ALL