HHH-18496 Add json_query

This commit is contained in:
Christian Beikov 2024-09-09 19:09:31 +02:00
parent 6454aaf055
commit 6b4cc28f0e
48 changed files with 1837 additions and 110 deletions

View File

@ -1633,6 +1633,7 @@ The following functions deal with SQL JSON types, which are not supported on eve
| `json_array()` | Constructs a JSON array from arguments
| `json_value()` | Extracts a value from a JSON document by JSON path
| `json_exists()` | Checks if a JSON path exists in a JSON document
| `json_query()` | Queries non-scalar values by JSON path in a JSON document
|===
@ -1712,7 +1713,7 @@ include::{extrasdir}/json_value_bnf.txt[]
The first argument is an expression to a JSON document. The second argument is a JSON path as String expression.
WARNING: Some databases might also return non-scalar values. Beware that this behavior is not portable.
WARNING: Some databases might also allow extracting non-scalar values. Beware that this behavior is not portable.
NOTE: It is recommended to only us the dot notation for JSON paths instead of the bracket notation,
since most databases support only that.
@ -1832,6 +1833,92 @@ include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-on-erro
NOTE: The H2 emulation only supports absolute JSON paths using the dot notation.
[[hql-json-query-function]]
===== `json_query()`
Queries non-scalar values from a JSON document by a https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path].
[[hql-json-query-bnf]]
[source, antlrv4, indent=0]
----
include::{extrasdir}/json_query_bnf.txt[]
----
The first argument is an expression to a JSON document. The second argument is a JSON path as String expression.
WARNING: Some databases might also allow querying scalar values. Beware that this behavior is not portable.
NOTE: It is recommended to only us the dot notation for JSON paths instead of the bracket notation,
since most databases support only that.
[[hql-json-query-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-example]
----
====
The `passing` clause allows to reuse the same JSON path but pass different values for evaluation.
[[hql-json-query-passing-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-passing-example]
----
====
The `wrapper` clause allows to specify whether results of a query should be wrapped in brackets `[]` i.e. an array.
The default behavior is to omit an array wrapper i.e. `without wrapper`.
It is an error when a `json_query` returns more than a single result and `without wrapper` is used.
How an error like this should be handled can be controlled with the `on error` clause.
WARNING: Since the default behavior of `on error` is database dependent,
some databases might return a comma separated list of values even when using `without wrapper`. This is not portable.
[[hql-json-query-wrapper-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-with-wrapper-example]
----
====
The `on error` clause defines the behavior when an error occurs while querying with the JSON path.
Conditions that classify as errors are database dependent, but usual errors which can be handled with this clause are:
* First argument is not a valid JSON document
* Second argument is not a valid JSON path
* Multiple `json_query` results when `without wrapper` is used
The default behavior of `on error` is database specific, but usually, `null` is returned on an error.
It is recommended to specify this clause when the exact error behavior is important.
[[hql-json-query-on-error-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-on-error-example]
----
====
The `on empty` clause defines the behavior when the JSON path does not match the JSON document.
By default, `null` is returned on empty.
[[hql-json-query-on-empty-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-on-empty-example]
----
====
To actually receive an error `on empty`, it is necessary to also specify `error on error`.
Depending on the database, an error might still be thrown even without that, but that is not portable.
NOTE: The H2 emulation only supports absolute JSON paths using the dot notation.
[[hql-user-defined-functions]]
==== Native and user-defined functions

View File

@ -1,4 +1,4 @@
"json_exists(" expression, expression passingClause? onErrorClause? ")"
"json_exists(" expression "," expression passingClause? onErrorClause? ")"
passingClause
: "passing" expression "as" identifier ("," expression "as" identifier)*

View File

@ -0,0 +1,14 @@
"json_query(" expression "," expression passingClause? wrapperClause? onErrorClause? onEmptyClause? ")"
wrapperClause
: "with" ("conditional"|"unconditional")? "array"? "wrapper"
| "without" "array"? "wrapper"
passingClause
: "passing" expression "as" identifier ("," expression "as" identifier)*
onErrorClause
: ( "error" | "null" | ( "empty" ( "array" | "object" )? ) ) "on error";
onEmptyClause
: ( "error" | "null" | ( "empty" ( "array" | "object" )? ) ) "on empty";

View File

@ -1,4 +1,4 @@
"json_value(" expression, expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")"
"json_value(" expression "," expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")"
passingClause
: "passing" expression "as" identifier ("," expression "as" identifier)*

View File

@ -432,6 +432,7 @@ public class DB2LegacyDialect extends Dialect {
if ( getDB2Version().isSameOrAfter( 11 ) ) {
functionFactory.jsonValue_no_passing();
functionFactory.jsonQuery_no_passing();
functionFactory.jsonExists_no_passing();
functionFactory.jsonObject_db2();
functionFactory.jsonArray_db2();

View File

@ -404,6 +404,7 @@ public class H2LegacyDialect extends Dialect {
if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) {
functionFactory.jsonValue_h2();
functionFactory.jsonQuery_h2();
functionFactory.jsonExists_h2();
}
}

View File

@ -654,6 +654,7 @@ public class MySQLLegacyDialect extends Dialect {
if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) {
functionFactory.jsonValue_mysql();
functionFactory.jsonQuery_mysql();
functionFactory.jsonExists_mysql();
functionFactory.jsonObject_mysql();
functionFactory.jsonArray_mysql();

View File

@ -323,6 +323,7 @@ public class OracleLegacyDialect extends Dialect {
if ( getVersion().isSameOrAfter( 12 ) ) {
functionFactory.jsonValue_oracle();
functionFactory.jsonQuery_oracle();
functionFactory.jsonExists_oracle();
functionFactory.jsonObject_oracle();
functionFactory.jsonArray_oracle();

View File

@ -634,12 +634,14 @@ public class PostgreSQLLegacyDialect extends Dialect {
if ( getVersion().isSameOrAfter( 17 ) ) {
functionFactory.jsonValue();
functionFactory.jsonQuery();
functionFactory.jsonExists();
functionFactory.jsonObject();
functionFactory.jsonArray();
}
else {
functionFactory.jsonValue_postgresql();
functionFactory.jsonQuery_postgresql();
functionFactory.jsonExists_postgresql();
if ( getVersion().isSameOrAfter( 16 ) ) {
functionFactory.jsonObject();

View File

@ -402,6 +402,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect {
functionFactory.hypotheticalOrderedSetAggregates_windowEmulation();
if ( getVersion().isSameOrAfter( 13 ) ) {
functionFactory.jsonValue_sqlserver();
functionFactory.jsonQuery_sqlserver();
functionFactory.jsonExists_sqlserver();
functionFactory.jsonObject_sqlserver();
functionFactory.jsonArray_sqlserver();

View File

@ -149,6 +149,7 @@ ABSENT : [aA] [bB] [sS] [eE] [nN] [tT];
ALL : [aA] [lL] [lL];
AND : [aA] [nN] [dD];
ANY : [aA] [nN] [yY];
ARRAY : [aA] [rR] [rR] [aA] [yY];
AS : [aA] [sS];
ASC : [aA] [sS] [cC];
AVG : [aA] [vV] [gG];
@ -160,6 +161,7 @@ CASE : [cC] [aA] [sS] [eE];
CAST : [cC] [aA] [sS] [tT];
COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE];
COLUMN : [cC] [oO] [lL] [uU] [mM] [nN];
CONDITIONAL : [cC] [oO] [nN] [dD] [iI] [tT] [iI] [oO] [nN] [aA] [lL];
CONFLICT : [cC] [oO] [nN] [fF] [lL] [iI] [cC] [tT];
CONSTRAINT : [cC] [oO] [nN] [sS] [tT] [rR] [aA] [iI] [nN] [tT];
CONTAINS : [cC] [oO] [nN] [tT] [aA] [iI] [nN] [sS];
@ -225,6 +227,7 @@ JOIN : [jJ] [oO] [iI] [nN];
JSON_ARRAY : [jJ] [sS] [oO] [nN] '_' [aA] [rR] [rR] [aA] [yY];
JSON_EXISTS : [jJ] [sS] [oO] [nN] '_' [eE] [xX] [iI] [sS] [tT] [sS];
JSON_OBJECT : [jJ] [sS] [oO] [nN] '_' [oO] [bB] [jJ] [eE] [cC] [tT];
JSON_QUERY : [jJ] [sS] [oO] [nN] '_' [qQ] [uU] [eE] [rR] [yY];
JSON_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE];
KEY : [kK] [eE] [yY];
KEYS : [kK] [eE] [yY] [sS];
@ -310,6 +313,7 @@ TRUNC : [tT] [rR] [uU] [nN] [cC];
TRUNCATE : [tT] [rR] [uU] [nN] [cC] [aA] [tT] [eE];
TYPE : [tT] [yY] [pP] [eE];
UNBOUNDED : [uU] [nN] [bB] [oO] [uU] [nN] [dD] [eE] [dD];
UNCONDITIONAL : [uU] [nN] [cC] [oO] [nN] [dD] [iI] [tT] [iI] [oO] [nN] [aA] [lL];
UNION : [uU] [nN] [iI] [oO] [nN];
UPDATE : [uU] [pP] [dD] [aA] [tT] [eE];
USING : [uU] [sS] [iI] [nN] [gG];
@ -321,6 +325,7 @@ WHERE : [wW] [hH] [eE] [rR] [eE];
WITH : [wW] [iI] [tT] [hH];
WITHIN : [wW] [iI] [tT] [hH] [iI] [nN];
WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT];
WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR];
YEAR : [yY] [eE] [aA] [rR];
ZONED : [zZ] [oO] [nN] [eE] [dD];

View File

@ -1625,6 +1625,7 @@ jsonFunction
: jsonArrayFunction
| jsonExistsFunction
| jsonObjectFunction
| jsonQueryFunction
| jsonValueFunction
;
@ -1646,6 +1647,21 @@ jsonValueReturningClause
jsonValueOnErrorOrEmptyClause
: ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY);
/**
* The 'json_query()' function
*/
jsonQueryFunction
: JSON_QUERY LEFT_PAREN expression COMMA expression jsonPassingClause? jsonQueryWrapperClause? jsonQueryOnErrorOrEmptyClause? jsonQueryOnErrorOrEmptyClause? RIGHT_PAREN
;
jsonQueryWrapperClause
: WITH (CONDITIONAL|UNCONDITIONAL)? ARRAY? WRAPPER
| WITHOUT ARRAY? WRAPPER
;
jsonQueryOnErrorOrEmptyClause
: ( ERROR | NULL | ( EMPTY ( ARRAY | OBJECT )? ) ) ON (ERROR|EMPTY);
/**
* The 'json_exists()' function
*/
@ -1699,6 +1715,7 @@ jsonNullClause
| ALL
| AND
| ANY
| ARRAY
| AS
| ASC
| AVG
@ -1710,6 +1727,7 @@ jsonNullClause
| CAST
| COLLATE
| COLUMN
| CONDITIONAL
| CONFLICT
| CONSTRAINT
| CONTAINS
@ -1777,6 +1795,7 @@ jsonNullClause
| JSON_ARRAY
| JSON_EXISTS
| JSON_OBJECT
| JSON_QUERY
| JSON_VALUE
| KEY
| KEYS
@ -1863,6 +1882,7 @@ jsonNullClause
| TRUNCATE
| TYPE
| UNBOUNDED
| UNCONDITIONAL
| UNION
| UPDATE
| USING
@ -1876,6 +1896,7 @@ jsonNullClause
| WITH
| WITHIN
| WITHOUT
| WRAPPER
| YEAR
| ZONED) {
logUseOfReservedWordAsIdentifier( getCurrentToken() );

View File

@ -418,6 +418,7 @@ public class DB2Dialect extends Dialect {
if ( getDB2Version().isSameOrAfter( 11 ) ) {
functionFactory.jsonValue_no_passing();
functionFactory.jsonQuery_no_passing();
functionFactory.jsonExists_no_passing();
functionFactory.jsonObject_db2();
functionFactory.jsonArray_db2();

View File

@ -347,6 +347,7 @@ public class H2Dialect extends Dialect {
functionFactory.jsonArray();
if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) {
functionFactory.jsonValue_h2();
functionFactory.jsonQuery_h2();
functionFactory.jsonExists_h2();
}
}

View File

@ -492,6 +492,7 @@ public class HANADialect extends Dialect {
if ( getVersion().isSameOrAfter(2, 0, 20) ) {
// Introduced in 2.0 SPS 02
functionFactory.jsonValue_no_passing();
functionFactory.jsonQuery_no_passing();
functionFactory.jsonExists_hana();
if ( getVersion().isSameOrAfter(2, 0, 40) ) {
// Introduced in 2.0 SPS 04

View File

@ -639,6 +639,7 @@ public class MySQLDialect extends Dialect {
functionFactory.listagg_groupConcat();
functionFactory.jsonValue_mysql();
functionFactory.jsonQuery_mysql();
functionFactory.jsonExists_mysql();
functionFactory.jsonObject_mysql();
functionFactory.jsonArray_mysql();

View File

@ -400,6 +400,7 @@ public class OracleDialect extends Dialect {
functionFactory.arrayToString_oracle();
functionFactory.jsonValue_oracle();
functionFactory.jsonQuery_oracle();
functionFactory.jsonExists_oracle();
functionFactory.jsonObject_oracle();
functionFactory.jsonArray_oracle();

View File

@ -595,12 +595,14 @@ public class PostgreSQLDialect extends Dialect {
if ( getVersion().isSameOrAfter( 17 ) ) {
functionFactory.jsonValue();
functionFactory.jsonQuery();
functionFactory.jsonExists();
functionFactory.jsonObject();
functionFactory.jsonArray();
}
else {
functionFactory.jsonValue_postgresql();
functionFactory.jsonQuery_postgresql();
functionFactory.jsonExists_postgresql();
if ( getVersion().isSameOrAfter( 16 ) ) {
functionFactory.jsonObject();

View File

@ -420,6 +420,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect {
functionFactory.hypotheticalOrderedSetAggregates_windowEmulation();
if ( getVersion().isSameOrAfter( 13 ) ) {
functionFactory.jsonValue_sqlserver();
functionFactory.jsonQuery_sqlserver();
functionFactory.jsonExists_sqlserver();
functionFactory.jsonObject_sqlserver();
functionFactory.jsonArray_sqlserver();

View File

@ -80,6 +80,7 @@ import org.hibernate.dialect.function.json.CockroachDBJsonValueFunction;
import org.hibernate.dialect.function.json.DB2JsonArrayFunction;
import org.hibernate.dialect.function.json.DB2JsonObjectFunction;
import org.hibernate.dialect.function.json.H2JsonExistsFunction;
import org.hibernate.dialect.function.json.H2JsonQueryFunction;
import org.hibernate.dialect.function.json.H2JsonValueFunction;
import org.hibernate.dialect.function.json.HANAJsonArrayFunction;
import org.hibernate.dialect.function.json.HANAJsonExistsFunction;
@ -89,22 +90,26 @@ import org.hibernate.dialect.function.json.HSQLJsonObjectFunction;
import org.hibernate.dialect.function.json.JsonArrayFunction;
import org.hibernate.dialect.function.json.JsonExistsFunction;
import org.hibernate.dialect.function.json.JsonObjectFunction;
import org.hibernate.dialect.function.json.JsonQueryFunction;
import org.hibernate.dialect.function.json.JsonValueFunction;
import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction;
import org.hibernate.dialect.function.json.MariaDBJsonValueFunction;
import org.hibernate.dialect.function.json.MySQLJsonArrayFunction;
import org.hibernate.dialect.function.json.MySQLJsonExistsFunction;
import org.hibernate.dialect.function.json.MySQLJsonObjectFunction;
import org.hibernate.dialect.function.json.MySQLJsonQueryFunction;
import org.hibernate.dialect.function.json.MySQLJsonValueFunction;
import org.hibernate.dialect.function.json.OracleJsonArrayFunction;
import org.hibernate.dialect.function.json.OracleJsonObjectFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction;
import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction;
import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction;
import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction;
import org.hibernate.dialect.function.json.SQLServerJsonQueryFunction;
import org.hibernate.dialect.function.json.SQLServerJsonValueFunction;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
@ -3414,6 +3419,55 @@ public class CommonFunctionFactory {
functionRegistry.register( "json_value", new H2JsonValueFunction( typeConfiguration ) );
}
/**
* json_query() function
*/
public void jsonQuery() {
functionRegistry.register( "json_query", new JsonQueryFunction( typeConfiguration, true, true ) );
}
/**
* json_query() function
*/
public void jsonQuery_no_passing() {
functionRegistry.register( "json_query", new JsonQueryFunction( typeConfiguration, true, false ) );
}
/**
* Oracle json_query() function
*/
public void jsonQuery_oracle() {
functionRegistry.register( "json_query", new JsonQueryFunction( typeConfiguration, false, false ) );
}
/**
* PostgreSQL json_query() function
*/
public void jsonQuery_postgresql() {
functionRegistry.register( "json_query", new PostgreSQLJsonQueryFunction( typeConfiguration ) );
}
/**
* MySQL json_query() function
*/
public void jsonQuery_mysql() {
functionRegistry.register( "json_query", new MySQLJsonQueryFunction( typeConfiguration ) );
}
/**
* SQL Server json_query() function
*/
public void jsonQuery_sqlserver() {
functionRegistry.register( "json_query", new SQLServerJsonQueryFunction( typeConfiguration ) );
}
/**
* H2 json_query() function
*/
public void jsonQuery_h2() {
functionRegistry.register( "json_query", new H2JsonQueryFunction( typeConfiguration ) );
}
/**
* json_exists() function
*/

View File

@ -0,0 +1,67 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function.json;
import org.hibernate.QueryException;
import org.hibernate.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode;
import org.hibernate.type.spi.TypeConfiguration;
/**
* H2 json_query function.
*/
public class H2JsonQueryFunction extends JsonQueryFunction {
public H2JsonQueryFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, false, true );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonQueryArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
// Json dereference errors by default if the JSON is invalid
if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) {
throw new QueryException( "Can't emulate on error clause on H2" );
}
if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR ) {
throw new QueryException( "Can't emulate error on empty clause on H2" );
}
final String jsonPath;
try {
jsonPath = walker.getLiteralValue( arguments.jsonPath() );
}
catch (Exception ex) {
throw new QueryException( "H2 json_query only support literal json paths, but got " + arguments.jsonPath() );
}
if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) {
sqlAppender.appendSql( "'['||" );
}
sqlAppender.appendSql( "stringdecode(btrim(nullif(" );
sqlAppender.appendSql( "cast(" );
H2JsonValueFunction.renderJsonPath(
sqlAppender,
arguments.jsonDocument(),
arguments.isJsonType(),
walker,
jsonPath,
arguments.passingClause()
);
sqlAppender.appendSql( " as varchar)" );
sqlAppender.appendSql( ",'null'),'\"'))");
if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) {
sqlAppender.appendSql( "||']'" );
}
}
}

View File

@ -51,9 +51,7 @@ public class JsonExistsFunction extends AbstractSqmSelfRenderingFunctionDescript
super(
"json_exists",
FunctionKind.NORMAL,
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY )
),
new ArgumentTypesValidator( null, IMPLICIT_JSON, STRING ),
StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.standardBasicTypeForJavaType( Boolean.class ) ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING )
);

View File

@ -0,0 +1,222 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function.json;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.function.FunctionKind;
import org.hibernate.query.sqm.function.SelfRenderingSqmFunction;
import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode;
import org.hibernate.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;
import org.checkerframework.checker.nullness.qual.Nullable;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.IMPLICIT_JSON;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.JSON;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING;
/**
* Standard json_query function.
*/
public class JsonQueryFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
protected final boolean supportsJsonPathExpression;
protected final boolean supportsJsonPathPassingClause;
public JsonQueryFunction(
TypeConfiguration typeConfiguration,
boolean supportsJsonPathExpression,
boolean supportsJsonPathPassingClause) {
super(
"json_query",
FunctionKind.NORMAL,
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY )
),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON )
),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING )
);
this.supportsJsonPathExpression = supportsJsonPathExpression;
this.supportsJsonPathPassingClause = supportsJsonPathPassingClause;
}
@Override
protected <T> SelfRenderingSqmFunction<T> generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine) {
//noinspection unchecked
return (SelfRenderingSqmFunction<T>) new SqmJsonQueryExpression(
this,
this,
arguments,
(ReturnableType<String>) impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
queryEngine.getCriteriaBuilder(),
getName()
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
render( sqlAppender, JsonQueryArguments.extract( sqlAstArguments ), returnType, walker );
}
protected void render(
SqlAppender sqlAppender,
JsonQueryArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "json_query(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ',' );
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( supportsJsonPathPassingClause || passingClause == null ) {
if ( supportsJsonPathExpression ) {
arguments.jsonPath().accept( walker );
}
else {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
sqlAppender,
walker.getLiteralValue( arguments.jsonPath() )
);
}
if ( passingClause != null ) {
sqlAppender.appendSql( " passing " );
final Map<String, Expression> passingExpressions = passingClause.getPassingExpressions();
final Iterator<Map.Entry<String, Expression>> iterator = passingExpressions.entrySet().iterator();
Map.Entry<String, Expression> entry = iterator.next();
entry.getValue().accept( walker );
sqlAppender.appendSql( " as " );
sqlAppender.appendDoubleQuoteEscapedString( entry.getKey() );
while ( iterator.hasNext() ) {
entry = iterator.next();
sqlAppender.appendSql( ',' );
entry.getValue().accept( walker );
sqlAppender.appendSql( " as " );
sqlAppender.appendDoubleQuoteEscapedString( entry.getKey() );
}
}
}
else {
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
"",
arguments.jsonPath(),
passingClause,
walker
);
}
if ( arguments.wrapMode() != null ) {
switch ( arguments.wrapMode() ) {
case WITH_WRAPPER -> sqlAppender.appendSql( " with wrapper" );
case WITHOUT_WRAPPER -> sqlAppender.appendSql( " without wrapper" );
case WITH_CONDITIONAL_WRAPPER -> sqlAppender.appendSql( " with conditional wrapper" );
}
}
if ( arguments.errorBehavior() != null ) {
switch ( arguments.errorBehavior() ) {
case ERROR -> sqlAppender.appendSql( " error on error" );
case NULL -> sqlAppender.appendSql( " null on error" );
case EMPTY_OBJECT -> sqlAppender.appendSql( " empty object on error" );
case EMPTY_ARRAY -> sqlAppender.appendSql( " empty array on error" );
}
}
if ( arguments.emptyBehavior() != null ) {
switch ( arguments.emptyBehavior() ) {
case ERROR -> sqlAppender.appendSql( " error on empty" );
case NULL -> sqlAppender.appendSql( " null on empty" );
case EMPTY_OBJECT -> sqlAppender.appendSql( " empty object on empty" );
case EMPTY_ARRAY -> sqlAppender.appendSql( " empty array on empty" );
}
}
sqlAppender.appendSql( ')' );
}
protected record JsonQueryArguments(
Expression jsonDocument,
Expression jsonPath,
boolean isJsonType,
@Nullable JsonPathPassingClause passingClause,
@Nullable JsonQueryWrapMode wrapMode,
@Nullable JsonQueryErrorBehavior errorBehavior,
@Nullable JsonQueryEmptyBehavior emptyBehavior) {
public static JsonQueryArguments extract(List<? extends SqlAstNode> sqlAstArguments) {
int nextIndex = 2;
JsonPathPassingClause passingClause = null;
JsonQueryWrapMode wrapMode = null;
JsonQueryErrorBehavior errorBehavior = null;
JsonQueryEmptyBehavior emptyBehavior = null;
if ( nextIndex < sqlAstArguments.size() ) {
final SqlAstNode node = sqlAstArguments.get( nextIndex );
if ( node instanceof JsonPathPassingClause ) {
passingClause = (JsonPathPassingClause) node;
nextIndex++;
}
}
if ( nextIndex < sqlAstArguments.size() ) {
final SqlAstNode node = sqlAstArguments.get( nextIndex );
if ( node instanceof JsonQueryWrapMode ) {
wrapMode = (JsonQueryWrapMode) node;
nextIndex++;
}
}
if ( nextIndex < sqlAstArguments.size() ) {
final SqlAstNode node = sqlAstArguments.get( nextIndex );
if ( node instanceof JsonQueryErrorBehavior ) {
errorBehavior = (JsonQueryErrorBehavior) node;
nextIndex++;
}
}
if ( nextIndex < sqlAstArguments.size() ) {
final SqlAstNode node = sqlAstArguments.get( nextIndex );
if ( node instanceof JsonQueryEmptyBehavior ) {
emptyBehavior = (JsonQueryEmptyBehavior) node;
}
}
final Expression jsonDocument = (Expression) sqlAstArguments.get( 0 );
return new JsonQueryArguments(
jsonDocument,
(Expression) sqlAstArguments.get( 1 ),
jsonDocument.getExpressionType() != null
&& jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(),
passingClause,
wrapMode,
errorBehavior,
emptyBehavior
);
}
}
}

View File

@ -53,7 +53,7 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescripto
"json_value",
FunctionKind.NORMAL,
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 5 ), IMPLICIT_JSON, STRING, ANY, ANY, ANY )
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY )
),
new CastTargetReturnTypeResolver( typeConfiguration ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING )

