HHH-17335 Add array_slice function

This commit is contained in:
Christian Beikov 2023-10-24 16:01:58 +02:00
parent 950423e7dd
commit 937116ed8a
18 changed files with 358 additions and 20 deletions

View File

@ -1130,6 +1130,7 @@ The following functions deal with SQL array types, which are not supported on ev
| `array_get()` | Accesses the element of an array by index
| `array_set()` | Creates array copy with given element at given index
| `array_remove()` | Creates array copy with given element removed
| `array_slice()` | Creates a sub-array of the based on lower and upper index
|===
===== `array()`
@ -1301,6 +1302,20 @@ include::{array-example-dir-hql}/ArrayRemoveIndexTest.java[tags=hql-array-remove
----
====
[[hql-array-slice-functions]]
===== `array_slice()`
Returns the sub-array as specified by the given start and end index. Returns `null` if any of the arguments is `null`
and also if the index is out of bounds.
[[hql-array-remove-index-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArrayRemoveIndexTest.java[tags=hql-array-remove-index-example]
----
====
[[hql-user-defined-functions]]
==== Native and user-defined functions

View File

@ -475,7 +475,8 @@ public class CockroachLegacyDialect extends Dialect {
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionFactory.arrayRemove();
functionFactory.arrayRemoveIndex_postgresql();
functionFactory.arrayRemoveIndex_unnest( true );
functionFactory.arraySlice_operator();
functionContributions.getFunctionRegistry().register(
"trunc",

View File

@ -384,6 +384,7 @@ public class H2LegacyDialect extends Dialect {
functionFactory.arraySet_h2();
functionFactory.arrayRemove_h2();
functionFactory.arrayRemoveIndex_h2();
functionFactory.arraySlice();
}
else {
// Use group_concat until 2.x as listagg was buggy

View File

@ -261,7 +261,8 @@ public class HSQLLegacyDialect extends Dialect {
functionFactory.arrayGet_unnest();
functionFactory.arraySet_hsql();
functionFactory.arrayRemove_hsql();
functionFactory.arrayRemoveIndex_unnest();
functionFactory.arrayRemoveIndex_unnest( false );
functionFactory.arraySlice_unnest();
}
@Override

View File

@ -298,6 +298,7 @@ public class OracleLegacyDialect extends Dialect {
functionFactory.arraySet_oracle();
functionFactory.arrayRemove_oracle();
functionFactory.arrayRemoveIndex_oracle();
functionFactory.arraySlice_oracle();
}
@Override

View File

@ -595,7 +595,8 @@ public class PostgreSQLLegacyDialect extends Dialect {
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionFactory.arrayRemove();
functionFactory.arrayRemoveIndex_postgresql();
functionFactory.arrayRemoveIndex_unnest( true );
functionFactory.arraySlice_operator();
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
functionFactory.makeDateTimeTimestamp();

View File

@ -462,7 +462,8 @@ public class CockroachDialect extends Dialect {
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionFactory.arrayRemove();
functionFactory.arrayRemoveIndex_postgresql();
functionFactory.arrayRemoveIndex_unnest( true );
functionFactory.arraySlice_operator();
functionContributions.getFunctionRegistry().register(
"trunc",

View File

@ -93,7 +93,6 @@ import static org.hibernate.type.SqlTypes.LONG32VARCHAR;
import static org.hibernate.type.SqlTypes.NCHAR;
import static org.hibernate.type.SqlTypes.NVARCHAR;
import static org.hibernate.type.SqlTypes.OTHER;
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
import static org.hibernate.type.SqlTypes.UUID;
import static org.hibernate.type.SqlTypes.VARBINARY;
import static org.hibernate.type.SqlTypes.VARCHAR;
@ -324,6 +323,7 @@ public class H2Dialect extends Dialect {
functionFactory.arraySet_h2();
functionFactory.arrayRemove_h2();
functionFactory.arrayRemoveIndex_h2();
functionFactory.arraySlice();
}
@Override

View File

@ -201,7 +201,8 @@ public class HSQLDialect extends Dialect {
functionFactory.arrayGet_unnest();
functionFactory.arraySet_hsql();
functionFactory.arrayRemove_hsql();
functionFactory.arrayRemoveIndex_unnest();
functionFactory.arrayRemoveIndex_unnest( false );
functionFactory.arraySlice_unnest();
}
@Override

View File

@ -419,6 +419,26 @@ public class OracleArrayJdbcType extends ArrayJdbcType {
false
)
);
database.addAuxiliaryDatabaseObject(
new NamedAuxiliaryDatabaseObject(
arrayTypeName + "_slice",
database.getDefaultNamespace(),
new String[]{
"create or replace function " + arrayTypeName + "_slice(arr in " + arrayTypeName +
", startIdx in number, endIdx in number) return " + arrayTypeName + " deterministic is " +
"res " + arrayTypeName + ":=" + arrayTypeName + "(); begin " +
"if arr is null or startIdx is null or endIdx is null then return null; end if; " +
"for i in startIdx .. least(arr.count,endIdx) loop " +
"res.extend; res(res.last) := arr(i); " +
"end loop; " +
"return res; " +
"end;"
},
new String[] { "drop function " + arrayTypeName + "_slice" },
emptySet(),
false
)
);
}
protected String createOrReplaceConcatFunction(String arrayTypeName) {

View File

@ -327,6 +327,7 @@ public class OracleDialect extends Dialect {
functionFactory.arraySet_oracle();
functionFactory.arrayRemove_oracle();
functionFactory.arrayRemoveIndex_oracle();
functionFactory.arraySlice_oracle();
}
@Override

View File

@ -643,7 +643,8 @@ public class PostgreSQLDialect extends Dialect {
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionFactory.arrayRemove();
functionFactory.arrayRemoveIndex_postgresql();
functionFactory.arrayRemoveIndex_unnest( true );
functionFactory.arraySlice_operator();
functionFactory.makeDateTimeTimestamp();
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions

View File

@ -24,6 +24,7 @@ import org.hibernate.dialect.function.array.ArrayContainsQuantifiedUnnestFunctio
import org.hibernate.dialect.function.array.ArrayGetUnnestFunction;
import org.hibernate.dialect.function.array.ArrayRemoveIndexUnnestFunction;
import org.hibernate.dialect.function.array.ArraySetUnnestFunction;
import org.hibernate.dialect.function.array.ArraySliceUnnestFunction;
import org.hibernate.dialect.function.array.ArrayViaArgumentReturnTypeResolver;
import org.hibernate.dialect.function.array.ElementViaArrayArgumentReturnTypeResolver;
import org.hibernate.dialect.function.array.H2ArrayContainsQuantifiedEmulation;
@ -42,6 +43,7 @@ import org.hibernate.dialect.function.array.OracleArrayPositionFunction;
import org.hibernate.dialect.function.array.OracleArrayRemoveFunction;
import org.hibernate.dialect.function.array.OracleArrayRemoveIndexFunction;
import org.hibernate.dialect.function.array.OracleArraySetFunction;
import org.hibernate.dialect.function.array.OracleArraySliceFunction;
import org.hibernate.dialect.function.array.PostgreSQLArrayConcatFunction;
import org.hibernate.dialect.function.array.PostgreSQLArrayPositionFunction;
import org.hibernate.dialect.function.array.CastingArrayConstructorFunction;
@ -2123,7 +2125,7 @@ public class CommonFunctionFactory {
.setArgumentCountBetween( 1, 3 )
.setParameterTypes( ANY, INTEGER, ANY )
.setArgumentTypeResolver(
StandardFunctionArgumentTypeResolvers.composite(
StandardFunctionArgumentTypeResolvers.byArgument(
StandardFunctionArgumentTypeResolvers.argumentsOrImplied( 2 ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, INTEGER ),
StandardFunctionArgumentTypeResolvers.argumentsOrImplied( 0 )
@ -2135,7 +2137,7 @@ public class CommonFunctionFactory {
.setArgumentCountBetween( 1, 3 )
.setParameterTypes( ANY, INTEGER, ANY )
.setArgumentTypeResolver(
StandardFunctionArgumentTypeResolvers.composite(
StandardFunctionArgumentTypeResolvers.byArgument(
StandardFunctionArgumentTypeResolvers.argumentsOrImplied( 2 ),
StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, INTEGER ),
StandardFunctionArgumentTypeResolvers.argumentsOrImplied( 0 )
@ -3063,15 +3065,8 @@ public class CommonFunctionFactory {
/**
* HSQL, CockroachDB and PostgreSQL array_remove_index() function
*/
public void arrayRemoveIndex_unnest() {
functionRegistry.register( "array_remove_index", new ArrayRemoveIndexUnnestFunction( false ) );
}
/**
* HSQL, CockroachDB and PostgreSQL array_remove_index() function
*/
public void arrayRemoveIndex_postgresql() {
functionRegistry.register( "array_remove_index", new ArrayRemoveIndexUnnestFunction( true ) );
public void arrayRemoveIndex_unnest(boolean castEmptyArrayLiteral) {
functionRegistry.register( "array_remove_index", new ArrayRemoveIndexUnnestFunction( castEmptyArrayLiteral ) );
}
/**
@ -3080,4 +3075,62 @@ public class CommonFunctionFactory {
public void arrayRemoveIndex_oracle() {
functionRegistry.register( "array_remove_index", new OracleArrayRemoveIndexFunction() );
}
/**
* H2 array_slice() function
*/
public void arraySlice() {
functionRegistry.patternAggregateDescriptorBuilder( "array_slice", "case when ?1 is null or ?2 is null or ?3 is null then null else coalesce(array_slice(?1,?2,?3),array[]) end" )
.setArgumentsValidator(
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( null, ANY, INTEGER, INTEGER ),
ArrayArgumentValidator.DEFAULT_INSTANCE
)
)
.setReturnTypeResolver( ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE )
.setArgumentTypeResolver(
StandardFunctionArgumentTypeResolvers.composite(
StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, INTEGER ),
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
)
)
.setArgumentListSignature( "(ARRAY array, INTEGER start, INTEGER end)" )
.register();
}
/**
* HSQL array_slice() function
*/
public void arraySlice_unnest() {
functionRegistry.register( "array_slice", new ArraySliceUnnestFunction( false ) );
}
/**
* CockroachDB and PostgreSQL array_slice() function
*/
public void arraySlice_operator() {
functionRegistry.patternAggregateDescriptorBuilder( "array_slice", "?1[?2:?3]" )
.setArgumentsValidator(
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( null, ANY, INTEGER, INTEGER ),
ArrayArgumentValidator.DEFAULT_INSTANCE
)
)
.setReturnTypeResolver( ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE )
.setArgumentTypeResolver(
StandardFunctionArgumentTypeResolvers.composite(
StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, INTEGER ),
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
)
)
.setArgumentListSignature( "(ARRAY array, INTEGER start, INTEGER end)" )
.register();
}
/**
* Oracle array_slice() function
*/
public void arraySlice_oracle() {
functionRegistry.register( "array_slice", new OracleArraySliceFunction() );
}
}

View File

@ -21,8 +21,7 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER;
/**
* Encapsulates the validator, return type and argument type resolvers for the array_remove_index functions.
* Subclasses only have to implement the rendering.
* Implement the array remove index function by using {@code unnest}.
*/
public class ArrayRemoveIndexUnnestFunction extends AbstractSqmSelfRenderingFunctionDescriptor {

View File

@ -0,0 +1,77 @@
/*
* 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.function.AbstractSqmSelfRenderingFunctionDescriptor;
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.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 static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY;
import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER;
/**
* Implement the array slice function by using {@code unnest}.
*/
public class ArraySliceUnnestFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
private final boolean castEmptyArrayLiteral;
public ArraySliceUnnestFunction(boolean castEmptyArrayLiteral) {
super(
"array_slice",
StandardArgumentsValidators.composite(
new ArgumentTypesValidator( null, ANY, INTEGER, INTEGER ),
ArrayArgumentValidator.DEFAULT_INSTANCE
),
ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE,
StandardFunctionArgumentTypeResolvers.composite(
StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, INTEGER ),
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE
)
);
this.castEmptyArrayLiteral = castEmptyArrayLiteral;
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression startIndexExpression = (Expression) sqlAstArguments.get( 1 );
final Expression endIndexExpression = (Expression) sqlAstArguments.get( 2 );
sqlAppender.append( "case when ");
arrayExpression.accept( walker );
sqlAppender.append( " is null or ");
startIndexExpression.accept( walker );
sqlAppender.append( " is null or ");
endIndexExpression.accept( walker );
sqlAppender.append( " is null then null else coalesce((select array_agg(t.val) from unnest(" );
arrayExpression.accept( walker );
sqlAppender.append( ") with ordinality t(val,idx) where t.idx between " );
startIndexExpression.accept( walker );
sqlAppender.append( " and " );
endIndexExpression.accept( walker );
sqlAppender.append( "),");
if ( castEmptyArrayLiteral ) {
sqlAppender.append( "cast(array[] as " );
sqlAppender.append( ArrayTypeHelper.getArrayTypeName( arrayExpression.getExpressionType(), walker ) );
sqlAppender.append( ')' );
}
else {
sqlAppender.append( "array[]" );
}
sqlAppender.append(") end" );
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.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;
/**
* Oracle array_slice function.
*/
public class OracleArraySliceFunction extends ArraySliceUnnestFunction {
public OracleArraySliceFunction() {
super( false );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final String arrayTypeName = ArrayTypeHelper.getArrayTypeName(
( (Expression) sqlAstArguments.get( 0 ) ).getExpressionType(),
walker
);
sqlAppender.append( arrayTypeName );
sqlAppender.append( "_slice(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 1 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 2 ).accept( walker );
sqlAppender.append( ')' );
}
}

View File

@ -132,6 +132,23 @@ public final class StandardFunctionArgumentTypeResolvers {
}
public static FunctionArgumentTypeResolver composite(FunctionArgumentTypeResolver... resolvers) {
return (function, argumentIndex, converter) -> {
for ( FunctionArgumentTypeResolver resolver : resolvers ) {
final MappingModelExpressible<?> result = resolver.resolveFunctionArgumentType(
function,
argumentIndex,
converter
);
if ( result != null ) {
return result;
}
}
return null;
};
}
public static FunctionArgumentTypeResolver byArgument(FunctionArgumentTypeResolver... resolvers) {
return (function, argumentIndex, converter) -> {
return resolvers[argumentIndex].resolveFunctionArgumentType( function, argumentIndex, converter );
};

View File

@ -0,0 +1,105 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.orm.test.function.array;
import java.util.List;
import org.hibernate.cfg.AvailableSettings;
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.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.hibernate.testing.orm.junit.Setting;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Tuple;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* @author Christian Beikov
*/
@DomainModel(annotatedClasses = EntityWithArrays.class)
@SessionFactory
@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class)
// Make sure this stuff runs on a dedicated connection pool,
// otherwise we might run into ORA-21700: object does not exist or is marked for delete
// because the JDBC connection or database session caches something that should have been invalidated
@ServiceRegistry(settings = @Setting(name = AvailableSettings.CONNECTION_PROVIDER, value = ""))
public class ArraySliceTest {
@BeforeEach
public void prepareData(SessionFactoryScope scope) {
scope.inTransaction( em -> {
em.persist( new EntityWithArrays( 1L, new String[]{} ) );
em.persist( new EntityWithArrays( 2L, new String[]{ "abc", null, "def" } ) );
em.persist( new EntityWithArrays( 3L, null ) );
} );
}
@AfterEach
public void cleanup(SessionFactoryScope scope) {
scope.inTransaction( em -> {
em.createMutationQuery( "delete from EntityWithArrays" ).executeUpdate();
} );
}
@Test
public void testSlice(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-array-slice-example[]
List<Tuple> results = em.createQuery( "select e.id, array_slice(e.theArray, 1, 1) from EntityWithArrays e order by e.id", Tuple.class )
.getResultList();
//end::hql-array-slice-example[]
assertEquals( 3, results.size() );
assertEquals( 1L, results.get( 0 ).get( 0 ) );
assertArrayEquals( new String[0], results.get( 0 ).get( 1, String[].class ) );
assertEquals( 2L, results.get( 1 ).get( 0 ) );
assertArrayEquals( new String[] { "abc" }, results.get( 1 ).get( 1, String[].class ) );
assertEquals( 3L, results.get( 2 ).get( 0 ) );
assertNull( results.get( 2 ).get( 1, String[].class ) );
} );
}
@Test
public void testSliceEmpty(SessionFactoryScope scope) {
scope.inSession( em -> {
List<Tuple> results = em.createQuery( "select e.id, array_slice(e.theArray, 1, 0) from EntityWithArrays e order by e.id", Tuple.class )
.getResultList();
assertEquals( 3, results.size() );
assertEquals( 1L, results.get( 0 ).get( 0 ) );
assertArrayEquals( new String[0], results.get( 0 ).get( 1, String[].class ) );
assertEquals( 2L, results.get( 1 ).get( 0 ) );
assertArrayEquals( new String[0], results.get( 1 ).get( 1, String[].class ) );
assertEquals( 3L, results.get( 2 ).get( 0 ) );
assertNull( results.get( 2 ).get( 1, String[].class ) );
} );
}
@Test
public void testSliceOutOfRange(SessionFactoryScope scope) {
scope.inSession( em -> {
List<Tuple> results = em.createQuery( "select e.id, array_slice(e.theArray, 10000, 1) from EntityWithArrays e order by e.id", Tuple.class )
.getResultList();
assertEquals( 3, results.size() );
assertEquals( 1L, results.get( 0 ).get( 0 ) );
assertArrayEquals( new String[0], results.get( 0 ).get( 1, String[].class ) );
assertEquals( 2L, results.get( 1 ).get( 0 ) );
assertArrayEquals( new String[0], results.get( 1 ).get( 1, String[].class ) );
assertEquals( 3L, results.get( 2 ).get( 0 ) );
assertNull( results.get( 2 ).get( 1, String[].class ) );
} );
}
}