From 6454aaf05570d879a7196cb8672499c10c4158d7 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 22 Aug 2024 15:24:02 +0200 Subject: [PATCH] HHH-18496 Add json_exists and support the passing clause --- .../chapters/query/hql/QueryLanguage.adoc | 89 +++++++- .../query/hql/extras/json_exists_bnf.txt | 7 + .../query/hql/extras/json_value_bnf.txt | 5 +- .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/DB2LegacyDialect.java | 3 +- .../community/dialect/H2LegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 3 +- .../dialect/PostgreSQLLegacyDialect.java | 2 + .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 2 + .../org/hibernate/grammars/hql/HqlParser.g4 | 23 +- .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/DB2Dialect.java | 3 +- .../java/org/hibernate/dialect/Dialect.java | 10 +- .../java/org/hibernate/dialect/H2Dialect.java | 1 + .../org/hibernate/dialect/HANADialect.java | 3 +- .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 3 +- .../hibernate/dialect/PostgreSQLDialect.java | 2 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 77 ++++++- .../json/CockroachDBJsonValueFunction.java | 19 +- .../function/json/H2JsonExistsFunction.java | 54 +++++ .../function/json/H2JsonValueFunction.java | 63 ++++-- .../function/json/HANAJsonExistsFunction.java | 61 ++++++ .../function/json/HANAJsonObjectFunction.java | 10 +- .../function/json/JsonExistsFunction.java | 190 ++++++++++++++++ .../dialect/function/json/JsonPathHelper.java | 143 +++++++++++- .../function/json/JsonValueFunction.java | 57 ++++- .../json/MariaDBJsonValueFunction.java | 15 +- .../json/MySQLJsonExistsFunction.java | 46 ++++ .../function/json/MySQLJsonValueFunction.java | 15 +- .../json/PostgreSQLJsonExistsFunction.java | 52 +++++ .../json/PostgreSQLJsonValueFunction.java | 19 +- .../json/SQLServerJsonExistsFunction.java | 88 ++++++++ .../json/SQLServerJsonValueFunction.java | 32 ++- .../internal/util/QuotingHelper.java | 23 ++ .../criteria/HibernateCriteriaBuilder.java | 20 +- .../criteria/JpaJsonExistsExpression.java | 79 +++++++ .../criteria/JpaJsonValueExpression.java | 7 + .../spi/HibernateCriteriaBuilderDelegate.java | 13 ++ .../hql/internal/SemanticQueryBuilder.java | 45 ++++ .../query/internal/QueryLiteralHelper.java | 33 --- .../org/hibernate/query/sqm/NodeBuilder.java | 8 + .../sqm/function/SqmFunctionDescriptor.java | 9 + .../sqm/internal/SqmCriteriaNodeBuilder.java | 15 ++ .../sqm/sql/BaseSqmToSqlAstConverter.java | 36 ++- .../AbstractSqmJsonPathExpression.java | 128 +++++++++++ .../expression/SqmJsonExistsExpression.java | 207 ++++++++++++++++++ .../expression/SqmJsonValueExpression.java | 39 +++- .../query/sqm/tree/expression/SqmLiteral.java | 4 +- .../sql/ast/spi/AbstractSqlAstTranslator.java | 26 +++ .../hibernate/sql/ast/spi/SqlAppender.java | 14 ++ .../expression/JsonExistsErrorBehavior.java | 25 +++ .../expression/JsonPathPassingClause.java | 34 +++ .../test/function/json/JsonExistsTest.java | 102 +++++++++ .../orm/test/function/json/JsonValueTest.java | 13 +- .../orm/test/query/hql/JsonFunctionTests.java | 54 +++-- .../internal/tools/query/QueryBuilder.java | 6 +- .../orm/junit/DialectFeatureChecks.java | 6 + 61 files changed, 1892 insertions(+), 158 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java delete mode 100644 hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 4821b60eb7..8232f46e73 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -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 <> 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 diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt new file mode 100644 index 0000000000..e91cab8b3d --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt @@ -0,0 +1,7 @@ +"json_exists(" expression, expression passingClause? onErrorClause? ")" + +passingClause + : "passing" expression "as" identifier ("," expression "as" identifier)* + +onErrorClause + : ( "error" | "true" | "false" ) "on error"; diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt index b5ea83bf62..c6f90535c6 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt @@ -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"; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 24ac28d528..23a95630ff 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -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 diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 2104ab9376..0f5f498351 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -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(); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index a7bb804c4a..dd8d289101 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -404,6 +404,7 @@ public class H2LegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_h2(); + functionFactory.jsonExists_h2(); } } else { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 1ac0fb0105..2368fdd8a0 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -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(); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 6b3e67573d..d35245b3dd 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -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(); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 7d161d0bb2..a8ba3cc6c1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -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(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index b430654829..51e6a48578 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -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(); } diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index d5e8d404f0..8162269e75 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -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]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index a8caeb932f..1e63f6a461 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -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 diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 3632998ce4..972a2fe880 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -469,6 +469,7 @@ public class CockroachDialect extends Dialect { functionFactory.arrayToString_postgresql(); functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonExists_postgresql(); functionFactory.jsonObject_postgresql(); functionFactory.jsonArray_postgresql(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index ec381f1ff5..58849be53a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -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(); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index df858fedeb..ad9d1f0a6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -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 ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 45086a0660..cd61595ae2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -347,6 +347,7 @@ public class H2Dialect extends Dialect { functionFactory.jsonArray(); if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_h2(); + functionFactory.jsonExists_h2(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java index ab7a720ec6..e8e1bbd320 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -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(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 1a0a36aa12..1d81a805f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -639,6 +639,7 @@ public class MySQLDialect extends Dialect { functionFactory.listagg_groupConcat(); functionFactory.jsonValue_mysql(); + functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 4469fe806a..4978c4918b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -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(); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 17bbd9d77a..27c57c1f47 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -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(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 1e7d8cb191..4a0df29245 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -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(); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 7cde6bb7b4..0194cc6952 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -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 */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java index ec61a58062..ada19353b3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java @@ -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() ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java new file mode 100644 index 0000000000..12fdc9d55b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java @@ -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" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java index 9ded156b7f..1777a23756 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -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 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( '"' ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java new file mode 100644 index 0000000000..6ce661e428 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java @@ -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" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java index 556b372139..1c971e830b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java @@ -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'" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java new file mode 100644 index 0000000000..83497b09f6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java @@ -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 SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + //noinspection unchecked + return (SelfRenderingSqmFunction) new SqmJsonExistsExpression( + this, + this, + arguments, + (ReturnableType) impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List 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 passingExpressions = passingClause.getPassingExpressions(); + final Iterator> iterator = passingExpressions.entrySet().iterator(); + Map.Entry 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 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 + ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java index 0eaa449524..743b9af997 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -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 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 {} } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java index 1414514161..88706ce9e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -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 passingExpressions = passingClause.getPassingExpressions(); + final Iterator> iterator = passingExpressions.entrySet().iterator(); + Map.Entry 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 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 diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java index fbea183386..c144dee92a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java @@ -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 " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java new file mode 100644 index 0000000000..f7e71abd3c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java @@ -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( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java index 2586d6d6d6..138c536ce1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java @@ -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 " ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java new file mode 100644 index 0000000000..aa193c6055 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java @@ -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 entry : passingClause.getPassingExpressions().entrySet() ) { + sqlAppender.append( separator ); + sqlAppender.appendSingleQuoteEscapedString( entry.getKey() ); + sqlAppender.append( ',' ); + entry.getValue().accept( walker ); + separator = ','; + } + sqlAppender.append( ')' ); + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java index acbf1d2e0d..f6b7f8b63a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -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 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 ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java new file mode 100644 index 0000000000..ca851ca4e6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java @@ -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)" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java index ec57b30944..687fbaaaa1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java @@ -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 { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java index 472613b414..9153315a79 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java @@ -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 ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 5f99a2a4ac..3c13abb6bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -3691,7 +3691,7 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { JpaJsonValueExpression 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 jsonValue(Expression jsonDocument, Expression 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 JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath, Class 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 jsonPath); + /** * Create a JSON object from the given map of key values. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java new file mode 100644 index 0000000000..77186f7e7d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java @@ -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 . + */ +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 { + /** + * 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 + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java index b35c2a9772..87e3991621 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java @@ -97,6 +97,13 @@ public interface JpaJsonValueExpression extends JpaExpression { */ JpaJsonValueExpression 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 passing(String parameterName, Expression expression); + /** * The behavior of the json value expression when a JSON processing error occurs. */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index fd08ae1f21..ea53f3e79f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -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 jsonPath) { + return criteriaBuilder.jsonExists( jsonDocument, jsonPath ); + } + @Override @Incubating public JpaExpression jsonObject(Map> keyValues) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 7db1d6e0b0..6c909bb5f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -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 extends HqlParserBaseVisitor implem } } } + final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause(); + if ( passingClause != null ) { + final List expressionContexts = passingClause.expressionOrPredicate(); + final List 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" ).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 expressionContexts = passingClause.expressionOrPredicate(); + final List 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(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java deleted file mode 100644 index 190524908e..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java +++ /dev/null @@ -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( '\'' ); - } - -} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 2ed0cd683a..3182976459 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -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 jsonValue(Expression jsonDocument, String jsonPath); + @Override + SqmJsonExistsExpression jsonExists(Expression jsonDocument, Expression jsonPath); + + @Override + SqmJsonExistsExpression jsonExists(Expression jsonDocument, String jsonPath); + @Override SqmExpression jsonArrayWithNulls(Expression... values); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java index 1092f73aba..926b9ec812 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java @@ -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; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 3e35140ffe..6c886c204f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -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 jsonPath) { + return (SqmJsonExistsExpression) getFunctionDescriptor( "json_exists" ).generateSqmExpression( + asList( (SqmTypedNode) jsonDocument, (SqmTypedNode) jsonPath ), + null, + queryEngine + ); + } + @Override public SqmExpression jsonArrayWithNulls(Expression... values) { final var arguments = new ArrayList>( values.length + 1 ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 5212a591a5..7a9854b87e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -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 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 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 extends Base ); } - private JdbcMappingContainer getBooleanType() { + private BasicType 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 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 ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java new file mode 100644 index 0000000000..72af4fa2a3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java @@ -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 . + */ +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 extends SelfRenderingSqmFunction { + + private @Nullable Map> passingExpressions; + + public AbstractSqmJsonPathExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType 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> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name, + @Nullable Map> passingExpressions) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + this.passingExpressions = passingExpressions; + } + + public Map> 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> copyPassingExpressions(SqmCopyContext context) { + if ( passingExpressions == null ) { + return null; + } + final HashMap> copy = new HashMap<>( passingExpressions.size() ); + for ( Map.Entry> 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 converted = new HashMap<>( passingExpressions.size() ); + for ( Map.Entry> 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> entry : passingExpressions.entrySet() ) { + entry.getValue().appendHqlString( sb ); + sb.append( " as " ); + QuotingHelper.appendDoubleQuoteEscapedString( sb, entry.getKey() ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java new file mode 100644 index 0000000000..32e29fd492 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java @@ -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 . + */ +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 implements JpaJsonExistsExpression { + private @Nullable ErrorBehavior errorBehavior; + + public SqmJsonExistsExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType 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> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name, + @Nullable Map> 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> 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 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( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java index 5a7b561bef..7b66ec67ee 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java @@ -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 extends SelfRenderingSqmFunction implements JpaJsonValueExpression { +public class SqmJsonValueExpression extends AbstractSqmJsonPathExpression implements JpaJsonValueExpression { private @Nullable ErrorBehavior errorBehavior; private SqmExpression errorDefaultExpression; private @Nullable EmptyBehavior emptyBehavior; @@ -72,6 +73,7 @@ public class SqmJsonValueExpression extends SelfRenderingSqmFunction imple FunctionReturnTypeResolver returnTypeResolver, NodeBuilder nodeBuilder, String name, + @Nullable Map> passingExpressions, @Nullable ErrorBehavior errorBehavior, SqmExpression errorDefaultExpression, @Nullable EmptyBehavior emptyBehavior, @@ -84,7 +86,8 @@ public class SqmJsonValueExpression extends SelfRenderingSqmFunction imple argumentsValidator, returnTypeResolver, nodeBuilder, - name + name, + passingExpressions ); this.errorBehavior = errorBehavior; this.errorDefaultExpression = errorDefaultExpression; @@ -112,6 +115,7 @@ public class SqmJsonValueExpression extends SelfRenderingSqmFunction imple getReturnTypeResolver(), nodeBuilder(), getFunctionName(), + copyPassingExpressions( context ), errorBehavior, errorDefaultExpression == null ? null : errorDefaultExpression.copy( context ), emptyBehavior, @@ -141,28 +145,28 @@ public class SqmJsonValueExpression extends SelfRenderingSqmFunction imple } @Override - public JpaJsonValueExpression unspecifiedOnError() { + public SqmJsonValueExpression unspecifiedOnError() { this.errorBehavior = ErrorBehavior.UNSPECIFIED; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression errorOnError() { + public SqmJsonValueExpression errorOnError() { this.errorBehavior = ErrorBehavior.ERROR; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression nullOnError() { + public SqmJsonValueExpression nullOnError() { this.errorBehavior = ErrorBehavior.NULL; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression defaultOnError(jakarta.persistence.criteria.Expression expression) { + public SqmJsonValueExpression defaultOnError(jakarta.persistence.criteria.Expression expression) { this.errorBehavior = ErrorBehavior.DEFAULT; //noinspection unchecked this.errorDefaultExpression = (SqmExpression) expression; @@ -170,34 +174,42 @@ public class SqmJsonValueExpression extends SelfRenderingSqmFunction imple } @Override - public JpaJsonValueExpression unspecifiedOnEmpty() { + public SqmJsonValueExpression unspecifiedOnEmpty() { this.errorBehavior = ErrorBehavior.UNSPECIFIED; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression errorOnEmpty() { + public SqmJsonValueExpression errorOnEmpty() { this.emptyBehavior = EmptyBehavior.ERROR; this.emptyDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression nullOnEmpty() { + public SqmJsonValueExpression nullOnEmpty() { this.emptyBehavior = EmptyBehavior.NULL; this.emptyDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression defaultOnEmpty(jakarta.persistence.criteria.Expression expression) { + public SqmJsonValueExpression defaultOnEmpty(jakarta.persistence.criteria.Expression expression) { this.emptyBehavior = EmptyBehavior.DEFAULT; //noinspection unchecked this.emptyDefaultExpression = (SqmExpression) expression; return this; } + @Override + public SqmJsonValueExpression 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 extends SelfRenderingSqmFunction 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 extends SelfRenderingSqmFunction imple sb.append( ',' ); getArguments().get( 1 ).appendHqlString( sb ); + appendPassingExpressionHqlString( sb ); if ( getArguments().size() > 2 ) { sb.append( " returning " ); getArguments().get( 2 ).appendHqlString( sb ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java index 737897c096..7dac8205b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java @@ -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 extends AbstractSqmExpression { else { final String string = javaType.toString( value ); if ( javaType.getJavaTypeClass() == String.class ) { - QueryLiteralHelper.appendStringLiteral( sb, string ); + QuotingHelper.appendSingleQuoteEscapedString( sb, string ); } else { sb.append( string ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index f5e0716828..c8cb37528b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -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 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 implemen } return (R) getParameterBindValue( (JdbcParameter) ( (SqmParameterInterpretation) expression).getResolvedExpression() ); } + else if ( expression instanceof FunctionExpression functionExpression ) { + if ( "concat".equals( functionExpression.getFunctionName() ) ) { + final List 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 ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java index 14ea197660..215e21e78a 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java @@ -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; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java new file mode 100644 index 0000000000..ba2f8a7990 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java @@ -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"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java new file mode 100644 index 0000000000..c77ef12a48 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java @@ -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 passingExpressions; + + public JsonPathPassingClause(Map passingExpressions) { + this.passingExpressions = passingExpressions; + } + + public Map getPassingExpressions() { + return passingExpressions; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonPathPassingClause doesn't support walking"); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java new file mode 100644 index 0000000000..d35207e9df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java @@ -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 . + */ +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 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 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; + } + } + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java index c532b025fe..14929dc4b7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java @@ -71,6 +71,17 @@ public class JsonValueTest { } ); } + @Test + public void testPassing(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-value-passing-example[] + List 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 ) ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 331c7aa64a..337dc55d15 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -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 parseObject(String json) { @@ -230,22 +266,6 @@ public class JsonFunctionTests { } } - private static double[] parseDoubleArray( String s ) { - final List 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 diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java index 14f684c11c..50d16363d8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java @@ -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 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( ", " ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index d3cb459782..4f82a757e6 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -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" );