View File

@ -0,0 +1,57 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function.json;
import org.hibernate.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
/**
* MySQL json_query function.
*/
public class MySQLJsonQueryFunction extends JsonQueryFunction {
public MySQLJsonQueryFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, false );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonQueryArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
// json_extract errors by default
if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR
|| arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR
// Can't emulate DEFAULT ON EMPTY since we can't differentiate between a NULL value and EMPTY
|| arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) {
super.render( sqlAppender, arguments, returnType, walker );
}
else {
sqlAppender.appendSql( "nullif(json_extract(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( "," );
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( passingClause == null ) {
arguments.jsonPath().accept( walker );
}
else {
JsonPathHelper.appendJsonPathConcatPassingClause(
sqlAppender,
arguments.jsonPath(),
passingClause, walker
);
}
sqlAppender.appendSql( "),cast('null' as json))" );
}
}
}

View File

@ -0,0 +1,81 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function.json;
import java.util.Map;
import org.hibernate.QueryException;
import org.hibernate.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JdbcParameter;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior;
import org.hibernate.sql.ast.tree.expression.Literal;
import org.hibernate.type.spi.TypeConfiguration;
/**
* PostgreSQL json_query function.
*/
public class PostgreSQLJsonQueryFunction extends JsonQueryFunction {
public PostgreSQLJsonQueryFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, true );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonQueryArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
// jsonb_path_query_first errors by default
if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) {
throw new QueryException( "Can't emulate on error clause on PostgreSQL" );
}
if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) {
throw new QueryException( "Can't emulate on empty clause on PostgreSQL" );
}
sqlAppender.appendSql( "jsonb_path_query_array(" );
final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter;
if ( needsCast ) {
sqlAppender.appendSql( "cast(" );
}
arguments.jsonDocument().accept( walker );
if ( needsCast ) {
sqlAppender.appendSql( " as jsonb)" );
}
sqlAppender.appendSql( ',' );
final SqlAstNode jsonPath = arguments.jsonPath();
if ( jsonPath instanceof Literal ) {
jsonPath.accept( walker );
}
else {
sqlAppender.appendSql( "cast(" );
jsonPath.accept( walker );
sqlAppender.appendSql( " as jsonpath)" );
}
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( passingClause != null ) {
sqlAppender.append( ",jsonb_build_object" );
char separator = '(';
for ( Map.Entry<String, Expression> entry : passingClause.getPassingExpressions().entrySet() ) {
sqlAppender.append( separator );
sqlAppender.appendSingleQuoteEscapedString( entry.getKey() );
sqlAppender.append( ',' );
entry.getValue().accept( walker );
separator = ',';
}
sqlAppender.append( ')' );
}
// Unquote the value
sqlAppender.appendSql( ")#>>'{}'" );
}
}

