HHH-18496 Add json_exists and support the passing clause

This commit is contained in:
Christian Beikov 2024-08-22 15:24:02 +02:00
parent 016b463973
commit 6454aaf055
61 changed files with 1892 additions and 158 deletions

View File

@ -1632,6 +1632,7 @@ The following functions deal with SQL JSON types, which are not supported on eve
| `json_object()` | Constructs a JSON object from pairs of key and value arguments
| `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
|===
@ -1713,7 +1714,8 @@ The first argument is an expression to a JSON document. The second argument is a
WARNING: Some databases might also return non-scalar values. Beware that this behavior is not portable.
NOTE: It is recommended to only us the dot notation for JSON paths, since most databases support only that.
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-value-example]]
====
@ -1723,6 +1725,16 @@ include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-example]
----
====
The `passing` clause allows to reuse the same JSON path but pass different values for evaluation.
[[hql-json-value-passing-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-passing-example]
----
====
The `returning` clause allows to specify the <<hql-function-cast,cast target>> i.e. the type of value to extract.
[[hql-json-value-returning-example]]
@ -1733,17 +1745,6 @@ include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-returning
----
====
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-value-on-error-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-on-error-example]
----
====
The `on error` clause defines the behavior when an error occurs while resolving the value for the JSON path.
Conditions that classify as errors are database dependent, but usual errors which can be handled with this clause are:
@ -1754,6 +1755,17 @@ Conditions that classify as errors are database dependent, but usual errors whic
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-value-on-error-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-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-value-on-empty-example]]
====
[source, java, indent=0]
@ -1767,6 +1779,59 @@ Depending on the database, an error might still be thrown even without that, but
NOTE: The H2 emulation only supports absolute JSON paths using the dot notation.
[[hql-json-exists-function]]
===== `json_exists()`
Checks if a JSON document contains a https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path].
[[hql-json-exists-bnf]]
[source, antlrv4, indent=0]
----
include::{extrasdir}/json_exists_bnf.txt[]
----
The first argument is an expression to a JSON document. The second argument is a JSON path as String expression.
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-exists-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-example]
----
====
The `passing` clause allows to reuse the same JSON path but pass different values for evaluation.
[[hql-json-exists-passing-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-passing-example]
----
====
The `on error` clause defines the behavior when an error occurs while checking for existence 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
The default behavior of `on error` is database specific, but usually, `false` is returned on an error.
It is recommended to specify this clause when the exact error behavior is important.
[[hql-json-exists-on-error-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-on-error-example]
----
====
NOTE: The H2 emulation only supports absolute JSON paths using the dot notation.
[[hql-user-defined-functions]]
==== Native and user-defined functions

View File

@ -0,0 +1,7 @@
"json_exists(" expression, expression passingClause? onErrorClause? ")"
passingClause
: "passing" expression "as" identifier ("," expression "as" identifier)*
onErrorClause
: ( "error" | "true" | "false" ) "on error";

View File

@ -1,4 +1,7 @@
"json_value(" expression, expression ("returning" castTarget)? onErrorClause? onEmptyClause? ")"
"json_value(" expression, expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")"
passingClause
: "passing" expression "as" identifier ("," expression "as" identifier)*
onErrorClause
: ( "error" | "null" | ( "default" expression ) ) "on error";

View File

@ -503,6 +503,7 @@ public class CockroachLegacyDialect extends Dialect {
functionFactory.jsonValue_cockroachdb();
functionFactory.jsonObject_postgresql();
functionFactory.jsonExists_postgresql();
functionFactory.jsonArray_postgresql();
// Postgres uses # instead of ^ for XOR

View File

@ -431,7 +431,8 @@ public class DB2LegacyDialect extends Dialect {
functionFactory.listagg( null );
if ( getDB2Version().isSameOrAfter( 11 ) ) {
functionFactory.jsonValue();
functionFactory.jsonValue_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.jsonExists_h2();
}
}
else {

View File

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

View File

@ -322,7 +322,8 @@ public class OracleLegacyDialect extends Dialect {
functionFactory.arrayToString_oracle();
if ( getVersion().isSameOrAfter( 12 ) ) {
functionFactory.jsonValue_literal_path();
functionFactory.jsonValue_oracle();
functionFactory.jsonExists_oracle();
functionFactory.jsonObject_oracle();
functionFactory.jsonArray_oracle();
}

View File

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

View File

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

View File

@ -223,6 +223,7 @@ INTO : [iI] [nN] [tT] [oO];
IS : [iI] [sS];
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_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE];
KEY : [kK] [eE] [yY];
@ -274,6 +275,7 @@ OVERFLOW : [oO] [vV] [eE] [rR] [fF] [lL] [oO] [wW];
OVERLAY : [oO] [vV] [eE] [rR] [lL] [aA] [yY];
PAD : [pP] [aA] [dD];
PARTITION : [pP] [aA] [rR] [tT] [iI] [tT] [iI] [oO] [nN];
PASSING : [pP] [aA] [sS] [sS] [iI] [nN] [gG];
PERCENT : [pP] [eE] [rR] [cC] [eE] [nN] [tT];
PLACING : [pP] [lL] [aA] [cC] [iI] [nN] [gG];
POSITION : [pP] [oO] [sS] [iI] [tT] [iI] [oO] [nN];

View File

@ -1622,16 +1622,21 @@ rollup
;
jsonFunction
: jsonValueFunction
| jsonArrayFunction
: jsonArrayFunction
| jsonExistsFunction
| jsonObjectFunction
| jsonValueFunction
;
/**
* The 'json_value()' function
*/
jsonValueFunction
: JSON_VALUE LEFT_PAREN expression COMMA expression jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? RIGHT_PAREN
: JSON_VALUE LEFT_PAREN expression COMMA expression jsonPassingClause? jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? RIGHT_PAREN
;
jsonPassingClause
: PASSING expressionOrPredicate AS identifier (COMMA expressionOrPredicate AS identifier)*
;
jsonValueReturningClause
@ -1641,6 +1646,16 @@ jsonValueReturningClause
jsonValueOnErrorOrEmptyClause
: ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY);
/**
* The 'json_exists()' function
*/
jsonExistsFunction
: JSON_EXISTS LEFT_PAREN expression COMMA expression jsonPassingClause? jsonExistsOnErrorClause? RIGHT_PAREN
;
jsonExistsOnErrorClause
: ( ERROR | TRUE | FALSE ) ON ERROR;
/**
* The 'json_array()' function
*/
@ -1760,6 +1775,7 @@ jsonNullClause
| IS
| JOIN
| JSON_ARRAY
| JSON_EXISTS
| JSON_OBJECT
| JSON_VALUE
| KEY
@ -1812,6 +1828,7 @@ jsonNullClause
| OVERLAY
| PAD
| PARTITION
| PASSING
| PERCENT
| PLACING
| POSITION

View File

@ -469,6 +469,7 @@ public class CockroachDialect extends Dialect {
functionFactory.arrayToString_postgresql();
functionFactory.jsonValue_cockroachdb();
functionFactory.jsonExists_postgresql();
functionFactory.jsonObject_postgresql();
functionFactory.jsonArray_postgresql();

View File

@ -417,7 +417,8 @@ public class DB2Dialect extends Dialect {
functionFactory.listagg( null );
if ( getDB2Version().isSameOrAfter( 11 ) ) {
functionFactory.jsonValue();
functionFactory.jsonValue_no_passing();
functionFactory.jsonExists_no_passing();
functionFactory.jsonObject_db2();
functionFactory.jsonArray_db2();
}

View File

@ -4692,15 +4692,7 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun
* @apiNote Needed because MySQL has nonstandard escape characters
*/
public void appendLiteral(SqlAppender appender, String literal) {
appender.appendSql( '\'' );
for ( int i = 0; i < literal.length(); i++ ) {
final char c = literal.charAt( i );
if ( c == '\'' ) {
appender.appendSql( '\'' );
}
appender.appendSql( c );
}
appender.appendSql( '\'' );
appender.appendSingleQuoteEscapedString( literal );
}
/**

View File

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

View File

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

View File

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

View File

@ -399,7 +399,8 @@ public class OracleDialect extends Dialect {
functionFactory.arrayFill_oracle();
functionFactory.arrayToString_oracle();
functionFactory.jsonValue_literal_path();
functionFactory.jsonValue_oracle();
functionFactory.jsonExists_oracle();
functionFactory.jsonObject_oracle();
functionFactory.jsonArray_oracle();
}

View File

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

View File

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

View File

@ -79,25 +79,31 @@ import org.hibernate.dialect.function.array.PostgreSQLArrayTrimEmulation;
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.H2JsonValueFunction;
import org.hibernate.dialect.function.json.HANAJsonArrayFunction;
import org.hibernate.dialect.function.json.HANAJsonExistsFunction;
import org.hibernate.dialect.function.json.HANAJsonObjectFunction;
import org.hibernate.dialect.function.json.HSQLJsonArrayFunction;
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.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.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.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.SQLServerJsonValueFunction;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
@ -3349,14 +3355,21 @@ public class CommonFunctionFactory {
* json_value() function
*/
public void jsonValue() {
functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true ) );
functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, true ) );
}
/**
* json_value() function that supports only literal json paths
* json_value() function that doesn't support the passing clause
*/
public void jsonValue_literal_path() {
functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, false ) );
public void jsonValue_no_passing() {
functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, false ) );
}
/**
* Oracle json_value() function
*/
public void jsonValue_oracle() {
functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, false, false ) );
}
/**
@ -3401,6 +3414,62 @@ public class CommonFunctionFactory {
functionRegistry.register( "json_value", new H2JsonValueFunction( typeConfiguration ) );
}
/**
* json_exists() function
*/
public void jsonExists() {
functionRegistry.register( "json_exists", new JsonExistsFunction( typeConfiguration, true, true ) );
}
/**
* json_exists() function that doesn't support the passing clause
*/
public void jsonExists_no_passing() {
functionRegistry.register( "json_exists", new JsonExistsFunction( typeConfiguration, true, false ) );
}
/**
* Oracle json_exists() function
*/
public void jsonExists_oracle() {
functionRegistry.register( "json_exists", new JsonExistsFunction( typeConfiguration, false, true ) );
}
/**
* H2 json_exists() function
*/
public void jsonExists_h2() {
functionRegistry.register( "json_exists", new H2JsonExistsFunction( typeConfiguration ) );
}
/**
* SQL Server json_exists() function
*/
public void jsonExists_sqlserver() {
functionRegistry.register( "json_exists", new SQLServerJsonExistsFunction( typeConfiguration ) );
}
/**
* PostgreSQL json_exists() function
*/
public void jsonExists_postgresql() {
functionRegistry.register( "json_exists", new PostgreSQLJsonExistsFunction( typeConfiguration ) );
}
/**
* MySQL json_exists() function
*/
public void jsonExists_mysql() {
functionRegistry.register( "json_exists", new MySQLJsonExistsFunction( typeConfiguration ) );
}
/**
* SAP HANA json_exists() function
*/
public void jsonExists_hana() {
functionRegistry.register( "json_exists", new HANAJsonExistsFunction( typeConfiguration ) );
}
/**
* json_object() function
*/

View File

@ -13,11 +13,11 @@ import org.hibernate.dialect.Dialect;
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.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.sql.ast.tree.expression.Literal;
import org.hibernate.type.spi.TypeConfiguration;
/**
@ -26,7 +26,7 @@ import org.hibernate.type.spi.TypeConfiguration;
public class CockroachDBJsonValueFunction extends JsonValueFunction {
public CockroachDBJsonValueFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true );
super( typeConfiguration, true, false );
}
@Override
@ -75,6 +75,19 @@ public class CockroachDBJsonValueFunction extends JsonValueFunction {
if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) {
dialect.appendLiteral( sqlAppender, attribute.attribute() );
}
else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) {
final JsonPathPassingClause jsonPathPassingClause = arguments.passingClause();
assert jsonPathPassingClause != null;
final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) jsonPathElement ).parameterName();
final Expression expression = jsonPathPassingClause.getPassingExpressions().get( parameterName );
if ( expression == null ) {
throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" );
}
sqlAppender.appendSql( "cast(" );
expression.accept( walker );
sqlAppender.appendSql( " as text)" );
}
else {
sqlAppender.appendSql( '\'' );
sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() );

View File

@ -0,0 +1,54 @@
/*
* 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.JsonExistsErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
/**
* H2 json_exists function.
*/
public class H2JsonExistsFunction extends JsonExistsFunction {
public H2JsonExistsFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, true );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonExistsArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
// Json dereference errors by default if the JSON is invalid
if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonExistsErrorBehavior.ERROR ) {
throw new QueryException( "Can't emulate on error clause on H2" );
}
final String jsonPath;
try {
jsonPath = walker.getLiteralValue( arguments.jsonPath() );
}
catch (Exception ex) {
throw new QueryException( "H2 json_value only support literal json paths, but got " + arguments.jsonPath() );
}
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( " is not null and " );
H2JsonValueFunction.renderJsonPath(
sqlAppender,
arguments.jsonDocument(),
arguments.isJsonType(),
walker,
jsonPath,
arguments.passingClause()
);
sqlAppender.appendSql( " is not null" );
}
}

View File

@ -10,21 +10,26 @@ import java.util.List;
import org.hibernate.QueryException;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation;
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.ColumnReference;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* H2 json_value function.
*/
public class H2JsonValueFunction extends JsonValueFunction {
public H2JsonValueFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, false );
super( typeConfiguration, false, true );
}
@Override
@ -58,7 +63,16 @@ public class H2JsonValueFunction extends JsonValueFunction {
if ( defaultExpression != null ) {
sqlAppender.appendSql( "coalesce(" );
}
renderJsonPath( sqlAppender, arguments.jsonDocument(), walker, jsonPath );
sqlAppender.appendSql( "cast(" );
renderJsonPath(
sqlAppender,
arguments.jsonDocument(),
arguments.isJsonType(),
walker,
jsonPath,
arguments.passingClause()
);
sqlAppender.appendSql( " as varchar)" );
if ( defaultExpression != null ) {
sqlAppender.appendSql( ",cast(" );
defaultExpression.accept( walker );
@ -73,27 +87,45 @@ public class H2JsonValueFunction extends JsonValueFunction {
}
}
private void renderJsonPath(
public static void renderJsonPath(
SqlAppender sqlAppender,
SqlAstNode jsonDocument,
Expression jsonDocument,
boolean isJson,
SqlAstTranslator<?> walker,
String jsonPath) {
sqlAppender.appendSql( "cast(" );
String jsonPath,
@Nullable JsonPathPassingClause passingClause) {
final List<JsonPathHelper.JsonPathElement> jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath );
final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute;
final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute
&& jsonDocument.getColumnReference() != null
|| !isJson;
if ( needsWrapping ) {
sqlAppender.appendSql( '(' );
}
jsonDocument.accept( walker );
if ( needsWrapping ) {
if ( !isJson ) {
sqlAppender.append( " format json" );
}
sqlAppender.appendSql( ')' );
}
for ( int i = 0; i < jsonPathElements.size(); i++ ) {
final JsonPathHelper.JsonPathElement jsonPathElement = jsonPathElements.get( i );
if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) {
final String attributeName = attribute.attribute();
appendInDoubleQuotes( sqlAppender, attributeName );
sqlAppender.appendSql( "." );
sqlAppender.appendDoubleQuoteEscapedString( attributeName );
}
else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) {
assert passingClause != null;
final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) jsonPathElement ).parameterName();
final Expression expression = passingClause.getPassingExpressions().get( parameterName );
if ( expression == null ) {
throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" );
}
sqlAppender.appendSql( '[' );
expression.accept( walker );
sqlAppender.appendSql( "+1]" );
}
else {
sqlAppender.appendSql( '[' );
@ -101,18 +133,5 @@ public class H2JsonValueFunction extends JsonValueFunction {
sqlAppender.appendSql( ']' );
}
}
sqlAppender.appendSql( " as varchar)" );
}
private static void appendInDoubleQuotes(SqlAppender sqlAppender, String attributeName) {
sqlAppender.appendSql( ".\"" );
for ( int j = 0; j < attributeName.length(); j++ ) {
final char c = attributeName.charAt( j );
if ( c == '"' ) {
sqlAppender.appendSql( '"' );
}
sqlAppender.appendSql( c );
}
sqlAppender.appendSql( '"' );
}
}

View File

@ -0,0 +1,61 @@
/*
* 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.Expression;
import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.type.spi.TypeConfiguration;
/**
* SAP HANA json_exists function.
*/
public class HANAJsonExistsFunction extends JsonExistsFunction {
public HANAJsonExistsFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, false );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonExistsArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "json_query(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ',' );
final Expression jsonPath = arguments.jsonPath();
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( passingClause == null ) {
jsonPath.accept( walker );
}
else {
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
"",
arguments.jsonPath(),
passingClause,
walker
);
}
final JsonExistsErrorBehavior errorBehavior = arguments.errorBehavior();
if ( errorBehavior != null && errorBehavior != JsonExistsErrorBehavior.FALSE ) {
if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) {
sqlAppender.appendSql( " empty object on error" );
}
else {
sqlAppender.appendSql( " error on error" );
}
}
sqlAppender.appendSql( ") is not null" );
}
}

View File

@ -106,15 +106,7 @@ public class HANAJsonObjectFunction extends JsonObjectFunction {
value.accept( walker );
sqlAppender.appendSql( ' ' );
final String literalValue = walker.getLiteralValue( (Expression) key );
sqlAppender.appendSql( '"' );
for ( int j = 0; j < literalValue.length(); j++ ) {
final char c = literalValue.charAt( j );
if ( c == '"' ) {
sqlAppender.appendSql( '"' );
}
sqlAppender.appendSql( c );
}
sqlAppender.appendSql( '"' );
sqlAppender.appendDoubleQuoteEscapedString( literalValue );
separator = ',';
}
sqlAppender.appendSql( " from sys.dummy for json('arraywrap'='no'" );

View File

@ -0,0 +1,190 @@
/*
* 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.SqmJsonExistsExpression;
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.JsonExistsErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
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_exists function.
*/
public class JsonExistsFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
protected final boolean supportsJsonPathExpression;
protected final boolean supportsJsonPathPassingClause;
public JsonExistsFunction(
TypeConfiguration typeConfiguration,
boolean supportsJsonPathExpression,
boolean supportsJsonPathPassingClause) {
super(
"json_exists",
FunctionKind.NORMAL,
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY )
),
StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.standardBasicTypeForJavaType( Boolean.class ) ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING )
);
this.supportsJsonPathExpression = supportsJsonPathExpression;
this.supportsJsonPathPassingClause = supportsJsonPathPassingClause;
}
@Override
public boolean isPredicate() {
return true;
}
@Override
protected <T> SelfRenderingSqmFunction<T> generateSqmFunctionExpression(
List<? extends SqmTypedNode<?>> arguments,
ReturnableType<T> impliedResultType,
QueryEngine queryEngine) {
//noinspection unchecked
return (SelfRenderingSqmFunction<T>) new SqmJsonExistsExpression(
this,
this,
arguments,
(ReturnableType<Boolean>) impliedResultType,
getArgumentsValidator(),
getReturnTypeResolver(),
queryEngine.getCriteriaBuilder(),
getName()
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
render( sqlAppender, JsonExistsArguments.extract( sqlAstArguments ), returnType, walker );
}
protected void render(
SqlAppender sqlAppender,
JsonExistsArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "json_exists(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ',' );
final Expression jsonPath = arguments.jsonPath();
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( supportsJsonPathPassingClause || passingClause == null ) {
if ( supportsJsonPathExpression ) {
jsonPath.accept( walker );
}
else {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
sqlAppender,
walker.getLiteralValue( 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
);
}
final JsonExistsErrorBehavior errorBehavior = arguments.errorBehavior();
if ( errorBehavior != null && errorBehavior != JsonExistsErrorBehavior.FALSE ) {
if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) {
sqlAppender.appendSql( " true on error" );
}
else {
sqlAppender.appendSql( " error on error" );
}
}
sqlAppender.appendSql( ')' );
}
protected record JsonExistsArguments(
Expression jsonDocument,
Expression jsonPath,
boolean isJsonType,
@Nullable JsonPathPassingClause passingClause,
@Nullable JsonExistsErrorBehavior errorBehavior) {
public static JsonExistsArguments extract(List<? extends SqlAstNode> sqlAstArguments) {
int nextIndex = 2;
JsonPathPassingClause passingClause = null;
JsonExistsErrorBehavior errorBehavior = 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 JsonExistsErrorBehavior ) {
errorBehavior = (JsonExistsErrorBehavior) node;
nextIndex++;
}
}
final Expression jsonDocument = (Expression) sqlAstArguments.get( 0 );
return new JsonExistsArguments(
jsonDocument,
(Expression) sqlAstArguments.get( 1 ),
jsonDocument.getExpressionType() != null
&& jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(),
passingClause,
errorBehavior
);
}
}
}

View File

@ -10,6 +10,10 @@ import java.util.ArrayList;
import java.util.List;
import org.hibernate.QueryException;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
public class JsonPathHelper {
@ -47,6 +51,112 @@ public class JsonPathHelper {
return jsonPathElements;
}
public static void appendJsonPathConcatPassingClause(
SqlAppender sqlAppender,
Expression jsonPathExpression,
JsonPathPassingClause passingClause, SqlAstTranslator<?> walker) {
appendJsonPathConcatenatedPassingClause( sqlAppender, jsonPathExpression, passingClause, walker, "concat", "," );
}
public static void appendJsonPathDoublePipePassingClause(
SqlAppender sqlAppender,
Expression jsonPathExpression,
JsonPathPassingClause passingClause,
SqlAstTranslator<?> walker) {
appendJsonPathConcatenatedPassingClause( sqlAppender, jsonPathExpression, passingClause, walker, "", "||" );
}
public static void appendInlinedJsonPathIncludingPassingClause(
SqlAppender sqlAppender,
String prefix,
Expression jsonPathExpression,
JsonPathPassingClause passingClause,
SqlAstTranslator<?> walker) {
final String jsonPath = walker.getLiteralValue( jsonPathExpression );
final String[] parts = jsonPath.split( "\\$" );
sqlAppender.append( '\'' );
sqlAppender.append( prefix );
final int start;
if ( parts[0].isEmpty() ) {
start = 2;
sqlAppender.append( '$' );
sqlAppender.append( parts[1] );
}
else {
start = 0;
}
for ( int i = start; i < parts.length; i++ ) {
final String part = parts[i];
final int parameterNameEndIndex = indexOfNonIdentifier( part, 0 );
final String parameterName = part.substring( 0, parameterNameEndIndex );
final Expression expression = passingClause.getPassingExpressions().get( parameterName );
if ( expression == null ) {
throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" );
}
Object literalValue = walker.getLiteralValue( expression );
if ( literalValue instanceof String ) {
appendLiteral( sqlAppender, 0, (String) literalValue );
}
else {
sqlAppender.appendSql( String.valueOf( literalValue ) );
}
appendLiteral( sqlAppender, parameterNameEndIndex, part );
}
sqlAppender.appendSql( '\'' );
}
private static void appendLiteral(SqlAppender sqlAppender, int parameterNameEndIndex, String part) {
for ( int j = parameterNameEndIndex; j < part.length(); j++ ) {
final char c = part.charAt( j );
if ( c == '\'') {
sqlAppender.appendSql( '\'' );
}
sqlAppender.appendSql( c );
}
}
private static void appendJsonPathConcatenatedPassingClause(
SqlAppender sqlAppender,
Expression jsonPathExpression,
JsonPathPassingClause passingClause,
SqlAstTranslator<?> walker,
String concatStart,
String concatCombine) {
final String jsonPath = walker.getLiteralValue( jsonPathExpression );
final String[] parts = jsonPath.split( "\\$" );
sqlAppender.append( concatStart );
final int start;
String separator = "(";
if ( parts[0].isEmpty() ) {
start = 2;
sqlAppender.append( separator );
sqlAppender.append( "'$'" );
sqlAppender.append( concatCombine );
sqlAppender.appendSingleQuoteEscapedString( parts[1] );
separator = concatCombine;
}
else {
start = 0;
}
for ( int i = start; i < parts.length; i++ ) {
final String part = parts[i];
sqlAppender.append( separator );
final int parameterNameEndIndex = indexOfNonIdentifier( part, 0 );
final String parameterName = part.substring( 0, parameterNameEndIndex );
final Expression expression = passingClause.getPassingExpressions().get( parameterName );
if ( expression == null ) {
throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" );
}
expression.accept( walker );
sqlAppender.append( ',' );
sqlAppender.appendSingleQuoteEscapedString( part.substring( parameterNameEndIndex ) );
separator = concatCombine;
}
sqlAppender.appendSql( ')' );
}
private static void parseAttribute(String jsonPath, int startIndex, int endIndex, ArrayList<JsonPathElement> jsonPathElements) {
final int bracketIndex = jsonPath.indexOf( '[', startIndex );
if ( bracketIndex != -1 && bracketIndex < endIndex ) {
@ -64,11 +174,40 @@ public class JsonPathHelper {
if ( bracketEndIndex < bracketStartIndex ) {
throw new QueryException( "Can't emulate non-simple json path expression: " + jsonPath );
}
final int index = Integer.parseInt( jsonPath, bracketStartIndex + 1, bracketEndIndex, 10 );
jsonPathElements.add( new JsonIndexAccess( index ) );
final int contentStartIndex = indexOfNonWhitespace( jsonPath, bracketStartIndex + 1 );
final int contentEndIndex = lastIndexOfWhitespace( jsonPath, bracketEndIndex - 1 );
if ( jsonPath.charAt( contentStartIndex ) == '$' ) {
jsonPathElements.add( new JsonParameterIndexAccess( jsonPath.substring( contentStartIndex + 1, contentEndIndex ) ) );
}
else {
final int index = Integer.parseInt( jsonPath, contentStartIndex, contentEndIndex, 10 );
jsonPathElements.add( new JsonIndexAccess( index ) );
}
}
public static int indexOfNonIdentifier(String jsonPath, int i) {
while ( i < jsonPath.length() && Character.isJavaIdentifierPart( jsonPath.charAt( i ) ) ) {
i++;
}
return i;
}
private static int indexOfNonWhitespace(String jsonPath, int i) {
while ( i < jsonPath.length() && Character.isWhitespace( jsonPath.charAt( i ) ) ) {
i++;
}
return i;
}
private static int lastIndexOfWhitespace(String jsonPath, int i) {
while ( i > 0 && Character.isWhitespace( jsonPath.charAt( i ) ) ) {
i--;
}
return i + 1;
}
public sealed interface JsonPathElement {}
public record JsonAttribute(String attribute) implements JsonPathElement {}
public record JsonIndexAccess(int index) implements JsonPathElement {}
public record JsonParameterIndexAccess(String parameterName) implements JsonPathElement {}
}

View File

@ -6,7 +6,9 @@
*/
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;
@ -23,6 +25,7 @@ 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.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
@ -40,8 +43,12 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STR
public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
protected final boolean supportsJsonPathExpression;
protected final boolean supportsJsonPathPassingClause;
public JsonValueFunction(TypeConfiguration typeConfiguration, boolean supportsJsonPathExpression) {
public JsonValueFunction(
TypeConfiguration typeConfiguration,
boolean supportsJsonPathExpression,
boolean supportsJsonPathPassingClause) {
super(
"json_value",
FunctionKind.NORMAL,
@ -52,6 +59,7 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescripto
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING )
);
this.supportsJsonPathExpression = supportsJsonPathExpression;
this.supportsJsonPathPassingClause = supportsJsonPathPassingClause;
}
@Override
@ -88,16 +96,43 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescripto
sqlAppender.appendSql( "json_value(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ',' );
if ( supportsJsonPathExpression ) {
arguments.jsonPath().accept( walker );
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 {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
walker.getLiteralValue( arguments.jsonPath() )
"",
arguments.jsonPath(),
passingClause,
walker
);
}
if ( arguments.returningType() != null ) {
sqlAppender.appendSql( " returning " );
arguments.returningType().accept( walker );
@ -133,11 +168,13 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescripto
Expression jsonDocument,
Expression jsonPath,
boolean isJsonType,
@Nullable JsonPathPassingClause passingClause,
@Nullable CastTarget returningType,
@Nullable JsonValueErrorBehavior errorBehavior,
@Nullable JsonValueEmptyBehavior emptyBehavior) {
public static JsonValueArguments extract(List<? extends SqlAstNode> sqlAstArguments) {
int nextIndex = 2;
JsonPathPassingClause passingClause = null;
CastTarget castTarget = null;
JsonValueErrorBehavior errorBehavior = null;
JsonValueEmptyBehavior emptyBehavior = null;
@ -148,6 +185,13 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescripto
nextIndex++;
}
}
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 JsonValueErrorBehavior ) {
@ -167,6 +211,7 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescripto
(Expression) sqlAstArguments.get( 1 ),
jsonDocument.getExpressionType() != null
&& jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(),
passingClause,
castTarget,
errorBehavior,
emptyBehavior

View File

@ -10,6 +10,7 @@ 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.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
@ -20,7 +21,7 @@ import org.hibernate.type.spi.TypeConfiguration;
public class MariaDBJsonValueFunction extends JsonValueFunction {
public MariaDBJsonValueFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true );
super( typeConfiguration, true, false );
}
@Override
@ -42,7 +43,17 @@ public class MariaDBJsonValueFunction extends JsonValueFunction {
sqlAppender.appendSql( "json_unquote(nullif(json_extract(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( "," );
arguments.jsonPath().accept( walker );
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( passingClause == null ) {
arguments.jsonPath().accept( walker );
}
else {
JsonPathHelper.appendJsonPathConcatPassingClause(
sqlAppender,
arguments.jsonPath(),
passingClause, walker
);
}
sqlAppender.appendSql( "),'null'))" );
if ( arguments.returningType() != null ) {
sqlAppender.appendSql( " as " );

View File

@ -0,0 +1,46 @@
/*
* 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.type.spi.TypeConfiguration;
/**
* MySQL json_exists function.
*/
public class MySQLJsonExistsFunction extends JsonExistsFunction {
public MySQLJsonExistsFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, false );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonExistsArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
final JsonPathPassingClause passingClause = arguments.passingClause();
sqlAppender.appendSql( "json_contains_path(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ",'one'," );
if ( passingClause == null ) {
arguments.jsonPath().accept( walker );
}
else {
JsonPathHelper.appendJsonPathConcatPassingClause(
sqlAppender,
arguments.jsonPath(),
passingClause, walker
);
}
sqlAppender.appendSql( ')' );
}
}

View File

@ -9,6 +9,7 @@ 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.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
@ -19,7 +20,7 @@ import org.hibernate.type.spi.TypeConfiguration;
public class MySQLJsonValueFunction extends JsonValueFunction {
public MySQLJsonValueFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true );
super( typeConfiguration, true, false );
}
@Override
@ -42,7 +43,17 @@ public class MySQLJsonValueFunction extends JsonValueFunction {
sqlAppender.appendSql( "json_unquote(nullif(json_extract(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( "," );
arguments.jsonPath().accept( walker );
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)))" );
if ( arguments.returningType() != null ) {
sqlAppender.appendSql( " as " );

View File

@ -0,0 +1,52 @@
/*
* 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.query.ReturnableType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.type.spi.TypeConfiguration;
/**
* PostgreSQL json_exists function.
*/
public class PostgreSQLJsonExistsFunction extends JsonExistsFunction {
public PostgreSQLJsonExistsFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, true );
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonExistsArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
sqlAppender.appendSql( "jsonb_path_exists(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ',' );
arguments.jsonPath().accept( walker );
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( ')' );
}
sqlAppender.appendSql( ')' );
}
}

View File

@ -6,12 +6,16 @@
*/
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.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.sql.ast.tree.expression.Literal;
@ -23,7 +27,7 @@ import org.hibernate.type.spi.TypeConfiguration;
public class PostgreSQLJsonValueFunction extends JsonValueFunction {
public PostgreSQLJsonValueFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true );
super( typeConfiguration, true, true );
}
@Override
@ -61,6 +65,19 @@ public class PostgreSQLJsonValueFunction extends JsonValueFunction {
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( ")#>>'{}'" );
if ( arguments.returningType() != null ) {

View File

@ -0,0 +1,88 @@
/*
* 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.JsonExistsErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior;
import org.hibernate.type.spi.TypeConfiguration;
/**
* SQL Server json_exists function.
*/
public class SQLServerJsonExistsFunction extends JsonExistsFunction {
public SQLServerJsonExistsFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true, false );
}
@Override
public boolean isPredicate() {
return false;
}
@Override
protected void render(
SqlAppender sqlAppender,
JsonExistsArguments arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
if ( arguments.errorBehavior() == JsonExistsErrorBehavior.TRUE ) {
throw new QueryException( "Can't emulate json_exists(... true on error) on SQL Server" );
}
if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) {
sqlAppender.append( '(' );
}
sqlAppender.appendSql( "json_path_exists(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ',' );
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( passingClause != null ) {
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
"",
arguments.jsonPath(),
passingClause,
walker
);
}
else {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
sqlAppender,
walker.getLiteralValue( arguments.jsonPath() )
);
}
sqlAppender.appendSql( ')' );
if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) {
// json_path_exists returns 0 if an invalid JSON is given,
// so we have to run openjson to be sure the json is valid and potentially throw an error
sqlAppender.appendSql( "=1 or (select v from openjson(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ") with (v varchar(max) " );
if ( passingClause != null ) {
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
"",
arguments.jsonPath(),
passingClause,
walker
);
}
else {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
sqlAppender,
walker.getLiteralValue( arguments.jsonPath() )
);
}
sqlAppender.appendSql( ")) is null)" );
}
}
}

View File

@ -10,7 +10,7 @@ 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.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
import org.hibernate.type.spi.TypeConfiguration;
@ -21,7 +21,7 @@ import org.hibernate.type.spi.TypeConfiguration;
public class SQLServerJsonValueFunction extends JsonValueFunction {
public SQLServerJsonValueFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration, true );
super( typeConfiguration, true, false );
}
@Override
@ -36,7 +36,7 @@ public class SQLServerJsonValueFunction extends JsonValueFunction {
}
sqlAppender.appendSql( "(select v from openjson(" );
arguments.jsonDocument().accept( walker );
sqlAppender.appendSql( ",'$') with (v " );
sqlAppender.appendSql( ") with (v " );
if ( arguments.returningType() != null ) {
arguments.returningType().accept( walker );
}
@ -44,10 +44,32 @@ public class SQLServerJsonValueFunction extends JsonValueFunction {
sqlAppender.appendSql( "varchar(max)" );
}
sqlAppender.appendSql( ' ' );
final JsonPathPassingClause passingClause = arguments.passingClause();
if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
// The strict modifier will cause an error to be thrown if a field doesn't exist
if ( passingClause != null ) {
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
"strict ",
arguments.jsonPath(),
passingClause,
walker
);
}
else {
walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral(
sqlAppender,
"strict " + walker.getLiteralValue( arguments.jsonPath() )
);
}
}
else if ( passingClause != null ) {
JsonPathHelper.appendInlinedJsonPathIncludingPassingClause(
sqlAppender,
"strict " + walker.getLiteralValue( arguments.jsonPath() )
"",
arguments.jsonPath(),
passingClause,
walker
);
}
else {

View File

@ -4,6 +4,8 @@
*/
package org.hibernate.internal.util;
import org.hibernate.sql.ast.spi.SqlAppender;
public final class QuotingHelper {
private QuotingHelper() { /* static methods only - hide constructor */
@ -150,4 +152,25 @@ public final class QuotingHelper {
}
return sb.toString();
}
public static void appendDoubleQuoteEscapedString(StringBuilder sb, String text) {
appendWithDoubleEscaping( sb, text, '"' );
}
public static void appendSingleQuoteEscapedString(StringBuilder sb, String text) {
appendWithDoubleEscaping( sb, text, '\'' );
}
private static void appendWithDoubleEscaping(StringBuilder sb, String text, char quoteChar) {
sb.append( quoteChar );
for ( int i = 0; i < text.length(); i++ ) {
final char c = text.charAt( i );
if ( c == quoteChar ) {
sb.append( quoteChar );
}
sb.append( c );
}
sb.append( quoteChar );
}
}

View File

@ -3691,7 +3691,7 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
JpaJsonValueExpression<String> jsonValue(Expression<?> jsonDocument, String jsonPath);
/**
* Extracts a value by JSON path from a json document.
* Extracts a value by JSON path from a JSON document.
*
* @since 7.0
*/
@ -3706,13 +3706,29 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
JpaJsonValueExpression<String> jsonValue(Expression<?> jsonDocument, Expression<String> jsonPath);
/**
* Extracts a value by JSON path from a json document.
* Extracts a value by JSON path from a JSON document.
*
* @since 7.0
*/
@Incubating
<T> JpaJsonValueExpression<T> jsonValue(Expression<?> jsonDocument, Expression<String> jsonPath, Class<T> returningType);
/**
* Checks if a JSON document contains a node for the given JSON path.
*
* @since 7.0
*/
@Incubating
JpaJsonExistsExpression jsonExists(Expression<?> jsonDocument, String jsonPath);
/**
* Checks if a JSON document contains a node for the given JSON path.
*
* @since 7.0
*/
@Incubating
JpaJsonExistsExpression jsonExists(Expression<?> jsonDocument, Expression<String> jsonPath);
/**
* Create a JSON object from the given map of key values.
*

View File

@ -0,0 +1,79 @@
/*
* 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_exists} function.
* @since 7.0
*/
@Incubating
public interface JpaJsonExistsExpression extends JpaExpression<Boolean> {
/**
* Get the {@link ErrorBehavior} of this json value expression.
*
* @return the error behavior
*/
ErrorBehavior getErrorBehavior();
/**
* Sets the {@link ErrorBehavior#UNSPECIFIED} for this json exists expression.
*
* @return {@code this} for method chaining
*/
JpaJsonExistsExpression unspecifiedOnError();
/**
* Sets the {@link ErrorBehavior#ERROR} for this json exists expression.
*
* @return {@code this} for method chaining
*/
JpaJsonExistsExpression errorOnError();
/**
* Sets the {@link ErrorBehavior#TRUE} for this json exists expression.
*
* @return {@code this} for method chaining
*/
JpaJsonExistsExpression trueOnError();
/**
* Sets the {@link ErrorBehavior#FALSE} for this json exists expression.
*
* @return {@code this} for method chaining
*/
JpaJsonExistsExpression falseOnError();
/**
* Passes the given {@link Expression} as value for the parameter with the given name in the JSON path.
*
* @return {@code this} for method chaining
*/
JpaJsonExistsExpression passing(String parameterName, Expression<?> expression);
/**
* The behavior of the json value expression when a JSON processing error occurs.
*/
enum ErrorBehavior {
/**
* SQL/JDBC error should be raised.
*/
ERROR,
/**
* {@code true} should be returned on error.
*/
TRUE,
/**
* {@code false} should be returned on error.
*/
FALSE,
/**
* Unspecified behavior i.e. the default database behavior.
*/
UNSPECIFIED
}
}

View File

@ -97,6 +97,13 @@ public interface JpaJsonValueExpression<T> extends JpaExpression<T> {
*/
JpaJsonValueExpression<T> defaultOnEmpty(Expression<?> expression);
/**
* Passes the given {@link Expression} as value for the parameter with the given name in the JSON path.
*
* @return {@code this} for method chaining
*/
JpaJsonValueExpression<T> passing(String parameterName, Expression<?> expression);
/**
* The behavior of the json value expression when a JSON processing error occurs.
*/

View File

@ -38,6 +38,7 @@ import org.hibernate.query.criteria.JpaExpression;
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.JpaJsonValueExpression;
import org.hibernate.query.criteria.JpaListJoin;
import org.hibernate.query.criteria.JpaMapJoin;
@ -3375,6 +3376,18 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
return criteriaBuilder.jsonValue( jsonDocument, jsonPath, returningType );
}
@Override
@Incubating
public JpaJsonExistsExpression jsonExists(Expression<?> jsonDocument, String jsonPath) {
return criteriaBuilder.jsonExists( jsonDocument, jsonPath );
}
@Override
@Incubating
public JpaJsonExistsExpression jsonExists(Expression<?> jsonDocument, Expression<String> jsonPath) {
return criteriaBuilder.jsonExists( jsonDocument, jsonPath );
}
@Override
@Incubating
public JpaExpression<String> jsonObject(Map<?, ? extends Expression<?>> keyValues) {

View File

@ -144,6 +144,7 @@ import org.hibernate.query.sqm.tree.expression.SqmExtractUnit;
import org.hibernate.query.sqm.tree.expression.SqmFormat;
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.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
@ -2730,9 +2731,53 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
}
}
}
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++ ) {
jsonValue.passing(
visitIdentifier( identifierContexts.get( i ) ),
(SqmExpression<?>) expressionContexts.get( i ).accept( this )
);
}
}
return jsonValue;
}
@Override
public SqmExpression<?> visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext ctx) {
final SqmExpression<?> jsonDocument = (SqmExpression<?>) ctx.expression( 0 ).accept( this );
final SqmExpression<?> jsonPath = (SqmExpression<?>) ctx.expression( 1 ).accept( this );
final SqmJsonExistsExpression jsonExists = (SqmJsonExistsExpression) getFunctionDescriptor( "json_exists" ).<Boolean>generateSqmExpression(
asList( jsonDocument, jsonPath ),
null,
creationContext.getQueryEngine()
);
final HqlParser.JsonExistsOnErrorClauseContext subCtx = ctx.jsonExistsOnErrorClause();
if ( subCtx != null ) {
final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 );
switch ( firstToken.getSymbol().getType() ) {
case HqlParser.ERROR -> jsonExists.errorOnError();
case HqlParser.TRUE -> jsonExists.trueOnError();
case HqlParser.FALSE -> jsonExists.falseOnError();
}
}
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++ ) {
jsonExists.passing(
visitIdentifier( identifierContexts.get( i ) ),
(SqmExpression<?>) expressionContexts.get( i ).accept( this )
);
}
}
return jsonExists;
}
@Override
public SqmExpression<?> visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext ctx) {
final HqlParser.JsonNullClauseContext subCtx = ctx.jsonNullClause();

View File

@ -1,33 +0,0 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.query.internal;
/**
* @author Christian Beikov
*/
public class QueryLiteralHelper {
private QueryLiteralHelper() {
// disallow direct instantiation
}
public static String toStringLiteral(String value) {
final StringBuilder sb = new StringBuilder( value.length() + 2 );
appendStringLiteral( sb, value );
return sb.toString();
}
public static void appendStringLiteral(StringBuilder sb, String value) {
sb.append( '\'' );
for ( int i = 0; i < value.length(); i++ ) {
final char c = value.charAt( i );
if ( c == '\'' ) {
sb.append( '\'' );
}
sb.append( c );
}
sb.append( '\'' );
}
}

View File

@ -26,6 +26,7 @@ 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;
@ -44,6 +45,7 @@ import org.hibernate.query.sqm.tree.domain.SqmSetJoin;
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.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression;
import org.hibernate.query.sqm.tree.expression.SqmTuple;
@ -629,6 +631,12 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
@Override
SqmJsonValueExpression<String> jsonValue(Expression<?> jsonDocument, String jsonPath);
@Override
SqmJsonExistsExpression jsonExists(Expression<?> jsonDocument, Expression<String> jsonPath);
@Override
SqmJsonExistsExpression jsonExists(Expression<?> jsonDocument, String jsonPath);
@Override
SqmExpression<String> jsonArrayWithNulls(Expression<?>... values);

View File

@ -220,4 +220,13 @@ public interface SqmFunctionDescriptor {
* @return an instance of {@link ArgumentsValidator}
*/
ArgumentsValidator getArgumentsValidator();
/**
* Whether the function renders as a predicate.
*
* @since 7.0
*/
default boolean isPredicate() {
return false;
}
}

View File

@ -120,6 +120,7 @@ import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmExtractUnit;
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.SqmJsonValueExpression;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
@ -5333,6 +5334,20 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
}
}
@Override
public SqmJsonExistsExpression jsonExists(Expression<?> jsonDocument, String jsonPath) {
return jsonExists( jsonDocument, value( jsonPath ) );
}
@Override
public SqmJsonExistsExpression jsonExists(Expression<?> jsonDocument, Expression<String> jsonPath) {
return (SqmJsonExistsExpression) getFunctionDescriptor( "json_exists" ).<Boolean>generateSqmExpression(
asList( (SqmTypedNode<?>) jsonDocument, (SqmTypedNode<?>) jsonPath ),
null,
queryEngine
);
}
@Override
public SqmExpression<String> jsonArrayWithNulls(Expression<?>... values) {
final var arguments = new ArrayList<SqmTypedNode<?>>( values.length + 1 );

View File

@ -289,6 +289,7 @@ import org.hibernate.query.sqm.tree.update.SqmUpdateStatement;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.ast.SqlTreeCreationException;
import org.hibernate.sql.ast.SqlTreeCreationLogger;
import org.hibernate.sql.ast.internal.TableGroupJoinHelper;
@ -297,6 +298,7 @@ import org.hibernate.sql.ast.spi.SqlAliasBase;
import org.hibernate.sql.ast.spi.SqlAliasBaseConstant;
import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator;
import org.hibernate.sql.ast.spi.SqlAliasBaseManager;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.sql.ast.spi.SqlAstCreationContext;
import org.hibernate.sql.ast.spi.SqlAstCreationState;
import org.hibernate.sql.ast.spi.SqlAstProcessingState;
@ -6439,7 +6441,22 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
functionImpliedResultTypeAccess = inferrableTypeAccessStack.getCurrent();
inferrableTypeAccessStack.push( () -> null );
try {
return sqmFunction.convertToSqlAst( this );
final Expression expression = sqmFunction.convertToSqlAst( this );
if ( sqmFunction.getFunctionDescriptor().isPredicate()
&& expression instanceof SelfRenderingExpression selfRenderingExpression) {
final BasicType<Boolean> booleanType = getBooleanType();
return new CaseSearchedExpression(
booleanType,
List.of(
new CaseSearchedExpression.WhenFragment(
new SelfRenderingPredicate( selfRenderingExpression ),
new QueryLiteral<>( true, booleanType )
)
),
new QueryLiteral<>( false, booleanType )
);
}
return expression;
}
finally {
inferrableTypeAccessStack.pop();
@ -8220,14 +8237,27 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
);
}
private JdbcMappingContainer getBooleanType() {
private BasicType<Boolean> getBooleanType() {
return getTypeConfiguration().getBasicTypeForJavaType( Boolean.class );
}
@Override
public Object visitBooleanExpressionPredicate(SqmBooleanExpressionPredicate predicate) {
inferrableTypeAccessStack.push( this::getBooleanType );
final Expression booleanExpression = (Expression) predicate.getBooleanExpression().accept( this );
final SqmExpression<Boolean> sqmExpression = predicate.getBooleanExpression();
final Expression booleanExpression = (Expression) sqmExpression.accept( this );
if ( booleanExpression instanceof CaseSearchedExpression caseExpr
&& sqmExpression instanceof SqmFunction<?> sqmFunction
&& sqmFunction.getFunctionDescriptor().isPredicate() ) {
// Functions that are rendered as predicates are always wrapped,
// so the following unwraps the predicate and returns it directly instead of wrapping once more
final Predicate sqlPredicate = caseExpr.getWhenFragments().get( 0 ).getPredicate();
if ( predicate.isNegated() ) {
return new NegatedPredicate( sqlPredicate );
}
return sqlPredicate;
}
inferrableTypeAccessStack.pop();
if ( booleanExpression instanceof SelfRenderingExpression ) {
final Predicate sqlPredicate = new SelfRenderingPredicate( (SelfRenderingExpression) booleanExpression );

View File

@ -0,0 +1,128 @@
/*
* 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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.Incubating;
import org.hibernate.internal.util.QuotingHelper;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.function.FunctionRenderer;
import org.hibernate.query.sqm.function.SelfRenderingSqmFunction;
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.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Base class for expressions that contain a json path. Maintains a map of expressions for identifiers.
*
* @since 7.0
*/
@Incubating
public abstract class AbstractSqmJsonPathExpression<T> extends SelfRenderingSqmFunction<T> {
private @Nullable Map<String, SqmExpression<?>> passingExpressions;
public AbstractSqmJsonPathExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<T> impliedResultType,
@Nullable ArgumentsValidator argumentsValidator,
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name) {
super(
descriptor,
renderer,
arguments,
impliedResultType,
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name
);
}
protected AbstractSqmJsonPathExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<T> impliedResultType,
@Nullable ArgumentsValidator argumentsValidator,
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name,
@Nullable Map<String, SqmExpression<?>> passingExpressions) {
super(
descriptor,
renderer,
arguments,
impliedResultType,
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name
);
this.passingExpressions = passingExpressions;
}
public Map<String, SqmExpression<?>> getPassingExpressions() {
return passingExpressions == null ? Collections.emptyMap() : Collections.unmodifiableMap( passingExpressions );
}
protected void addPassingExpression(String identifier, SqmExpression<?> expression) {
if ( passingExpressions == null ) {
passingExpressions = new HashMap<>();
}
passingExpressions.put( identifier, expression );
}
protected Map<String, SqmExpression<?>> copyPassingExpressions(SqmCopyContext context) {
if ( passingExpressions == null ) {
return null;
}
final HashMap<String, SqmExpression<?>> copy = new HashMap<>( passingExpressions.size() );
for ( Map.Entry<String, SqmExpression<?>> entry : passingExpressions.entrySet() ) {
copy.put( entry.getKey(), entry.getValue().copy( context ) );
}
return copy;
}
protected @Nullable JsonPathPassingClause createJsonPathPassingClause(SqmToSqlAstConverter walker) {
if ( passingExpressions == null || passingExpressions.isEmpty() ) {
return null;
}
final HashMap<String, Expression> converted = new HashMap<>( passingExpressions.size() );
for ( Map.Entry<String, SqmExpression<?>> entry : passingExpressions.entrySet() ) {
converted.put( entry.getKey(), (Expression) entry.getValue().accept( walker ) );
}
return new JsonPathPassingClause( converted );
}
protected void appendPassingExpressionHqlString(StringBuilder sb) {
if ( passingExpressions != null && !passingExpressions.isEmpty() ) {
sb.append( " passing " );
for ( Map.Entry<String, SqmExpression<?>> entry : passingExpressions.entrySet() ) {
entry.getValue().appendHqlString( sb );
sb.append( " as " );
QuotingHelper.appendDoubleQuoteEscapedString( sb, entry.getKey() );
}
}
}
}

View File

@ -0,0 +1,207 @@
/*
* 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.JpaJsonExistsExpression;
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.JsonExistsErrorBehavior;
import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* Special expression for the json_exists function that also captures special syntax elements like error behavior and passing variables.
*
* @since 7.0
*/
@Incubating
public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression<Boolean> implements JpaJsonExistsExpression {
private @Nullable ErrorBehavior errorBehavior;
public SqmJsonExistsExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<Boolean> impliedResultType,
@Nullable ArgumentsValidator argumentsValidator,
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name) {
super(
descriptor,
renderer,
arguments,
impliedResultType,
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name
);
}
private SqmJsonExistsExpression(
SqmFunctionDescriptor descriptor,
FunctionRenderer renderer,
List<? extends SqmTypedNode<?>> arguments,
@Nullable ReturnableType<Boolean> impliedResultType,
@Nullable ArgumentsValidator argumentsValidator,
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name,
@Nullable Map<String, SqmExpression<?>> passingExpressions,
@Nullable ErrorBehavior errorBehavior) {
super(
descriptor,
renderer,
arguments,
impliedResultType,
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name,
passingExpressions
);
this.errorBehavior = errorBehavior;
}
public SqmJsonExistsExpression copy(SqmCopyContext context) {
final SqmJsonExistsExpression 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 SqmJsonExistsExpression(
getFunctionDescriptor(),
getFunctionRenderer(),
arguments,
getImpliedResultType(),
getArgumentsValidator(),
getReturnTypeResolver(),
nodeBuilder(),
getFunctionName(),
copyPassingExpressions( context ),
errorBehavior
)
);
}
@Override
public ErrorBehavior getErrorBehavior() {
return errorBehavior;
}
@Override
public SqmJsonExistsExpression unspecifiedOnError() {
this.errorBehavior = ErrorBehavior.UNSPECIFIED;
return this;
}
@Override
public SqmJsonExistsExpression errorOnError() {
this.errorBehavior = ErrorBehavior.ERROR;
return this;
}
@Override
public SqmJsonExistsExpression trueOnError() {
this.errorBehavior = ErrorBehavior.TRUE;
return this;
}
@Override
public SqmJsonExistsExpression falseOnError() {
this.errorBehavior = ErrorBehavior.FALSE;
return this;
}
@Override
public SqmJsonExistsExpression 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 );
}
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;
}
}
return new SelfRenderingFunctionSqlAstExpression(
getFunctionName(),
getFunctionRenderer(),
arguments,
resultType,
resultType == null ? null : getMappingModelExpressible( walker, resultType, arguments )
);
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( "json_exists(" );
getArguments().get( 0 ).appendHqlString( sb );
sb.append( ',' );
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;
}
}
sb.append( ')' );
}
}

View File

@ -8,6 +8,7 @@ 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;
@ -16,7 +17,6 @@ import org.hibernate.query.criteria.JpaJsonValueExpression;
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.SelfRenderingSqmFunction;
import org.hibernate.query.sqm.function.SqmFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver;
@ -25,6 +25,7 @@ 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.JsonValueEmptyBehavior;
import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior;
@ -36,7 +37,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* @since 7.0
*/
@Incubating
public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> implements JpaJsonValueExpression<T> {
public class SqmJsonValueExpression<T> extends AbstractSqmJsonPathExpression<T> implements JpaJsonValueExpression<T> {
private @Nullable ErrorBehavior errorBehavior;
private SqmExpression<T> errorDefaultExpression;
private @Nullable EmptyBehavior emptyBehavior;
@ -72,6 +73,7 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
FunctionReturnTypeResolver returnTypeResolver,
NodeBuilder nodeBuilder,
String name,
@Nullable Map<String, SqmExpression<?>> passingExpressions,
@Nullable ErrorBehavior errorBehavior,
SqmExpression<T> errorDefaultExpression,
@Nullable EmptyBehavior emptyBehavior,
@ -84,7 +86,8 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
argumentsValidator,
returnTypeResolver,
nodeBuilder,
name
name,
passingExpressions
);
this.errorBehavior = errorBehavior;
this.errorDefaultExpression = errorDefaultExpression;
@ -112,6 +115,7 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
getReturnTypeResolver(),
nodeBuilder(),
getFunctionName(),
copyPassingExpressions( context ),
errorBehavior,
errorDefaultExpression == null ? null : errorDefaultExpression.copy( context ),
emptyBehavior,
@ -141,28 +145,28 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
}
@Override
public JpaJsonValueExpression<T> unspecifiedOnError() {
public SqmJsonValueExpression<T> unspecifiedOnError() {
this.errorBehavior = ErrorBehavior.UNSPECIFIED;
this.errorDefaultExpression = null;
return this;
}
@Override
public JpaJsonValueExpression<T> errorOnError() {
public SqmJsonValueExpression<T> errorOnError() {
this.errorBehavior = ErrorBehavior.ERROR;
this.errorDefaultExpression = null;
return this;
}
@Override
public JpaJsonValueExpression<T> nullOnError() {
public SqmJsonValueExpression<T> nullOnError() {
this.errorBehavior = ErrorBehavior.NULL;
this.errorDefaultExpression = null;
return this;
}
@Override
public JpaJsonValueExpression<T> defaultOnError(jakarta.persistence.criteria.Expression<?> expression) {
public SqmJsonValueExpression<T> defaultOnError(jakarta.persistence.criteria.Expression<?> expression) {
this.errorBehavior = ErrorBehavior.DEFAULT;
//noinspection unchecked
this.errorDefaultExpression = (SqmExpression<T>) expression;
@ -170,34 +174,42 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
}
@Override
public JpaJsonValueExpression<T> unspecifiedOnEmpty() {
public SqmJsonValueExpression<T> unspecifiedOnEmpty() {
this.errorBehavior = ErrorBehavior.UNSPECIFIED;
this.errorDefaultExpression = null;
return this;
}
@Override
public JpaJsonValueExpression<T> errorOnEmpty() {
public SqmJsonValueExpression<T> errorOnEmpty() {
this.emptyBehavior = EmptyBehavior.ERROR;
this.emptyDefaultExpression = null;
return this;
}
@Override
public JpaJsonValueExpression<T> nullOnEmpty() {
public SqmJsonValueExpression<T> nullOnEmpty() {
this.emptyBehavior = EmptyBehavior.NULL;
this.emptyDefaultExpression = null;
return this;
}
@Override
public JpaJsonValueExpression<T> defaultOnEmpty(jakarta.persistence.criteria.Expression<?> expression) {
public SqmJsonValueExpression<T> defaultOnEmpty(jakarta.persistence.criteria.Expression<?> expression) {
this.emptyBehavior = EmptyBehavior.DEFAULT;
//noinspection unchecked
this.emptyDefaultExpression = (SqmExpression<T>) expression;
return this;
}
@Override
public SqmJsonValueExpression<T> 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 );
@ -206,6 +218,10 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
if ( validator != null ) {
validator.validateSqlTypes( arguments, getFunctionName() );
}
final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker );
if ( jsonPathPassingClause != null ) {
arguments.add( jsonPathPassingClause );
}
if ( errorBehavior != null ) {
switch ( errorBehavior ) {
case NULL:
@ -252,6 +268,7 @@ public class SqmJsonValueExpression<T> extends SelfRenderingSqmFunction<T> imple
sb.append( ',' );
getArguments().get( 1 ).appendHqlString( sb );
appendPassingExpressionHqlString( sb );
if ( getArguments().size() > 2 ) {
sb.append( " returning " );
getArguments().get( 2 ).appendHqlString( sb );

View File

@ -4,7 +4,7 @@
*/
package org.hibernate.query.sqm.tree.expression;
import org.hibernate.query.internal.QueryLiteralHelper;
import org.hibernate.internal.util.QuotingHelper;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SemanticQueryWalker;
import org.hibernate.query.sqm.SqmExpressible;
@ -82,7 +82,7 @@ public class SqmLiteral<T> extends AbstractSqmExpression<T> {
else {
final String string = javaType.toString( value );
if ( javaType.getJavaTypeClass() == String.class ) {
QueryLiteralHelper.appendStringLiteral( sb, string );
QuotingHelper.appendSingleQuoteEscapedString( sb, string );
}
else {
sb.append( string );

View File

@ -38,6 +38,7 @@ import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.FilterJdbcParameter;
import org.hibernate.internal.util.MathHelper;
import org.hibernate.internal.util.QuotingHelper;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.internal.util.collections.Stack;
@ -119,6 +120,7 @@ import org.hibernate.sql.ast.tree.expression.Every;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.ExtractUnit;
import org.hibernate.sql.ast.tree.expression.Format;
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
import org.hibernate.sql.ast.tree.expression.JdbcLiteral;
import org.hibernate.sql.ast.tree.expression.JdbcParameter;
import org.hibernate.sql.ast.tree.expression.Literal;
@ -548,6 +550,16 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
sqlBuffer.append( value );
}
@Override
public void appendDoubleQuoteEscapedString(String value) {
QuotingHelper.appendDoubleQuoteEscapedString( sqlBuffer, value );
}
@Override
public void appendSingleQuoteEscapedString(String value) {
QuotingHelper.appendSingleQuoteEscapedString( sqlBuffer, value );
}
@Override
public Appendable append(CharSequence csq) {
sqlBuffer.append( csq );
@ -680,6 +692,20 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
}
return (R) getParameterBindValue( (JdbcParameter) ( (SqmParameterInterpretation) expression).getResolvedExpression() );
}
else if ( expression instanceof FunctionExpression functionExpression ) {
if ( "concat".equals( functionExpression.getFunctionName() ) ) {
final List<? extends SqlAstNode> arguments = functionExpression.getArguments();
final StringBuilder sb = new StringBuilder();
for ( SqlAstNode argument : arguments ) {
final Object argumentLiteral = interpretExpression( (Expression) argument, jdbcParameterBindings );
if ( argumentLiteral == null ) {
return null;
}
sb.append( argumentLiteral );
}
return (R) sb.toString();
}
}
throw new UnsupportedOperationException( "Can't interpret expression: " + expression );
}

View File

@ -4,6 +4,8 @@
*/
package org.hibernate.sql.ast.spi;
import org.hibernate.internal.util.QuotingHelper;
/**
* Access to appending SQL fragments to an in-flight buffer
*
@ -44,6 +46,18 @@ public interface SqlAppender extends Appendable {
appendSql( String.valueOf( value ) );
}
default void appendDoubleQuoteEscapedString(String value) {
final StringBuilder sb = new StringBuilder( value.length() + 2 );
QuotingHelper.appendDoubleQuoteEscapedString( sb, value );
appendSql( sb.toString() );
}
default void appendSingleQuoteEscapedString(String value) {
final StringBuilder sb = new StringBuilder( value.length() + 2 );
QuotingHelper.appendSingleQuoteEscapedString( sb, value );
appendSql( sb.toString() );
}
default Appendable append(CharSequence csq) {
appendSql( csq.toString() );
return this;

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 JsonExistsErrorBehavior implements SqlAstNode {
TRUE,
FALSE,
ERROR;
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("JsonExistsErrorBehavior doesn't support walking");
}
}

View File

@ -0,0 +1,34 @@
/*
* 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 java.util.Map;
import org.hibernate.sql.ast.SqlAstWalker;
import org.hibernate.sql.ast.tree.SqlAstNode;
/**
* @since 7.0
*/
public class JsonPathPassingClause implements SqlAstNode {
private final Map<String, Expression> passingExpressions;
public JsonPathPassingClause(Map<String, Expression> passingExpressions) {
this.passingExpressions = passingExpressions;
}
public Map<String, Expression> getPassingExpressions() {
return passingExpressions;
}
@Override
public void accept(SqlAstWalker sqlTreeWalker) {
throw new UnsupportedOperationException("JsonPathPassingClause doesn't support walking");
}
}

View File

@ -0,0 +1,102 @@
/*
* 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 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.SupportsJsonExists.class)
public class JsonExistsTest {
@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-exists-example[]
List<Boolean> results = em.createQuery( "select json_exists(e.json, '$.theString') from EntityWithJson e", Boolean.class )
.getResultList();
//end::hql-json-exists-example[]
assertEquals( 1, results.size() );
} );
}
@Test
public void testPassing(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-json-exists-passing-example[]
List<Boolean> results = em.createQuery( "select json_exists(e.json, '$.theArray[$idx]' passing 1 as idx) from EntityWithJson e", Boolean.class )
.getResultList();
//end::hql-json-exists-passing-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-exists-on-error-example[]
em.createQuery( "select json_exists('invalidJson', '$.theInt' error on error) from EntityWithJson e")
.getResultList();
//end::hql-json-exists-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;
}
}
} );
}
}

View File

@ -71,6 +71,17 @@ public class JsonValueTest {
} );
}
@Test
public void testPassing(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-json-value-passing-example[]
List<Tuple> results = em.createQuery( "select json_value(e.json, '$.theArray[$idx]' passing 1 as idx) from EntityWithJson e", Tuple.class )
.getResultList();
//end::hql-json-value-passing-example[]
assertEquals( 1, results.size() );
} );
}
@Test
public void testReturning(SessionFactoryScope scope) {
scope.inSession( em -> {
@ -91,7 +102,7 @@ public class JsonValueTest {
em.createQuery( "select json_value('invalidJson', '$.theInt' error on error) from EntityWithJson e")
.getResultList();
//end::hql-json-value-on-error-example[]
fail("error clause should fail because of invalid json path");
fail("error clause should fail because of invalid json document");
}
catch ( HibernateException e ) {
if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) {

View File

@ -6,7 +6,6 @@
*/
package org.hibernate.orm.test.query.hql;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -101,6 +100,20 @@ public class JsonFunctionTests {
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonValue.class)
public void testJsonValueExpression(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Tuple tuple = session.createQuery(
"select json_value('{\"theArray\":[1,10]}', '$.theArray[$idx]' passing :idx as idx) ",
Tuple.class
).setParameter( "idx", 0 ).getSingleResult();
assertEquals( "1", tuple.get( 0 ) );
}
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArray.class)
public void testJsonArray(SessionFactoryScope scope) {
@ -209,6 +222,29 @@ public class JsonFunctionTests {
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonExists.class)
public void testJsonExists(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
Tuple tuple = session.createQuery(
"select " +
"json_exists(e.json, '$.theUnknown'), " +
"json_exists(e.json, '$.theInt'), " +
"json_exists(e.json, '$.theArray[0]'), " +
"json_exists(e.json, '$.theArray[$idx]' passing :idx as idx) " +
"from JsonHolder e " +
"where e.id = 1L",
Tuple.class
).setParameter( "idx", 3 ).getSingleResult();
assertEquals( false, tuple.get( 0 ) );
assertEquals( true, tuple.get( 1 ) );
assertEquals( true, tuple.get( 2 ) );
assertEquals( false, tuple.get( 3 ) );
}
);
}
private static final ObjectMapper MAPPER = new ObjectMapper();
private static Map<String, Object> parseObject(String json) {
@ -230,22 +266,6 @@ public class JsonFunctionTests {
}
}
private static double[] parseDoubleArray( String s ) {
final List<Double> list = new ArrayList<>();
int startIndex = 1;
int commaIndex;
while ( (commaIndex = s.indexOf(',', startIndex)) != -1 ) {
list.add( Double.parseDouble( s.substring( startIndex, commaIndex ) ) );
startIndex = commaIndex + 1;
}
list.add( Double.parseDouble( s.substring( startIndex, s.length() - 1 ) ) );
double[] array = new double[list.size()];
for ( int i = 0; i < list.size(); i++ ) {
array[i] = list.get( i );
}
return array;
}
@Entity(name = "JsonHolder")
public static class JsonHolder {
@Id

View File

@ -30,8 +30,8 @@ import org.hibernate.envers.query.criteria.AuditProperty;
import org.hibernate.envers.query.criteria.internal.CriteriaTools;
import org.hibernate.envers.query.order.NullPrecedence;
import org.hibernate.envers.tools.Pair;
import org.hibernate.internal.util.QuotingHelper;
import org.hibernate.query.Query;
import org.hibernate.query.internal.QueryLiteralHelper;
import org.hibernate.type.BasicType;
/**
@ -365,10 +365,10 @@ public class QueryBuilder {
final Pair<String, String> fragment = fragmentIterator.next();
sb.append( OrderByFragmentFunction.FUNCTION_NAME ).append( '(' );
// The first argument is the sqm alias of the from node
QueryLiteralHelper.appendStringLiteral( sb, fragment.getFirst() );
QuotingHelper.appendSingleQuoteEscapedString( sb, fragment.getFirst() );
sb.append( ", " );
// The second argument is the collection role that contains the order by fragment
QueryLiteralHelper.appendStringLiteral( sb, fragment.getSecond() );
QuotingHelper.appendSingleQuoteEscapedString( sb, fragment.getSecond() );
sb.append( ')' );
if ( fragmentIterator.hasNext() ) {
sb.append( ", " );

View File

@ -732,6 +732,12 @@ abstract public class DialectFeatureChecks {
}
}
public static class SupportsJsonExists implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "json_exists" );
}
}
public static class SupportsJsonArray implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "json_array" );