Skip to content

Commit

Permalink
HHH-17375 Introduce array_includes() and INCLUDES predicate for check…
Browse files Browse the repository at this point in the history
…ing if array contains all elements of subarray as replacement to array_contains() overload
  • Loading branch information
beikov committed May 13, 2024
1 parent dcedc5c commit c8aa4f3
Show file tree
Hide file tree
Showing 26 changed files with 897 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1189,8 +1189,10 @@ The following functions deal with SQL array types, which are not supported on ev
| `array_concat()` | Concatenates array with each other in order
| `array_prepend()` | Prepends element to array
| `array_append()` | Appends element to array
| `array_contains()` | Whether an array contains element(s)
| `array_contains_nullable()` | Whether an array contains element(s), supporting `null` elements
| `array_contains()` | Whether an array contains an element
| `array_contains_nullable()` | Whether an array contains an element, supporting `null` element
| `array_includes()` | Whether an array contains another array
| `array_includes_nullable()` | Whether an array contains another array, supporting `null` elements
| `array_intersects()` | Whether an array holds at least one element of another array
| `array_intersects_nullable()` | Whether an array holds at least one element of another array, supporting `null` elements
| `array_get()` | Accesses the element of an array by index
Expand Down Expand Up @@ -1367,7 +1369,7 @@ include::{array-example-dir-hql}/ArrayAppendTest.java[tags=hql-array-append-exam
[[hql-array-contains-functions]]
===== `array_contains()` and `array_contains_nullable()`
Checks if the first array argument contains the element(s) of the second argument.
Checks if the first array argument contains the element represented by the second argument.
Returns `null` if the first argument is `null`. The result of the `array_contains` function
is undefined when the second argument, the element to search, is `null`.
Expand All @@ -1379,29 +1381,8 @@ include::{array-example-dir-hql}/ArrayContainsTest.java[tags=hql-array-contains-
----
====
The second argument may be an array of the same type as the first argument.
The result of `array_contains` is undefined when the second argument is an array that contains a `null` element.
[[hql-array-contains-array-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArrayContainsArrayTest.java[tags=hql-array-contains-array-example]
----
====
To search for `null` elements, the `array_contains_nullable` function must be used with an array argument.
[[hql-array-contains-array-nullable-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArrayContainsArrayTest.java[tags=hql-array-contains-array-nullable-example]
----
====
Alternatively, it's also possible to check for containment with the `contains` predicate,
where the left hand side of the predicate is the array and the right hand side the value(s) to check.
where the left hand side of the predicate is the array and the right hand side the value to check.
This is syntax sugar that translates to the `array_contains` function.
[[hql-array-contains-hql-example]]
Expand All @@ -1412,29 +1393,42 @@ include::{array-example-dir-hql}/ArrayContainsTest.java[tags=hql-array-contains-
----
====
[[hql-array-contains-array-hql-example]]
[[hql-array-includes-functions]]
===== `array_includes()` and `array_includes_nullable()`
Checks if the first array argument contains the elements of the second array argument.
Returns `null` if the first argument is `null`. The result of the `array_includes` function
is undefined when the second argument contains a `null`.
[[hql-array-contains-array-example]]
[[hql-array-includes-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArrayContainsArrayTest.java[tags=hql-array-contains-array-hql-example]
include::{array-example-dir-hql}/ArrayIncludesTest.java[tags=hql-array-includes-example]
----
====
In addition to that, it is also possible to use the `in` predicate with arrays.
To search for `null` elements, the `array_includes_nullable` function must be used.
[[hql-array-in-hql-example]]
[[hql-array-contains-array-nullable-example]]
[[hql-array-includes-nullable-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArrayContainsTest.java[tags=hql-array-in-hql-example]
include::{array-example-dir-hql}/ArrayIncludesTest.java[tags=hql-array-includes-nullable-example]
----
====
[[hql-array-in-array-hql-example]]
Alternatively, it's also possible to use the `includes` predicate,
where the left hand side of the predicate is the array and the right hand side the array of values to check.
This is syntax sugar that translates to the `array_includes` function.
[[hql-array-includes-hql-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArrayContainsArrayTest.java[tags=hql-array-in-array-hql-example]
include::{array-example-dir-hql}/ArrayIncludesTest.java[tags=hql-array-includes-hql-example]
----
====
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ HOUR : [hH] [oO] [uU] [rR];
IGNORE : [iI] [gG] [nN] [oO] [rR] [eE];
ILIKE : [iI] [lL] [iI] [kK] [eE];
IN : [iI] [nN];
INCLUDES : [iI] [nN] [cC] [lL] [uU] [dD] [eE] [sS];
INDEX : [iI] [nN] [dD] [eE] [xX];
INDICES : [iI] [nN] [dD] [iI] [cC] [eE] [sS];
INNER : [iI] [nN] [nN] [eE] [rR];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,7 @@ predicate
| expression NOT? BETWEEN expression AND expression # BetweenPredicate
| expression NOT? (LIKE | ILIKE) expression likeEscape? # LikePredicate
| expression NOT? CONTAINS expression # ContainsPredicate
| expression NOT? INCLUDES expression # IncludesPredicate
| expression NOT? INTERSECTS expression # IntersectsPredicate
| expression comparisonOperator expression # ComparisonPredicate
| EXISTS collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN # ExistsCollectionPartPredicate
Expand Down Expand Up @@ -1703,6 +1704,7 @@ rollup
| ILIKE
| IN
| INDEX
| INCLUDES
| INDICES
// | INNER
| INSERT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public String[] getSqlCreateStrings(
"return arr.count; " +
"end;",
createOrReplaceConcatFunction( arrayTypeName ),
"create or replace function " + arrayTypeName + "_contains(haystack in " + arrayTypeName +
"create or replace function " + arrayTypeName + "_includes(haystack in " + arrayTypeName +
", needle in " + arrayTypeName + ", nullable in number) return number deterministic is found number(1,0); begin " +
"if haystack is null or needle is null then return null; end if; " +
"for i in 1 .. needle.count loop " +
Expand Down Expand Up @@ -295,7 +295,7 @@ public String[] getSqlDropStrings(UserDefinedArrayType userDefinedType, Metadata
"drop function " + arrayTypeName + "_position",
"drop function " + arrayTypeName + "_length",
"drop function " + arrayTypeName + "_concat",
"drop function " + arrayTypeName + "_contains",
"drop function " + arrayTypeName + "_includes",
"drop function " + arrayTypeName + "_intersects",
"drop function " + arrayTypeName + "_get",
"drop function " + arrayTypeName + "_set",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.hibernate.dialect.function.array.ArrayConstructorFunction;
import org.hibernate.dialect.function.array.ArrayContainsOperatorFunction;
import org.hibernate.dialect.function.array.ArrayContainsUnnestFunction;
import org.hibernate.dialect.function.array.ArrayIncludesOperatorFunction;
import org.hibernate.dialect.function.array.ArrayIncludesUnnestFunction;
import org.hibernate.dialect.function.array.ArrayIntersectsOperatorFunction;
import org.hibernate.dialect.function.array.ArrayIntersectsUnnestFunction;
import org.hibernate.dialect.function.array.ArrayGetUnnestFunction;
Expand All @@ -34,6 +36,7 @@
import org.hibernate.dialect.function.array.ElementViaArrayArgumentReturnTypeResolver;
import org.hibernate.dialect.function.array.H2ArrayContainsFunction;
import org.hibernate.dialect.function.array.H2ArrayFillFunction;
import org.hibernate.dialect.function.array.H2ArrayIncludesFunction;
import org.hibernate.dialect.function.array.H2ArrayIntersectsFunction;
import org.hibernate.dialect.function.array.H2ArrayPositionFunction;
import org.hibernate.dialect.function.array.H2ArrayPositionsFunction;
Expand All @@ -52,6 +55,7 @@
import org.hibernate.dialect.function.array.OracleArrayConcatElementFunction;
import org.hibernate.dialect.function.array.OracleArrayConcatFunction;
import org.hibernate.dialect.function.array.OracleArrayFillFunction;
import org.hibernate.dialect.function.array.OracleArrayIncludesFunction;
import org.hibernate.dialect.function.array.OracleArrayIntersectsFunction;
import org.hibernate.dialect.function.array.OracleArrayGetFunction;
import org.hibernate.dialect.function.array.OracleArrayLengthFunction;
Expand Down Expand Up @@ -2681,6 +2685,14 @@ public void arrayContains_h2(int maximumArraySize) {
"array_contains_nullable",
new H2ArrayContainsFunction( true, maximumArraySize, typeConfiguration )
);
functionRegistry.register(
"array_includes",
new H2ArrayIncludesFunction( false, maximumArraySize, typeConfiguration )
);
functionRegistry.register(
"array_includes_nullable",
new H2ArrayIncludesFunction( true, maximumArraySize, typeConfiguration )
);
}

/**
Expand All @@ -2695,6 +2707,14 @@ public void arrayContains_hsql() {
"array_contains_nullable",
new ArrayContainsUnnestFunction( true, typeConfiguration )
);
functionRegistry.register(
"array_includes",
new ArrayIncludesUnnestFunction( false, typeConfiguration )
);
functionRegistry.register(
"array_includes_nullable",
new ArrayIncludesUnnestFunction( true, typeConfiguration )
);
}

/**
Expand All @@ -2703,6 +2723,8 @@ public void arrayContains_hsql() {
public void arrayContains_postgresql() {
functionRegistry.register( "array_contains", new ArrayContainsOperatorFunction( false, typeConfiguration ) );
functionRegistry.register( "array_contains_nullable", new ArrayContainsOperatorFunction( true, typeConfiguration ) );
functionRegistry.register( "array_includes", new ArrayIncludesOperatorFunction( false, typeConfiguration ) );
functionRegistry.register( "array_includes_nullable", new ArrayIncludesOperatorFunction( true, typeConfiguration ) );
}

/**
Expand All @@ -2711,6 +2733,8 @@ public void arrayContains_postgresql() {
public void arrayContains_oracle() {
functionRegistry.register( "array_contains", new OracleArrayContainsFunction( false, typeConfiguration ) );
functionRegistry.register( "array_contains_nullable", new OracleArrayContainsFunction( true, typeConfiguration ) );
functionRegistry.register( "array_includes", new OracleArrayIncludesFunction( false, typeConfiguration ) );
functionRegistry.register( "array_includes_nullable", new OracleArrayIncludesFunction( true, typeConfiguration ) );
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
*/
package org.hibernate.dialect.function.array;

import org.hibernate.internal.log.DeprecationLogger;
import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.type.spi.TypeConfiguration;

import org.jboss.logging.Logger;

/**
* Encapsulates the validator, return type and argument type resolvers for the array_contains function.
* Subclasses only have to implement the rendering.
*/
public abstract class AbstractArrayContainsFunction extends AbstractSqmSelfRenderingFunctionDescriptor {

protected static final DeprecationLogger LOG = Logger.getMessageLogger( DeprecationLogger.class, AbstractArrayContainsFunction.class.getName() );

protected final boolean nullable;

public AbstractArrayContainsFunction(boolean nullable, TypeConfiguration typeConfiguration) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.array;

import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor;
import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators;
import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers;
import org.hibernate.type.spi.TypeConfiguration;

/**
* Encapsulates the validator, return type and argument type resolvers for the array_includes function.
* Subclasses only have to implement the rendering.
*/
public abstract class AbstractArrayIncludesFunction extends AbstractSqmSelfRenderingFunctionDescriptor {

protected final boolean nullable;

public AbstractArrayIncludesFunction(boolean nullable, TypeConfiguration typeConfiguration) {
super(
"array_includes" + ( nullable ? "_nullable" : "" ),
StandardArgumentsValidators.composite(
StandardArgumentsValidators.exactly( 2 ),
ArrayIncludesArgumentValidator.INSTANCE
),
StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.standardBasicTypeForJavaType( Boolean.class ) ),
ArrayIncludesArgumentTypeResolver.INSTANCE
);
this.nullable = nullable;
}

@Override
public String getArgumentListSignature() {
return "(ARRAY haystackArray, OBJECT needleArray)";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void render(
final JdbcMappingContainer needleTypeContainer = needleExpression.getExpressionType();
final JdbcMapping needleType = needleTypeContainer == null ? null : needleTypeContainer.getSingleJdbcMapping();
if ( needleType == null || needleType instanceof BasicPluralType<?, ?> ) {
LOG.deprecatedArrayContainsWithArray();
if ( nullable ) {
super.render( sqlAppender, sqlAstArguments, returnType, walker );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void render(
final JdbcMappingContainer needleTypeContainer = needleExpression.getExpressionType();
final JdbcMapping needleType = needleTypeContainer == null ? null : needleTypeContainer.getSingleJdbcMapping();
if ( needleType == null || needleType instanceof BasicPluralType<?, ?> ) {
LOG.deprecatedArrayContainsWithArray();
sqlAppender.append( '(' );
if ( ArrayHelper.isNullable( haystackExpression ) ) {
walker.render( haystackExpression, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.array;

import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.model.domain.DomainType;
import org.hibernate.query.sqm.produce.function.FunctionArgumentTypeResolver;
import org.hibernate.query.sqm.sql.SqmToSqlAstConverter;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmFunction;
import org.hibernate.type.BasicPluralType;

/**
* A {@link FunctionArgumentTypeResolver} that resolves the argument types for the {@code array_includes} function.
*/
public class ArrayIncludesArgumentTypeResolver implements FunctionArgumentTypeResolver {

public static final FunctionArgumentTypeResolver INSTANCE = new ArrayIncludesArgumentTypeResolver();

@Override
public MappingModelExpressible<?> resolveFunctionArgumentType(
SqmFunction<?> function,
int argumentIndex,
SqmToSqlAstConverter converter) {
if ( argumentIndex == 0 ) {
final SqmTypedNode<?> node = function.getArguments().get( 1 );
if ( node instanceof SqmExpression<?> ) {
return converter.determineValueMapping( (SqmExpression<?>) node );
}
}
else if ( argumentIndex == 1 ) {
final SqmTypedNode<?> node = function.getArguments().get( 0 );
if ( node instanceof SqmExpression<?> ) {
return converter.determineValueMapping( (SqmExpression<?>) node );
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.array;

import java.util.List;

import org.hibernate.query.sqm.produce.function.ArgumentsValidator;
import org.hibernate.query.sqm.produce.function.FunctionArgumentException;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.type.BasicPluralType;
import org.hibernate.type.spi.TypeConfiguration;

/**
* A {@link ArgumentsValidator} that validates the arguments for the {@code array_includes} function.
*/
public class ArrayIncludesArgumentValidator extends ArrayArgumentValidator {

public static final ArgumentsValidator INSTANCE = new ArrayIncludesArgumentValidator();

protected ArrayIncludesArgumentValidator() {
super( 0 );
}

@Override
public void validate(
List<? extends SqmTypedNode<?>> arguments,
String functionName,
TypeConfiguration typeConfiguration) {
final BasicPluralType<?, ?> haystackType =
getPluralType( 0, arguments, functionName, typeConfiguration );
final BasicPluralType<?, ?> needleType =
getPluralType( 1, arguments, functionName, typeConfiguration );
if ( haystackType != null && needleType != null
&& !haystackType.equals( needleType )
&& !haystackType.getElementType().equals( needleType ) ) {
throw new FunctionArgumentException(
String.format(
"Parameter 1 of function '%s()' has type %s, but argument is of type '%s'",
functionName,
haystackType.getJavaTypeDescriptor().getTypeName(),
needleType.getTypeName()
)
);
}
}
}
Loading

0 comments on commit c8aa4f3

Please sign in to comment.