View File

@ -0,0 +1,164 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function.json;
import java.util.List;
import org.hibernate.QueryException;
import org.hibernate.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode;
import org.hibernate.type.spi.TypeConfiguration;
/**
* SQL Server json_query function.
*/
public class SQLServerJsonQueryFunction extends JsonQueryFunction {
public SQLServerJsonQueryFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, false );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonQueryArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
// openjson errors by default
if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) {
throw new QueryException( "Can't emulate on error clause on SQL server" );
}
final List<JsonPathHelper.JsonPathElement> jsonPathElements = JsonPathHelper.parseJsonPathElements(
walker.getLiteralValue( arguments.jsonPath() )
);
if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_ARRAY
|| arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_OBJECT ) {
sqlAppender.appendSql( "coalesce(" );
}
render( sqlAppender, arguments, jsonPathElements, jsonPathElements.size() - 1, walker );
if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_ARRAY ) {
sqlAppender.appendSql( ",'[]')" );
}
else if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_OBJECT ) {
sqlAppender.appendSql( ",'{}')" );
}
}
private void render(
SqlAppender sqlAppender,
JsonQueryArguments arguments,
List<JsonPathHelper.JsonPathElement> jsonPathElements,
int index,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "(select " );
final boolean aggregate = index == jsonPathElements.size() - 1 && (
arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER
|| arguments.wrapMode() == JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER
);
if ( aggregate ) {
if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) {
sqlAppender.appendSql( "'['+" );
}
else {
sqlAppender.appendSql( "case when count(*)>1 then '[' else '' end+" );
}
sqlAppender.appendSql( "string_agg(t.v,',')" );
if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) {
sqlAppender.appendSql( "+']'" );
}
else {
sqlAppender.appendSql( "+case when count(*)>1 then ']' else '' end" );
}
// openjson unquotes values, so we have to quote them again
sqlAppender.appendSql( " from (select " );
// type 0 is a null literal
sqlAppender.appendSql( "case t.type when 0 then 'null' when 1 then ");
// type 1 is a string literal. to quote it, we use for json path and trim the string down to just the value
sqlAppender.appendSql(
"(select substring(a.v,6,len(a.v)-6) from (select t.value a for json path,without_array_wrapper) a(v))" );
sqlAppender.appendSql( " else t.value end v");
}
else {
sqlAppender.appendSql( "t.value" );
}
sqlAppender.appendSql( " from openjson(" );
if ( index == 0 ) {
arguments.jsonDocument().accept( walker );
}
else {
render( sqlAppender, arguments, jsonPathElements, index - 1, walker );
}
sqlAppender.appendSql( ')' );
if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR ) {
sqlAppender.appendSql( " with (value nvarchar(max) " );
final JsonPathHelper.JsonPathElement jsonPathElement = jsonPathElements.get( index );
if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) {
sqlAppender.appendSql( "'strict $." );
final String name = attribute.attribute();
for ( int i = 0; i < name.length(); i++ ) {
final char c = name.charAt( i );
if ( c == '\'' ) {
sqlAppender.append( '\'' );
}
sqlAppender.append( c );
}
sqlAppender.append( '\'' );
}
else if ( jsonPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) {
sqlAppender.appendSql( "'strict $[" );
sqlAppender.appendSql( indexAccess.index() );
sqlAppender.appendSql( "]'" );
}
else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess indexAccess ) {
final JsonPathPassingClause passingClause = arguments.passingClause();
assert passingClause != null;
final Object literalValue = walker.getLiteralValue(
passingClause.getPassingExpressions().get( indexAccess.parameterName() )
);
sqlAppender.appendSql( "'strict $[" );
sqlAppender.appendSql( literalValue.toString() );
sqlAppender.appendSql( "]'" );
}
else {
throw new UnsupportedOperationException( "Unsupported JSON path expression: " + jsonPathElement );
}
sqlAppender.appendSql( " as json) t" );
}
else {
sqlAppender.appendSql( " t where " );
final JsonPathHelper.JsonPathElement jsonPathElement = jsonPathElements.get( index );
if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) {
sqlAppender.appendSql( "t.[key]=" );
sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() );
}
else if ( jsonPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) {
sqlAppender.appendSql( "t.[key]=" );
sqlAppender.appendSql( indexAccess.index() );
}
else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess indexAccess ) {
final JsonPathPassingClause passingClause = arguments.passingClause();
assert passingClause != null;
sqlAppender.appendSql( "t.[key]=" );
passingClause.getPassingExpressions().get( indexAccess.parameterName() ).accept( walker );
}
else {
throw new UnsupportedOperationException( "Unsupported JSON path expression: " + jsonPathElement );
}
}
if ( aggregate ) {
sqlAppender.appendSql( ") t" );
}
sqlAppender.appendSql( ")" );
}
}

