From ae34408e400391595c8336a488326c66a5061fdd Mon Sep 17 00:00:00 2001
From: Moritz Becker <moritz.becker@gmx.at>
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 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<String, DbmsDialect> 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 extends JpqlFunction> 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<Class<?>, String> getSqlTypes() {
         Map<Class<?>, String> types = new HashMap<Class<?>, 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 <expression> with an optional second argument <sql-type-name>! 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 <jsonField>, <key1|arrayIndex1>, ..., <keyN|arrayIndexN>! 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 <jsonField>, <newValue>, <key1|arrayIndex1>, ..., <keyN|arrayIndexN>! 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 <subquery> 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 <subquery> 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 @@
             <scope>compile</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.11.0</version>
+            <scope>test</scope>
+        </dependency>
+
+
         <!-- Add mockito early to the local maven repo since the Hibernate,DB2 build froze on downloading this dependency -->
         <dependency>
             <groupId>org.mockito</groupId>
@@ -544,9 +552,9 @@
                         <configuration>
                             <excludedGroups>com.blazebit.persistence.testsuite.base.jpa.category.NoOracle,${jpa.excludedGroups}</excludedGroups>
                             <systemPropertyVariables>
-                                <jdbc.url>jdbc:oracle:thin:@localhost:1521/xe</jdbc.url>
+                                <jdbc.url>jdbc:oracle:thin:@localhost:1521:XE</jdbc.url>
                                 <jdbc.user>SYSTEM</jdbc.user>
-                                <jdbc.password>oracle</jdbc.password>
+                                <jdbc.password>Oracle18</jdbc.password>
                                 <jdbc.driver>oracle.jdbc.driver.OracleDriver</jdbc.driver>
                                 <!-- Careful, we need this otherwise the ordering will be wrong... -->
                                 <user.country>US</user.country>
@@ -584,8 +592,9 @@
             </build>
             <dependencies>
                 <dependency>
-                    <groupId>com.oracle</groupId>
-                    <artifactId>ojdbc14</artifactId>
+                    <groupId>com.oracle.database.jdbc</groupId>
+                    <artifactId>ojdbc8</artifactId>
+                    <version>18.3.0.0</version>
                     <scope>test</scope>
                 </dependency>
             </dependencies>
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 @@
         <class>com.blazebit.persistence.testsuite.entity.Sub2Sub1</class>
         <class>com.blazebit.persistence.testsuite.entity.Sub2Sub2</class>
 
+        <class>com.blazebit.persistence.testsuite.entity.JsonDocument</class>
+
         <exclude-unlisted-classes>true</exclude-unlisted-classes>
     </persistence-unit>
 </persistence>
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<Tuple> 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<Tuple> 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<Tuple> 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<Tuple> 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<Tuple> 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<Tuple> 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 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/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<Document> 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