diff --git a/.travis.yml b/.travis.yml index a685af92d4..aa60a57243 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" @@ -62,7 +62,7 @@ matrix: - env: JPAPROVIDER=hibernate-5.2 RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" - env: JPAPROVIDER=hibernate-5.2 RDBMS=mssql jdk: oraclejdk8 sudo: true @@ -89,7 +89,7 @@ matrix: - env: JPAPROVIDER=hibernate-apt RDBMS=postgresql SPRING_DATA=spring-data-2.3.x DELTASPIKE=deltaspike-1.9 jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" # - env: JPAPROVIDER=hibernate-5.2 RDBMS=sqlite # jdk: oraclejdk8 # - env: JPAPROVIDER=hibernate-5.2 RDBMS=firebird @@ -107,7 +107,7 @@ matrix: - env: JPAPROVIDER=datanucleus-5 RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" # - env: JPAPROVIDER=datanucleus-5 RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -138,7 +138,7 @@ matrix: # sudo: required # - env: JPAPROVIDER=eclipselink RDBMS=postgresql # addons: -# postgresql: "9.4" +# postgresql: "9.5" # - env: JPAPROVIDER=eclipselink RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -166,7 +166,7 @@ matrix: # sudo: required # - env: JPAPROVIDER=openjpa RDBMS=postgresql # addons: -# postgresql: "9.4" +# postgresql: "9.5" # - env: JPAPROVIDER=openjpa RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -197,7 +197,7 @@ matrix: - env: JPAPROVIDER=hibernate-5.3 RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" - env: JPAPROVIDER=hibernate-5.3 RDBMS=mssql jdk: oraclejdk8 sudo: true @@ -233,7 +233,7 @@ matrix: - env: JPAPROVIDER=hibernate-5.4 RDBMS=postgresql NATIVE=true jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" script: - travis_wait 70 ./build.sh - env: JPAPROVIDER=hibernate-5.4 RDBMS=mssql NATIVE=true @@ -270,7 +270,7 @@ matrix: # - env: JPAPROVIDER=datanucleus-5.1 RDBMS=postgresql # jdk: oraclejdk8 # addons: -# postgresql: "9.4" +# postgresql: "9.5" # - env: JPAPROVIDER=datanucleus-5.1 RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -303,7 +303,7 @@ matrix: - env: JPAPROVIDER=hibernate-5.1 RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" # - env: JPAPROVIDER=hibernate-5.1 RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -336,7 +336,7 @@ matrix: - env: JPAPROVIDER=hibernate-5.0 RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" # - env: JPAPROVIDER=hibernate-5.0 RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -369,7 +369,7 @@ matrix: - env: JPAPROVIDER=hibernate-4.3 RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" # - env: JPAPROVIDER=hibernate-4.3 RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -401,7 +401,7 @@ matrix: - env: JPAPROVIDER=hibernate RDBMS=postgresql jdk: oraclejdk8 addons: - postgresql: "9.4" + postgresql: "9.5" # - env: JPAPROVIDER=hibernate RDBMS=mssql # jdk: oraclejdk8 # sudo: true @@ -432,7 +432,7 @@ matrix: # sudo: required # - env: JPAPROVIDER=datanucleus-4 RDBMS=postgresql # addons: -# postgresql: "9.4" +# postgresql: "9.5" # jdk: oraclejdk8 # - env: JPAPROVIDER=datanucleus-4 RDBMS=mssql # jdk: oraclejdk8 diff --git a/README.md b/README.md index a3122f57f5..e64c4bab31 100644 --- a/README.md +++ b/README.md @@ -492,16 +492,8 @@ When setting up Oracle locally, keep in mind that when you connect to it, you ha Since the JDBC driver derives values from the locale settings of the JVM, you should set the default locale settings to en_US. In IntelliJ when defining the Oracle database, go to the Advanced tab an specify the JVM options `-Duser.country=us -Duser.language=en`. -When using the Oracle docker container via `docker_db.sh oracle` you might want to specify the following properties when executing tests `-Djdbc.url=jdbc:oracle:thin:@192.168.99.100:1521/xe -Djdbc.user=SYSTEM -Djdbc.password=oracle` - -### JDBC Driver - -You have to install the JDBC driver manually. If you install Oracle XE locally, you can take it from $ORACLE_HOME/jdbc otherwise download it from http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html -Copy the jar to $M2_HOME/com/oracle/ojdbc14/10.2.0.4.0/ojdbc14-10.2.0.4.0.jar and you should be good to go. - -If you use the docker container, extract the jdbc driver from the container via `docker cp oracle:/u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar ojdbc.jar` - -`mvn -q install:install-file -Dfile=ojdbc.jar -DgroupId=com.oracle -DartifactId=ojdbc14 -Dversion=10.2.0.4.0 -Dpackaging=jar -DgeneratePom=true` +For executing tests against the Oracle docker container started via `docker_db.sh oracle` you need to specify the following +system properties `-Djdbc.url=jdbc:oracle:thin:@192.168.99.100:1521/xe -Djdbc.user=SYSTEM -Djdbc.password=oracle -Doracle.jdbc.DateZeroTime=true`. ### Install Oracle locally 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 be24e21609..650c3c9f41 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; @@ -704,7 +717,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); } } @@ -1759,6 +1778,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..e47d76f9fb --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/cast/DB2CastFunction.java @@ -0,0 +1,102 @@ +/* + * 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 static final String[] CLOB_RETURNING_FUNCTIONS = new String[] { + "json_value(", + "json_query(" + }; + private static final String[] CLOB_COMPATIBLE_CAST_TARGET_TYPES = 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 static boolean isClobReturningFunction(String castSource) { + for (int i = 0; i < CLOB_RETURNING_FUNCTIONS.length; i++) { + if (castSource.toLowerCase().startsWith(CLOB_RETURNING_FUNCTIONS[i])) { + return true; + } + } + return false; + } + + private static boolean isClobCompatibleCastTarget(String castTargetType) { + for (int i = 0; i < CLOB_COMPATIBLE_CAST_TARGET_TYPES.length; i++) { + if (castTargetType.toLowerCase().indexOf(CLOB_COMPATIBLE_CAST_TARGET_TYPES[i]) == 0 && + (castTargetType.length() == CLOB_COMPATIBLE_CAST_TARGET_TYPES[i].length() || + castTargetType.charAt(CLOB_COMPATIBLE_CAST_TARGET_TYPES[i].length()) == '(') + ) { + 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..b67b3d88e7 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/AbstractJsonGetFunction.java @@ -0,0 +1,91 @@ +/* + * 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; +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 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(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 new file mode 100644 index 0000000000..f1f8b60455 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/DB2JsonGetFunction.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.jsonget; + +import com.blazebit.persistence.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class DB2JsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); + jsonPathElements.add(0, "val"); + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), 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..3dd7a45b0d --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MSSQLJsonGetFunction.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.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MSSQLJsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), 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..b0d43f1c94 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/MySQL8JsonGetFunction.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.jsonget; + +import com.blazebit.persistence.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MySQL8JsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), 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..f1f5a9d156 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/OracleJsonGetFunction.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.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class OracleJsonGetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 1); + String jsonPath = toJsonPath(jsonPathElements, jsonPathElements.size(), 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..53145fa862 --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonget/PostgreSQLJsonGetFunction.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.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.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("}'"); + } + + 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..aba897d958 --- /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.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MSSQLJsonSetFunction extends AbstractJsonGetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 2); + String jsonPath = AbstractJsonGetFunction.toJsonPath(jsonPathElements, jsonPathElements.size(), 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 case when LOWER(temp.val) = 'null' "); + context.addChunk("then "); + context.addChunk("json_modify("); + context.addArgument(0); + context.addChunk(", 'strict " + jsonPath + "', json_value(concat('{\"val\": ', temp.val, '}'), '$.val')) "); + context.addChunk("else "); + context.addChunk("json_modify("); + context.addArgument(0); + context.addChunk(", '" + jsonPath + "', json_value(concat('{\"val\": ', temp.val, '}'), '$.val')) end 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..a36eb7299d --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/MySQL8JsonSetFunction.java @@ -0,0 +1,131 @@ +/* + * 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.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class MySQL8JsonSetFunction extends AbstractJsonSetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 2); + + 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("', null) else "); + context.addChunk("json_merge_patch("); + context.addArgument(0); + context.addChunk(", concat('"); + + for (int i = 0; i < jsonPathElements.size(); i++) { + startJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("', "); + context.addChunk("temp.val"); + context.addChunk(", '"); + for (int i = jsonPathElements.size() - 1; i >= 0; i--) { + endJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("')) end"); + + context.addChunk(" from (values row("); + context.addArgument(1); + context.addChunk(")) temp(val))"); + } + + private void startJsonPathElement(FunctionRenderContext context, List pathElems, int curIndex) { + Object pathElem = pathElems.get(curIndex); + if (pathElem instanceof Integer) { + context.addChunk("[', "); + + 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 "); + context.addChunk("where array_element.rownumber != "); + context.addChunk(pathElem.toString()); + context.addChunk("+1"); + context.addChunk(" union all "); + context.addChunk("select "); + context.addChunk(pathElem.toString()); + context.addChunk("+1, "); + + if (curIndex < pathElems.size() - 1) { + context.addChunk("coalesce(json_merge_patch("); + renderJsonGet(context, AbstractJsonGetFunction.toJsonPath(pathElems, curIndex + 1, true)); + context.addChunk(", concat('"); + } else { + context.addChunk("concat('"); + } + } else { + context.addChunk("{\""); + context.addChunk((String) pathElem); + context.addChunk("\":"); + } + } + + private void endJsonPathElement(FunctionRenderContext context, List pathElems, int curIndex) { + Object pathElem = pathElems.get(curIndex); + if (pathElem instanceof Integer) { + context.addChunk("'"); + if (curIndex < pathElems.size() - 1) { + context.addChunk(")), concat('"); + for (int i = curIndex + 1; i < pathElems.size(); i++) { + startJsonPathElement(context, pathElems, i); + } + context.addChunk("', temp.val, '"); + for (int i = pathElems.size() - 1; i >= curIndex + 1; i--) { + endJsonPathElement(context, pathElems, i); + } + context.addChunk("'))"); + } else { + context.addChunk(")"); + } + context.addChunk(" order by rownumber"); + context.addChunk(") quoted_array_element)"); + + 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..baf9ef3f9b --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/jsonset/OracleJsonSetFunction.java @@ -0,0 +1,129 @@ +/* + * 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.spi.FunctionRenderContext; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class OracleJsonSetFunction extends AbstractJsonSetFunction { + + @Override + protected void render0(FunctionRenderContext context) { + List jsonPathElements = AbstractJsonGetFunction.retrieveJsonPathElements(context, 2); + + context.addChunk("(select "); + context.addChunk("json_mergepatch("); + context.addArgument(0); + context.addChunk(",'"); + + for (int i = 0; i < jsonPathElements.size(); i++) { + startJsonPathElement(context, jsonPathElements, i); + } + context.addChunk("' || "); + context.addChunk("column_value"); + context.addChunk(" || '"); + for (int i = jsonPathElements.size() - 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, List pathElems, int curIndex) { + Object pathElem = pathElems.get(curIndex); + if (pathElem instanceof Integer) { + 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 where array_element.row_number != "); + context.addChunk(pathElem.toString()); + context.addChunk("+1"); + context.addChunk(" union all "); + context.addChunk("select "); + context.addChunk(pathElem.toString()); + context.addChunk("+1, "); + + if (curIndex < pathElems.size() - 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, List pathElems, int curIndex) { + Object pathElem = pathElems.get(curIndex); + if (pathElem instanceof Integer) { + context.addChunk("'"); + if (curIndex < pathElems.size() - 1) { + context.addChunk("), '"); + for (int i = curIndex + 1; i < pathElems.size(); i++) { + startJsonPathElement(context, pathElems, i); + } + context.addChunk("' || column_value || '"); + for (int i = pathElems.size() - 1; i >= curIndex + 1; i--) { + endJsonPathElement(context, pathElems, i); + } + context.addChunk("')"); + } + + context.addChunk("from table (sys.ODCIVARCHAR2LIST(column_value)) "); + context.addChunk("order by row_number"); + context.addChunk(") quoted_array_element) "); + + 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..4e192d57fc --- /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(cast("); + context.addArgument(0); + context.addChunk(" as 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..c219400e8f 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,19 +25,30 @@ public class JpqlFunctionUtil { private JpqlFunctionUtil() { } - public static String unquote(String s) { + public static String unquoteSingleQuotes(String s) { + return unquote(s, '\''); + } + + public static String unquoteDoubleQuotes(String s) { + return unquote(s, '\"'); + } + + private static String unquote(String s, char quoteCharacter) { + if (!s.isEmpty() && s.charAt(0) != quoteCharacter) { + return 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('\''); + if (c != quoteCharacter) { + sb.append(quoteCharacter); } sb.append(c); } else { - if (c == '\'') { + if (c == quoteCharacter) { quote = true; } else { sb.append(c); @@ -45,7 +56,7 @@ public static String unquote(String s) { } } if (quote) { - sb.append('\''); + sb.append(quoteCharacter); } return sb.toString(); } 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..6a2faded56 --- /dev/null +++ b/core/impl/src/test/java/com/blazebit/persistence/impl/util/JpqlFunctionUtilTest.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.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class JpqlFunctionUtilTest { + + @Test + public void testUnquoteSingleQuotes() { + assertEquals("0", JpqlFunctionUtil.unquoteSingleQuotes("0")); + assertEquals("0", JpqlFunctionUtil.unquoteSingleQuotes("'0'")); + assertEquals("'0'", JpqlFunctionUtil.unquoteSingleQuotes("''0''")); + } + + @Test + public void testUnquoteDoubleQuotes() { + assertEquals("0", JpqlFunctionUtil.unquoteDoubleQuotes("0")); + assertEquals("0", JpqlFunctionUtil.unquoteDoubleQuotes("\"0\"")); + assertEquals("\"0\"", JpqlFunctionUtil.unquoteDoubleQuotes("\"\"0\"\"")); + } +} diff --git a/core/testsuite/pom.xml b/core/testsuite/pom.xml index 96d1eb7a50..d0d698c10c 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,13 +552,16 @@ 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 en + + + true @@ -584,8 +595,8 @@ - com.oracle - ojdbc14 + com.oracle.database.jdbc + ojdbc8 test 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 new file mode 100644 index 0000000000..03485777b2 --- /dev/null +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/JsonDocument.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.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; + private String content; + + public JsonDocument() { + } + + 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; + } + + @Column(nullable = false) + 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..1025086ffe --- /dev/null +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JsonGetAndSetTest.java @@ -0,0 +1,181 @@ +/* + * 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.NoFirebird; +import com.blazebit.persistence.testsuite.base.jpa.category.NoH2; +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.base.jpa.category.NoSQLite; +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 }; + } + + @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({ NoH2.class, NoSQLite.class, NoFirebird.class, 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({ NoH2.class, NoSQLite.class, NoFirebird.class, 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({ NoH2.class, NoSQLite.class, NoFirebird.class, NoMySQLOld.class, NoOracle.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 + .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()); + } +} diff --git a/docker_db.sh b/docker_db.sh index 7073861e00..d576ff58a3 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_4() { +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.4 + docker run --name postgres -e POSTGRES_DB=test -e POSTGRES_PASSWORD=postgres -p5432:5432 -d postgres:9.5 } db2() { @@ -51,12 +51,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 @@ -64,7 +59,7 @@ if [ -z ${1} ]; then echo "Provide one of:" echo -e "\tmysql_5_7" echo -e "\tmysql_8_0" - echo -e "\tpostgresql_9_4" + echo -e "\tpostgresql_9_5" echo -e "\tdb2" echo -e "\tmssql" echo -e "\toracle" 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 8e2befa56d..62f6bd541f 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 @@ -531,6 +531,46 @@ Syntax: `BASE64 ( bytes )` Returns a Base64 encoded string that represents the passed bytes. +==== JSON_GET + +Sytax: `JSON_GET(jsonDocument, pathSegment1, ..., pathSegmentN)` + +Where `pathSegmentN` is a quoted literal json key or array index. + +Returns the json node (scalar, object or array) within the `jsonDocument` designated by the path segments. + +Usage examples: + +[source] +---- +json_get('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "football", "tennis" ] } }', 'owner', 'firstName') +--> John + +json_get('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "football", "tennis" ] } }', 'owner', 'hobbies', '1') +--> tennis +---- + +==== JSON_SET + +Sytax: `JSON_SET(jsonDocument, newValue, pathSegment1, ..., pathSegmentN)` + +Where `newValue` is a quoted json node (scalar, object or array) and `pathSegmentN` is a quoted literal json key or array index. + +Returns the modified `jsonDocument` that results from replacing the json node designated by the path segments with `newValue`. + +Setting JSON `null` is not supported for Oracle. + +Usage examples: + +[source] +---- +json_set('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "football", "tennis" ] } }', 'James', 'owner', 'firstName') +--> { "owner": { "firstName": "James", "lastName": "Smith", hobbies: [ "football", "tennis" ] } } + +json_set('{ "owner": { "firstName": "John", "lastName": "Smith", hobbies: [ "football", "tennis" ] } }', 'table tennis', 'owner', 'hobbies', '1') +--> { "owner": { "firstName": "James", "lastName": "Smith", hobbies: [ "football", "table tennis" ] } } +---- + ==== STRING_JSON_AGG function Syntax: `STRING_JSON_AGG ( key1, value1, ..., keyN, valueN )` diff --git a/entity-view/testsuite/pom.xml b/entity-view/testsuite/pom.xml index 94b660a871..1180f2d5d5 100644 --- a/entity-view/testsuite/pom.xml +++ b/entity-view/testsuite/pom.xml @@ -423,13 +423,16 @@ 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 + US en + + + true @@ -444,10 +447,10 @@ - com.oracle - ojdbc14 - test - + com.oracle.database.jdbc + ojdbc8 + test + diff --git a/integration/deltaspike-data/testsuite/pom.xml b/integration/deltaspike-data/testsuite/pom.xml index b73447a626..6131c343d8 100644 --- a/integration/deltaspike-data/testsuite/pom.xml +++ b/integration/deltaspike-data/testsuite/pom.xml @@ -374,10 +374,16 @@ 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 + en + + + true ${spring.activeProfiles} @@ -393,8 +399,8 @@ - com.oracle - ojdbc14 + com.oracle.database.jdbc + ojdbc8 test diff --git a/integration/querydsl/testsuite/pom.xml b/integration/querydsl/testsuite/pom.xml index 3f8a735bbf..b608149350 100644 --- a/integration/querydsl/testsuite/pom.xml +++ b/integration/querydsl/testsuite/pom.xml @@ -342,13 +342,16 @@ 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 en + + + true @@ -363,8 +366,8 @@ - com.oracle - ojdbc14 + com.oracle.database.jdbc + ojdbc8 test diff --git a/integration/spring-data/testsuite/webflux/pom.xml b/integration/spring-data/testsuite/webflux/pom.xml index f01c720d8d..5bd7dbc20b 100644 --- a/integration/spring-data/testsuite/webflux/pom.xml +++ b/integration/spring-data/testsuite/webflux/pom.xml @@ -393,10 +393,16 @@ 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 + en + + + true ${spring.activeProfiles} @@ -412,8 +418,8 @@ - com.oracle - ojdbc14 + com.oracle.database.jdbc + ojdbc8 test diff --git a/integration/spring-data/testsuite/webmvc/pom.xml b/integration/spring-data/testsuite/webmvc/pom.xml index 578d7304af..41aa0a76df 100644 --- a/integration/spring-data/testsuite/webmvc/pom.xml +++ b/integration/spring-data/testsuite/webmvc/pom.xml @@ -403,10 +403,16 @@ 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 + en + + + true ${spring.activeProfiles} @@ -422,8 +428,8 @@ - com.oracle - ojdbc14 + com.oracle.database.jdbc + ojdbc8 test diff --git a/parent/pom.xml b/parent/pom.xml index c2305ef4d5..0b9f9c2e4e 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -568,9 +568,9 @@ test - com.oracle - ojdbc14 - 10.2.0.4.0 + com.oracle.database.jdbc + ojdbc8 + 19.7.0.0 test diff --git a/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java b/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java index a038f4135c..ab2de0b344 100644 --- a/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java +++ b/testsuite-base/hibernate/src/main/java/com/blazebit/persistence/testsuite/base/AbstractPersistenceTest.java @@ -140,6 +140,9 @@ protected Properties applyProperties(Properties properties) { if (isHibernate5()) { properties.put("hibernate.id.new_generator_mappings", "false"); } + } else if (isHibernate5() && properties.get("javax.persistence.jdbc.url").toString().contains("oracle")) { + // Apparently the dialect resolver doesn't choose the latest dialect + properties.put("hibernate.dialect", "org.hibernate.dialect.Oracle10gDialect"); } if (useHbm2ddl()) { diff --git a/travis/before_script_oracle.sh b/travis/before_script_oracle.sh index 296fcaec5b..63227b7007 100644 --- a/travis/before_script_oracle.sh +++ b/travis/before_script_oracle.sh @@ -3,11 +3,4 @@ # Sets up environment for Blaze-Persistence backend MSSQL at travis-ci.com # -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 cp oracle:/u01/app/oracle/product/11.2.0/xe/jdbc/lib/ojdbc6.jar ojdbc.jar -mvn install:install-file -Dfile=ojdbc.jar -DgroupId=com.oracle -DartifactId=ojdbc14 -Dversion=10.2.0.4.0 -Dpackaging=jar -DgeneratePom=true +docker run --shm-size=1536m --name oracle -d -p 1521:1521 quillbuilduser/oracle-18-xe \ No newline at end of file