View File

@ -41,7 +41,7 @@ public class SQLServerJsonValueFunction extends JsonValueFunction {
arguments.returningType().accept( walker );
}
else {
sqlAppender.appendSql( "varchar(max)" );
sqlAppender.appendSql( "nvarchar(max)" );
}
sqlAppender.appendSql( ' ' );
final JsonPathPassingClause passingClause = arguments.passingClause();

View File

@ -3713,6 +3713,20 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
@Incubating
<T> JpaJsonValueExpression<T> jsonValue(Expression<?> jsonDocument, Expression<String> jsonPath, Class<T> returningType);
/**
* @see #jsonQuery(Expression, Expression)
* @since 7.0
*/
@Incubating
JpaJsonQueryExpression jsonQuery(Expression<?> jsonDocument, String jsonPath);
/**
* Queries values by JSON path from a JSON document.
* @since 7.0
*/
@Incubating
JpaJsonQueryExpression jsonQuery(Expression<?> jsonDocument, Expression<String> jsonPath);
/**
* Checks if a JSON document contains a node for the given JSON path.
*

View File

@ -56,7 +56,7 @@ public interface JpaJsonExistsExpression extends JpaExpression<Boolean> {
JpaJsonExistsExpression passing(String parameterName, Expression<?> expression);
/**
* The behavior of the json value expression when a JSON processing error occurs.
* The behavior of the json exists expression when a JSON processing error occurs.
*/
enum ErrorBehavior {
/**

View File

@ -0,0 +1,204 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.query.criteria;
import org.hibernate.Incubating;
import jakarta.persistence.criteria.Expression;
/**
* A special expression for the {@code json_query} function.
* @since 7.0
*/
@Incubating
public interface JpaJsonQueryExpression extends JpaExpression<String> {
/**
* Get the {@link WrapMode} of this json query expression.
*
* @return the wrap mode
*/
WrapMode getWrapMode();
/**
* Get the {@link ErrorBehavior} of this json query expression.
*
* @return the error behavior
*/
ErrorBehavior getErrorBehavior();
/**
* Get the {@link EmptyBehavior} of this json query expression.
*
* @return the empty behavior
*/
EmptyBehavior getEmptyBehavior();
/**
* Sets the {@link WrapMode#WITHOUT_WRAPPER} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression withoutWrapper();
/**
* Sets the {@link WrapMode#WITH_WRAPPER} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression withWrapper();
/**
* Sets the {@link WrapMode#WITH_CONDITIONAL_WRAPPER} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression withConditionalWrapper();
/**
* Sets the {@link WrapMode#UNSPECIFIED} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression unspecifiedWrapper();
/**
* Sets the {@link ErrorBehavior#UNSPECIFIED} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression unspecifiedOnError();
/**
* Sets the {@link ErrorBehavior#ERROR} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression errorOnError();
/**
* Sets the {@link ErrorBehavior#NULL} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression nullOnError();
/**
* Sets the {@link ErrorBehavior#EMPTY_ARRAY} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression emptyArrayOnError();
/**
* Sets the {@link ErrorBehavior#EMPTY_OBJECT} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression emptyObjectOnError();
/**
* Sets the {@link EmptyBehavior#UNSPECIFIED} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression unspecifiedOnEmpty();
/**
* Sets the {@link EmptyBehavior#ERROR} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression errorOnEmpty();
/**
* Sets the {@link EmptyBehavior#NULL} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression nullOnEmpty();
/**
* Sets the {@link EmptyBehavior#EMPTY_ARRAY} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression emptyArrayOnEmpty();
/**
* Sets the {@link EmptyBehavior#EMPTY_OBJECT} for this json query expression.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression emptyObjectOnEmpty();
/**
* Passes the given {@link Expression} as value for the parameter with the given name in the JSON path.
*
* @return {@code this} for method chaining
*/
JpaJsonQueryExpression passing(String parameterName, Expression<?> expression);
/**
* The kind of wrapping to apply to the results of the query.
*/
enum WrapMode {
/**
* Omit the array wrapper in the result.
*/
WITHOUT_WRAPPER,
/**
* Force the array wrapper in the result.
*/
WITH_WRAPPER,
/**
* Only use an array wrapper in the result if there is more than one result.
*/
WITH_CONDITIONAL_WRAPPER,
/**
* Unspecified behavior i.e. the default database behavior.
*/
UNSPECIFIED
}
/**
* The behavior of the json query expression when a JSON processing error occurs.
*/
enum ErrorBehavior {
/**
* SQL/JDBC error should be raised.
*/
ERROR,
/**
* {@code null} should be returned.
*/
NULL,
/**
* An empty array should be returned.
*/
EMPTY_ARRAY,
/**
* An empty object should be returned.
*/
EMPTY_OBJECT,
/**
* Unspecified behavior i.e. the default database behavior.
*/
UNSPECIFIED
}
/**
* The behavior of the json query expression when a JSON path does not resolve for a JSON document.
*/
enum EmptyBehavior {
/**
* SQL/JDBC error should be raised.
*/
ERROR,
/**
* {@code null} should be returned.
*/
NULL,
/**
* An empty array should be returned.
*/
EMPTY_ARRAY,
/**
* An empty object should be returned.
*/
EMPTY_OBJECT,
/**
* Unspecified behavior i.e. the default database behavior.
*/
UNSPECIFIED
}
}

View File

@ -39,6 +39,7 @@ import org.hibernate.query.criteria.JpaFunction;
import org.hibernate.query.criteria.JpaInPredicate;
import org.hibernate.query.criteria.JpaJoin;
import org.hibernate.query.criteria.JpaJsonExistsExpression;
import org.hibernate.query.criteria.JpaJsonQueryExpression;
import org.hibernate.query.criteria.JpaJsonValueExpression;
import org.hibernate.query.criteria.JpaListJoin;
import org.hibernate.query.criteria.JpaMapJoin;
@ -3376,6 +3377,18 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
return criteriaBuilder.jsonValue( jsonDocument, jsonPath, returningType );
}
@Override
@Incubating
public JpaJsonQueryExpression jsonQuery(Expression<?> jsonDocument, String jsonPath) {
return criteriaBuilder.jsonQuery( jsonDocument, jsonPath );
}
@Override
@Incubating
public JpaJsonQueryExpression jsonQuery(Expression<?> jsonDocument, Expression<String> jsonPath) {
return criteriaBuilder.jsonQuery( jsonDocument, jsonPath );
}
@Override
@Incubating
public JpaJsonExistsExpression jsonExists(Expression<?> jsonDocument, String jsonPath) {

View File

@ -146,6 +146,7 @@ import org.hibernate.query.sqm.tree.expression.SqmFunction;
import org.hibernate.query.sqm.tree.expression.SqmHqlNumericLiteral;
import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression;
import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior;
import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType;
@ -2745,6 +2746,79 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
return jsonValue;
}
@Override
public SqmExpression<?> visitJsonQueryFunction(HqlParser.JsonQueryFunctionContext ctx) {
final SqmExpression<?> jsonDocument = (SqmExpression<?>) ctx.expression( 0 ).accept( this );
final SqmExpression<?> jsonPath = (SqmExpression<?>) ctx.expression( 1 ).accept( this );
final SqmJsonQueryExpression jsonQuery = (SqmJsonQueryExpression) getFunctionDescriptor( "json_query" ).<String>generateSqmExpression(
asList( jsonDocument, jsonPath ),
null,
creationContext.getQueryEngine()
);
final HqlParser.JsonQueryWrapperClauseContext wrapperClause = ctx.jsonQueryWrapperClause();
if ( wrapperClause != null ) {
final TerminalNode firstToken = (TerminalNode) wrapperClause.getChild( 0 );
if ( firstToken.getSymbol().getType() == HqlParser.WITH ) {
final TerminalNode secondToken = (TerminalNode) wrapperClause.getChild( 1 );
if ( wrapperClause.getChildCount() > 2 && secondToken.getSymbol().getType() == HqlParser.CONDITIONAL ) {
jsonQuery.withConditionalWrapper();
}
else {
jsonQuery.withWrapper();
}
}
else {
jsonQuery.withoutWrapper();
}
}
for ( HqlParser.JsonQueryOnErrorOrEmptyClauseContext subCtx : ctx.jsonQueryOnErrorOrEmptyClause() ) {
final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 );
final TerminalNode lastToken = (TerminalNode) subCtx.getChild( subCtx.getChildCount() - 1 );
if ( lastToken.getSymbol().getType() == HqlParser.ERROR ) {
switch ( firstToken.getSymbol().getType() ) {
case HqlParser.NULL -> jsonQuery.nullOnError();
case HqlParser.ERROR -> jsonQuery.errorOnError();
case HqlParser.EMPTY -> {
final TerminalNode secondToken = (TerminalNode) subCtx.getChild( 1 );
if ( secondToken.getSymbol().getType() == HqlParser.OBJECT ) {
jsonQuery.emptyObjectOnError();
}
else {
jsonQuery.emptyArrayOnError();
}
}
}
}
else {
switch ( firstToken.getSymbol().getType() ) {
case HqlParser.NULL -> jsonQuery.nullOnEmpty();
case HqlParser.ERROR -> jsonQuery.errorOnEmpty();
case HqlParser.EMPTY -> {
final TerminalNode secondToken = (TerminalNode) subCtx.getChild( 1 );
if ( secondToken.getSymbol().getType() == HqlParser.OBJECT ) {
jsonQuery.emptyObjectOnEmpty();
}
else {
jsonQuery.emptyArrayOnEmpty();
}
}
}
}
}
final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause();
if ( passingClause != null ) {
final List<HqlParser.ExpressionOrPredicateContext> expressionContexts = passingClause.expressionOrPredicate();
final List<HqlParser.IdentifierContext> identifierContexts = passingClause.identifier();
for ( int i = 0; i < expressionContexts.size(); i++ ) {
jsonQuery.passing(
visitIdentifier( identifierContexts.get( i ) ),
(SqmExpression<?>) expressionContexts.get( i ).accept( this )
);
}
}
return jsonQuery;
}
@Override
public SqmExpression<?> visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext ctx) {
final SqmExpression<?> jsonDocument = (SqmExpression<?>) ctx.expression( 0 ).accept( this );

View File

@ -26,7 +26,6 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder;
import org.hibernate.query.criteria.JpaCoalesce;
import org.hibernate.query.criteria.JpaCompoundSelection;
import org.hibernate.query.criteria.JpaExpression;
import org.hibernate.query.criteria.JpaJsonExistsExpression;
import org.hibernate.query.criteria.JpaOrder;
import org.hibernate.query.criteria.JpaParameterExpression;
import org.hibernate.query.criteria.JpaPredicate;
@ -46,6 +45,7 @@ import org.hibernate.query.sqm.tree.domain.SqmSingularJoin;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmFunction;
import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression;
import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
@ -631,6 +631,12 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
@Override
SqmJsonValueExpression<String> jsonValue(Expression<?> jsonDocument, String jsonPath);
@Override
SqmJsonQueryExpression jsonQuery(Expression<?> jsonDocument, Expression<String> jsonPath);
@Override
SqmJsonQueryExpression jsonQuery(Expression<?> jsonDocument, String jsonPath);
@Override
SqmJsonExistsExpression jsonExists(Expression<?> jsonDocument, Expression<String> jsonPath);

View File

@ -122,6 +122,7 @@ import org.hibernate.query.sqm.tree.expression.SqmFormat;
import org.hibernate.query.sqm.tree.expression.SqmFunction;
import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression;
import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior;
import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.query.sqm.tree.expression.SqmLiteralNull;
@ -5334,6 +5335,20 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
}
}
@Override
public SqmJsonQueryExpression jsonQuery(Expression<?> jsonDocument, String jsonPath) {
return jsonQuery( jsonDocument, value( jsonPath ) );
}
@Override
public SqmJsonQueryExpression jsonQuery(Expression<?> jsonDocument, Expression<String> jsonPath) {
return (SqmJsonQueryExpression) getFunctionDescriptor( "json_query" ).<String>generateSqmExpression(
asList( (SqmTypedNode<?>) jsonDocument, (SqmTypedNode<?>) jsonPath ),
null,
queryEngine
);
}
@Override
public SqmJsonExistsExpression jsonExists(Expression<?> jsonDocument, String jsonPath) {
return jsonExists( jsonDocument, value( jsonPath ) );

View File

@ -36,7 +36,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/
@Incubating
public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression<Boolean> implements JpaJsonExistsExpression {
private @Nullable ErrorBehavior errorBehavior;
private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED;
public SqmJsonExistsExpression(
SqmFunctionDescriptor descriptor,
@ -69,7 +69,7 @@ public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression<Boole
NodeBuilder nodeBuilder,
String name,
@Nullable Map<String, SqmExpression<?>> passingExpressions,
@Nullable ErrorBehavior errorBehavior) {
ErrorBehavior errorBehavior) {
super(
descriptor,
renderer,
@ -159,18 +159,10 @@ public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression<Boole
if ( jsonPathPassingClause != null ) {
arguments.add( jsonPathPassingClause );
}
if ( errorBehavior != null ) {
switch ( errorBehavior ) {
case ERROR:
arguments.add( JsonExistsErrorBehavior.ERROR );
break;
case TRUE:
arguments.add( JsonExistsErrorBehavior.TRUE );
break;
case FALSE:
arguments.add( JsonExistsErrorBehavior.FALSE );
break;
}
switch ( errorBehavior ) {
case ERROR -> arguments.add( JsonExistsErrorBehavior.ERROR );
case TRUE -> arguments.add( JsonExistsErrorBehavior.TRUE );
case FALSE -> arguments.add( JsonExistsErrorBehavior.FALSE );
}
return new SelfRenderingFunctionSqlAstExpression(
getFunctionName(),
@ -189,18 +181,10 @@ public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression<Boole
getArguments().get( 1 ).appendHqlString( sb );
appendPassingExpressionHqlString( sb );
if ( errorBehavior != null ) {
switch ( errorBehavior ) {
case ERROR:
sb.append( " error on error" );
break;
case TRUE:
sb.append( " true on error" );
break;
case FALSE:
sb.append( " false on error" );
break;
}
switch ( errorBehavior ) {
case ERROR -> sb.append( " error on error" );
case TRUE -> sb.append( " true on error" );
case FALSE -> sb.append( " false on error" );
}
sb.append( ')' );
}

View File

@ -0,0 +1,295 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.query.sqm.tree.expression;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.hibernate.Incubating;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.criteria.JpaJsonQueryExpression;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.function.FunctionRenderer;
import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmCopyContext;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Special expression for the json_query function that also captures special syntax elements like error and empty behavior.
*
* @since 7.0
*/
@Incubating
public class SqmJsonQueryExpression extends AbstractSqmJsonPathExpression<String> implements JpaJsonQueryExpression {
private WrapMode wrapMode = WrapMode.UNSPECIFIED;
private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED;
private EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED;
public SqmJsonQueryExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<String> impliedResultType,
@Nullable ArgumentsValidator argumentsValidator,
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name) {
super(
descriptor,
renderer,
arguments,
impliedResultType,
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name
);
}
private SqmJsonQueryExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<String> impliedResultType,
@Nullable ArgumentsValidator argumentsValidator,
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name,
@Nullable Map<String, SqmExpression<?>> passingExpressions,
WrapMode wrapMode,
ErrorBehavior errorBehavior,
EmptyBehavior emptyBehavior) {
super(
descriptor,
renderer,
arguments,
impliedResultType,
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name,
passingExpressions
);
this.wrapMode = wrapMode;
this.errorBehavior = errorBehavior;
this.emptyBehavior = emptyBehavior;
}
public SqmJsonQueryExpression copy(SqmCopyContext context) {
final SqmJsonQueryExpression existing = context.getCopy( this );
if ( existing != null ) {
return existing;
}
final List<SqmTypedNode<?>> arguments = new ArrayList<>( getArguments().size() );
for ( SqmTypedNode<?> argument : getArguments() ) {
arguments.add( argument.copy( context ) );
}
return context.registerCopy(
this,
new SqmJsonQueryExpression(
getFunctionDescriptor(),
getFunctionRenderer(),
arguments,
getImpliedResultType(),
getArgumentsValidator(),
getReturnTypeResolver(),
nodeBuilder(),
getFunctionName(),
copyPassingExpressions( context ),
wrapMode,
errorBehavior,
emptyBehavior
)
);
}
@Override
public WrapMode getWrapMode() {
return wrapMode;
}
@Override
public ErrorBehavior getErrorBehavior() {
return errorBehavior;
}
@Override
public EmptyBehavior getEmptyBehavior() {
return emptyBehavior;
}
@Override
public SqmJsonQueryExpression withoutWrapper() {
this.wrapMode = WrapMode.WITHOUT_WRAPPER;
return this;
}
@Override
public SqmJsonQueryExpression withWrapper() {
this.wrapMode = WrapMode.WITH_WRAPPER;
return this;
}
@Override
public SqmJsonQueryExpression withConditionalWrapper() {
this.wrapMode = WrapMode.WITH_CONDITIONAL_WRAPPER;
return this;
}
@Override
public SqmJsonQueryExpression unspecifiedWrapper() {
this.wrapMode = WrapMode.UNSPECIFIED;
return this;
}
@Override
public SqmJsonQueryExpression unspecifiedOnError() {
this.errorBehavior = ErrorBehavior.UNSPECIFIED;
return this;
}
@Override
public SqmJsonQueryExpression errorOnError() {
this.errorBehavior = ErrorBehavior.ERROR;
return this;
}
@Override
public SqmJsonQueryExpression nullOnError() {
this.errorBehavior = ErrorBehavior.NULL;
return this;
}
@Override
public SqmJsonQueryExpression emptyArrayOnError() {
this.errorBehavior = ErrorBehavior.EMPTY_ARRAY;
return this;
}
@Override
public SqmJsonQueryExpression emptyObjectOnError() {
this.errorBehavior = ErrorBehavior.EMPTY_OBJECT;
return this;
}
@Override
public SqmJsonQueryExpression unspecifiedOnEmpty() {
this.errorBehavior = ErrorBehavior.UNSPECIFIED;
return this;
}
@Override
public SqmJsonQueryExpression errorOnEmpty() {
this.emptyBehavior = EmptyBehavior.ERROR;
return this;
}
@Override
public SqmJsonQueryExpression nullOnEmpty() {
this.emptyBehavior = EmptyBehavior.NULL;
return this;
}
@Override
public SqmJsonQueryExpression emptyArrayOnEmpty() {
this.emptyBehavior = EmptyBehavior.EMPTY_ARRAY;
return this;
}
@Override
public SqmJsonQueryExpression emptyObjectOnEmpty() {
this.emptyBehavior = EmptyBehavior.EMPTY_OBJECT;
return this;
}
@Override
public SqmJsonQueryExpression passing(
String parameterName,
jakarta.persistence.criteria.Expression<?> expression) {
addPassingExpression( parameterName, (SqmExpression<?>) expression );
return this;
}
@Override
public Expression convertToSqlAst(SqmToSqlAstConverter walker) {
final @Nullable ReturnableType<?> resultType = resolveResultType( walker );
final List<SqlAstNode> arguments = resolveSqlAstArguments( getArguments(), walker );
final ArgumentsValidator validator = getArgumentsValidator();
if ( validator != null ) {
validator.validateSqlTypes( arguments, getFunctionName() );
}
final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker );
if ( jsonPathPassingClause != null ) {
arguments.add( jsonPathPassingClause );
}
switch ( wrapMode ) {
case WITH_WRAPPER -> arguments.add( JsonQueryWrapMode.WITH_WRAPPER );
case WITHOUT_WRAPPER -> arguments.add( JsonQueryWrapMode.WITHOUT_WRAPPER );
case WITH_CONDITIONAL_WRAPPER -> arguments.add( JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER );
}
switch ( errorBehavior ) {
case NULL -> arguments.add( JsonQueryErrorBehavior.NULL );
case ERROR -> arguments.add( JsonQueryErrorBehavior.ERROR );
case EMPTY_OBJECT -> arguments.add( JsonQueryErrorBehavior.EMPTY_OBJECT );
case EMPTY_ARRAY -> arguments.add( JsonQueryErrorBehavior.EMPTY_ARRAY );
}
switch ( emptyBehavior ) {
case NULL -> arguments.add( JsonQueryEmptyBehavior.NULL );
case ERROR -> arguments.add( JsonQueryEmptyBehavior.ERROR );
case EMPTY_OBJECT -> arguments.add( JsonQueryEmptyBehavior.EMPTY_OBJECT );
case EMPTY_ARRAY -> arguments.add( JsonQueryEmptyBehavior.EMPTY_ARRAY );
}
return new SelfRenderingFunctionSqlAstExpression(
getFunctionName(),
getFunctionRenderer(),
arguments,
resultType,
resultType == null ? null : getMappingModelExpressible( walker, resultType, arguments )
);
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( "json_query(" );
getArguments().get( 0 ).appendHqlString( sb );
sb.append( ',' );
getArguments().get( 1 ).appendHqlString( sb );
appendPassingExpressionHqlString( sb );
switch ( wrapMode ) {
case WITH_WRAPPER -> sb.append( " with wrapper" );
case WITHOUT_WRAPPER -> sb.append( " without wrapper" );
case WITH_CONDITIONAL_WRAPPER -> sb.append( " with conditional wrapper" );
}
switch ( errorBehavior ) {
case NULL -> sb.append( " null on error" );
case ERROR -> sb.append( " error on error" );
case EMPTY_ARRAY -> sb.append( " empty array on error" );
case EMPTY_OBJECT -> sb.append( " empty object on error" );
}
switch ( emptyBehavior ) {
case NULL -> sb.append( " null on empty" );
case ERROR -> sb.append( " error on empty" );
case EMPTY_ARRAY -> sb.append( " empty array on empty" );
case EMPTY_OBJECT -> sb.append( " empty object on empty" );
}
sb.append( ')' );
}
}

View File

@ -38,9 +38,9 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/
@Incubating
public class SqmJsonValueExpression<T> extends AbstractSqmJsonPathExpression<T> implements JpaJsonValueExpression<T> {
private @Nullable ErrorBehavior errorBehavior;
private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED;
private SqmExpression<T> errorDefaultExpression;
private @Nullable EmptyBehavior emptyBehavior;
private EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED;
private SqmExpression<T> emptyDefaultExpression;
public SqmJsonValueExpression(
@ -74,9 +74,9 @@ public class SqmJsonValueExpression<T> extends AbstractSqmJsonPathExpression<T>
NodeBuilder nodeBuilder,
String name,
@Nullable Map<String, SqmExpression<?>> passingExpressions,
@Nullable ErrorBehavior errorBehavior,
ErrorBehavior errorBehavior,
SqmExpression<T> errorDefaultExpression,
@Nullable EmptyBehavior emptyBehavior,
EmptyBehavior emptyBehavior,
SqmExpression<T> emptyDefaultExpression) {
super(
descriptor,
@ -222,35 +222,19 @@ public class SqmJsonValueExpression<T> extends AbstractSqmJsonPathExpression<T>
if ( jsonPathPassingClause != null ) {
arguments.add( jsonPathPassingClause );
}
if ( errorBehavior != null ) {
switch ( errorBehavior ) {
case NULL:
arguments.add( JsonValueErrorBehavior.NULL );
break;
case ERROR:
arguments.add( JsonValueErrorBehavior.ERROR );
break;
case DEFAULT:
arguments.add( JsonValueErrorBehavior.defaultOnError(
(Expression) errorDefaultExpression.accept( walker )
) );
break;
}
switch ( errorBehavior ) {
case NULL -> arguments.add( JsonValueErrorBehavior.NULL );
case ERROR -> arguments.add( JsonValueErrorBehavior.ERROR );
case DEFAULT -> arguments.add( JsonValueErrorBehavior.defaultOnError(
(Expression) errorDefaultExpression.accept( walker )
) );
}
if ( emptyBehavior != null ) {
switch ( emptyBehavior ) {
case NULL:
arguments.add( JsonValueEmptyBehavior.NULL );
break;
case ERROR:
arguments.add( JsonValueEmptyBehavior.ERROR );
break;
case DEFAULT:
arguments.add( JsonValueEmptyBehavior.defaultOnEmpty(
(Expression) emptyDefaultExpression.accept( walker )
) );
break;
}
switch ( emptyBehavior ) {
case NULL -> arguments.add( JsonValueEmptyBehavior.NULL );
case ERROR -> arguments.add( JsonValueEmptyBehavior.ERROR );
case DEFAULT -> arguments.add( JsonValueEmptyBehavior.defaultOnEmpty(
(Expression) emptyDefaultExpression.accept( walker )
) );
}
return new SelfRenderingFunctionSqlAstExpression(
getFunctionName(),
@ -273,34 +257,22 @@ public class SqmJsonValueExpression<T> extends AbstractSqmJsonPathExpression<T>
sb.append( " returning " );
getArguments().get( 2 ).appendHqlString( sb );
}
if ( errorBehavior != null ) {
switch ( errorBehavior ) {
case NULL:
sb.append( " null on error" );
break;
case ERROR:
sb.append( " error on error" );
break;
case DEFAULT:
sb.append( " default " );
errorDefaultExpression.appendHqlString( sb );
sb.append( " on error" );
break;
switch ( errorBehavior ) {
case NULL -> sb.append( " null on error" );
case ERROR -> sb.append( " error on error" );
case DEFAULT -> {
sb.append( " default " );
errorDefaultExpression.appendHqlString( sb );
sb.append( " on error" );
}
}
if ( emptyBehavior != null ) {
switch ( emptyBehavior ) {
case NULL:
sb.append( " null on empty" );
break;
case ERROR:
sb.append( " error on empty" );
break;
case DEFAULT:
sb.append( " default " );
emptyDefaultExpression.appendHqlString( sb );
sb.append( " on empty" );
break;
switch ( emptyBehavior ) {
case NULL -> sb.append( " null on empty" );
case ERROR -> sb.append( " error on empty" );
case DEFAULT -> {
sb.append( " default " );
emptyDefaultExpression.appendHqlString( sb );
sb.append( " on empty" );
}
}
sb.append( ')' );

View File

@ -0,0 +1,26 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.sql.ast.tree.expression;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode;
/**
* @since 7.0
*/
public enum JsonQueryEmptyBehavior implements SqlAstNode {
ERROR,
NULL,
EMPTY_ARRAY,
EMPTY_OBJECT;
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("JsonQueryEmptyBehavior doesn't support walking");
}
}

View File

@ -0,0 +1,26 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.sql.ast.tree.expression;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode;
/**
* @since 7.0
*/
public enum JsonQueryErrorBehavior implements SqlAstNode {
ERROR,
NULL,
EMPTY_ARRAY,
EMPTY_OBJECT;
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("JsonQueryErrorBehavior doesn't support walking");
}
}

View File

@ -0,0 +1,25 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.sql.ast.tree.expression;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode;
/**
* @since 7.0
*/
public enum JsonQueryWrapMode implements SqlAstNode {
WITH_WRAPPER,
WITHOUT_WRAPPER,
WITH_CONDITIONAL_WRAPPER;
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("JsonQueryWrapMode doesn't support walking");
}
}

View File

@ -25,7 +25,6 @@ import org.hibernate.dialect.H2Dialect;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jpa;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.Test;
@ -41,7 +40,6 @@ import static org.junit.Assert.assertNotNull;
FetchingTest.Project.class
})
@RequiresDialect(H2Dialect.class)
@SkipForDialect(dialectClass = H2Dialect.class, majorVersion = 2, matchSubTypes = true, reason = "See https://github.com/h2database/h2database/issues/3338")
public class FetchingTest {
@Test
@ -168,17 +166,11 @@ public class FetchingTest {
@NaturalId
private String username;
@Column(name = "pswd")
@Column(name = "pswd", columnDefinition = "varbinary")
@ColumnTransformer(
read = "decrypt('AES', '00', pswd )",
read = "trim(trailing u&'\\0000' from cast(decrypt('AES', '00', pswd ) as character varying))",
write = "encrypt('AES', '00', ?)"
)
// For H2 2.0.202+ one must use the varbinary DDL type
// @Column(name = "pswd", columnDefinition = "varbinary")
// @ColumnTransformer(
// read = "trim(trailing u&'\\0000' from cast(decrypt('AES', '00', pswd ) as character varying))",
// write = "encrypt('AES', '00', ?)"
// )
private String password;
private int accessLevel;

View File

@ -0,0 +1,134 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.function.json;
import java.util.HashMap;
import java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.dialect.MariaDBDialect;
import org.hibernate.sql.exec.ExecutionException;
import org.hibernate.testing.orm.junit.DialectFeatureChecks;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Tuple;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/**
* @author Christian Beikov
*/
@DomainModel(annotatedClasses = EntityWithJson.class)
@SessionFactory
@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonQuery.class)
public class JsonQueryTest {
@BeforeEach
public void prepareData(SessionFactoryScope scope) {
scope.inTransaction( em -> {
EntityWithJson entity = new EntityWithJson();
entity.setId( 1L );
entity.getJson().put( "theInt", 1 );
entity.getJson().put( "theFloat", 0.1 );
entity.getJson().put( "theString", "abc" );
entity.getJson().put( "theBoolean", true );
entity.getJson().put( "theNull", null );
entity.getJson().put( "theArray", new String[] { "a", "b", "c" } );
entity.getJson().put( "theObject", new HashMap<>( entity.getJson() ) );
em.persist(entity);
} );
}
@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( em -> {
em.createMutationQuery( "delete from EntityWithJson" ).executeUpdate();
} );
}
@Test
public void testSimple(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-json-query-example[]
List<Tuple> results = em.createQuery( "select json_query(e.json, '$.theString') from EntityWithJson e", Tuple.class )
.getResultList();
//end::hql-json-query-example[]
assertEquals( 1, results.size() );
} );
}
@Test
public void testPassing(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-json-query-passing-example[]
List<Tuple> results = em.createQuery( "select json_query(e.json, '$.theArray[$idx]' passing 1 as idx) from EntityWithJson e", Tuple.class )
.getResultList();
//end::hql-json-query-passing-example[]
assertEquals( 1, results.size() );
} );
}
@Test
public void testWithWrapper(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-json-query-with-wrapper-example[]
List<Tuple> results = em.createQuery( "select json_query(e.json, '$.theInt' with wrapper) from EntityWithJson e", Tuple.class )
.getResultList();
//end::hql-json-query-with-wrapper-example[]
assertEquals( 1, results.size() );
} );
}
@Test
@SkipForDialect(dialectClass = MariaDBDialect.class, reason = "MariaDB reports the error 4038 as warning and simply returns null")
public void testOnError(SessionFactoryScope scope) {
scope.inSession( em -> {
try {
//tag::hql-json-query-on-error-example[]
em.createQuery( "select json_query('invalidJson', '$.theInt' error on error) from EntityWithJson e")
.getResultList();
//end::hql-json-query-on-error-example[]
fail("error clause should fail because of invalid json document");
}
catch ( HibernateException e ) {
if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) {
throw e;
}
}
} );
}
@Test
@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonValueErrorBehavior.class)
public void testOnEmpty(SessionFactoryScope scope) {
scope.inSession( em -> {
try {
//tag::hql-json-query-on-empty-example[]
em.createQuery("select json_query(e.json, '$.nonExisting' error on empty error on error) from EntityWithJson e" )
.getResultList();
//end::hql-json-query-on-empty-example[]
fail("empty clause should fail because of json path doesn't produce results");
}
catch ( HibernateException e ) {
if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) {
throw e;
}
}
} );
}
}

View File

@ -34,7 +34,6 @@ import static org.junit.Assert.assertEquals;
@SkipForDialect(dialectClass = HSQLDialect.class)
@SkipForDialect(dialectClass = DerbyDialect.class)
@SkipForDialect(dialectClass = SybaseASEDialect.class)
@SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 10, matchSubTypes = true)
@SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 11, matchSubTypes = true) // 'generated always' was added in 12
@SkipForDialect(dialectClass = AltibaseDialect.class, reason = "generated always is not supported in Altibase")
public class GeneratedAlwaysTest {

View File

@ -6,12 +6,15 @@
*/
package org.hibernate.orm.test.query.hql;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.dialect.OracleDialect;
import org.hibernate.type.SqlTypes;
import org.hibernate.testing.orm.junit.DialectContext;
@ -21,11 +24,21 @@ import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.RequiresDialectFeature;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.BeforeAll;
import org.hibernate.testing.orm.junit.SkipForDialect;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.BinaryNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Tuple;
@ -44,7 +57,7 @@ public class JsonFunctionTests {
JsonHolder entity;
@BeforeAll
@BeforeEach
public void prepareData(SessionFactoryScope scope) {
scope.inTransaction(
em -> {
@ -58,11 +71,26 @@ public class JsonFunctionTests {
entity.json.put( "theNull", null );
entity.json.put( "theArray", new String[] { "a", "b", "c" } );
entity.json.put( "theObject", new HashMap<>( entity.json ) );
entity.json.put(
"theNestedObjects",
List.of(
Map.of( "id", 1, "name", "val1" ),
Map.of( "id", 2, "name", "val2" ),
Map.of( "id", 3, "name", "val3" )
)
);
em.persist(entity);
}
);
}
@AfterEach
public void cleanupData(SessionFactoryScope scope) {
scope.inTransaction(
em -> em.createMutationQuery( "delete from JsonHolder" ).executeUpdate()
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonValue.class)
public void testJsonValue(SessionFactoryScope scope) {
@ -114,6 +142,48 @@ public class JsonFunctionTests {
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonQuery.class)
public void testJsonQuery(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Tuple tuple = session.createQuery(
"select " +
"json_query(e.json, '$.theArray'), " +
"json_query(e.json, '$.theNestedObjects'), " +
"json_query(e.json, '$.theNestedObjects[$idx]' passing :idx as idx with wrapper) " +
"from JsonHolder e " +
"where e.id = 1L",
Tuple.class
).setParameter( "idx", 0 ).getSingleResult();
assertEquals( parseJson( "[\"a\",\"b\",\"c\"]" ), parseJson( tuple.get( 0, String.class ) ) );
assertEquals(
parseJson(
"[{\"id\":1,\"name\":\"val1\"},{\"id\":2,\"name\":\"val2\"},{\"id\":3,\"name\":\"val3\"}]" ),
parseJson( tuple.get( 1, String.class ) )
);
assertEquals( parseJson( "[{\"id\":1,\"name\":\"val1\"}]" ), parseJson( tuple.get( 2, String.class ) ) );
}
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonQueryNestedPath.class)
public void testJsonQueryNested(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Tuple tuple = session.createQuery(
"select " +
"json_query(e.json, '$.theNestedObjects[*].id' with wrapper) " +
"from JsonHolder e " +
"where e.id = 1L",
Tuple.class
).getSingleResult();
assertEquals( parseJson( "[1,2,3]" ), parseJson( tuple.get( 0, String.class ) ) );
}
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArray.class)
public void testJsonArray(SessionFactoryScope scope) {
@ -224,6 +294,7 @@ public class JsonFunctionTests {
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonExists.class)
@SkipForDialect(dialectClass = OracleDialect.class, majorVersion = 21, matchSubTypes = true, reason = "Oracle bug in versions before 23")
public void testJsonExists(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
@ -266,6 +337,52 @@ public class JsonFunctionTests {
}
}
private static Object parseJson(String json) {
try {
return toJavaNode( MAPPER.readTree( json ) );
}
catch (JsonProcessingException e) {
throw new RuntimeException( e );
}
}
private static Object toJavaNode(JsonNode jsonNode) {
if ( jsonNode instanceof ArrayNode arrayNode ) {
final var list = new ArrayList<>( arrayNode.size() );
for ( JsonNode node : arrayNode ) {
list.add( toJavaNode( node ) );
}
return list;
}
else if ( jsonNode instanceof ObjectNode object ) {
final var map = new HashMap<>( object.size() );
final Iterator<Map.Entry<String, JsonNode>> iter = object.fields();
while ( iter.hasNext() ) {
final Map.Entry<String, JsonNode> entry = iter.next();
map.put( entry.getKey(), toJavaNode( entry.getValue() ) );
}
return map;
}
else if ( jsonNode instanceof NullNode ) {
return null;
}
else if ( jsonNode instanceof NumericNode numericNode ) {
return numericNode.numberValue();
}
else if ( jsonNode instanceof BooleanNode booleanNode ) {
return booleanNode.booleanValue();
}
else if ( jsonNode instanceof TextNode textNode ) {
return textNode.textValue();
}
else if ( jsonNode instanceof BinaryNode binaryNode ) {
return binaryNode.binaryValue();
}
else {
throw new UnsupportedOperationException( "Unsupported node type: " + jsonNode.getClass().getName() );
}
}
@Entity(name = "JsonHolder")
public static class JsonHolder {
@Id

View File

@ -322,6 +322,8 @@ public class CustomRunner extends BlockJUnit4ClassRunner {
effectiveSkipForDialect.microVersion(),
dialect,
effectiveSkipForDialect.matchSubTypes()
? DialectFilterExtension.VersionMatchMode.SAME_OR_OLDER
: DialectFilterExtension.VersionMatchMode.SAME
);
if ( versionsMatch ) {
@ -474,6 +476,8 @@ public class CustomRunner extends BlockJUnit4ClassRunner {
matchingMicroVersion,
dialect,
requiresDialect.matchSubTypes()
? DialectFilterExtension.VersionMatchMode.SAME_OR_NEWER
: DialectFilterExtension.VersionMatchMode.SAME
);
}
else {

View File

@ -732,6 +732,21 @@ abstract public class DialectFeatureChecks {
}
}
public static class SupportsJsonQuery implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "json_query" );
}
}
public static class SupportsJsonQueryNestedPath implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "json_query" )
&& !( dialect instanceof SQLServerDialect )
&& !( dialect instanceof H2Dialect )
&& !( dialect instanceof CockroachDialect );
}
}
public static class SupportsJsonExists implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "json_exists" );

View File

@ -84,6 +84,8 @@ public class DialectFilterExtension implements ExecutionCondition {
matchingMicroVersion,
dialect,
requiresDialect.matchSubTypes()
? VersionMatchMode.SAME_OR_NEWER
: VersionMatchMode.SAME
);
}
else {
@ -136,6 +138,21 @@ public class DialectFilterExtension implements ExecutionCondition {
int matchingMicroVersion,
Dialect dialect,
boolean matchNewerVersions) {
return versionsMatch(
matchingMajorVersion,
matchingMinorVersion,
matchingMicroVersion,
dialect,
matchNewerVersions ? VersionMatchMode.SAME_OR_NEWER : VersionMatchMode.SAME
);
}
public static boolean versionsMatch(
int matchingMajorVersion,
int matchingMinorVersion,
int matchingMicroVersion,
Dialect dialect,
VersionMatchMode matchMode) {
if ( matchingMajorVersion < 0 ) {
return false;
}
@ -148,12 +165,20 @@ public class DialectFilterExtension implements ExecutionCondition {
matchingMicroVersion = 0;
}
if ( matchNewerVersions ) {
if ( matchMode == VersionMatchMode.SAME_OR_NEWER ) {
return dialect.getVersion().isSameOrAfter( matchingMajorVersion, matchingMinorVersion, matchingMicroVersion );
}
else {
return dialect.getVersion().isSame( matchingMajorVersion );
if ( matchMode == VersionMatchMode.SAME_OR_OLDER
&& dialect.getVersion().isBefore( matchingMajorVersion, matchingMinorVersion, matchingMicroVersion ) ) {
return true;
}
return dialect.getVersion().isSame( matchingMajorVersion );
}
public enum VersionMatchMode {
SAME,
SAME_OR_NEWER,
SAME_OR_OLDER
}
private ConditionEvaluationResult evaluateSkipConditions(ExtensionContext context, Dialect dialect, String enabledResult) {
@ -194,6 +219,8 @@ public class DialectFilterExtension implements ExecutionCondition {
effectiveSkipForDialect.microVersion(),
dialect,
effectiveSkipForDialect.matchSubTypes()
? VersionMatchMode.SAME_OR_OLDER
: VersionMatchMode.SAME
);
if ( versionsMatch ) {