HHH-17335 Add array_set function

This commit is contained in:
Christian Beikov 2023-10-24 10:13:45 +02:00
parent 8c4ed1ef48
commit b8b8a874fd
19 changed files with 477 additions and 0 deletions

View File

@ -1128,6 +1128,7 @@ The following functions deal with SQL array types, which are not supported on ev
| `array_contains_any()` | Determines if one array holds at least one element of another array
| `array_contains_any_nullable()` | Determines if one array holds at least one element of another array, supporting null elements
| `array_get()` | Accesses the element of an array by index
| `array_set()` | Creates array copy with given element at given index
|===
===== `array()`
@ -1260,6 +1261,19 @@ include::{array-example-dir-hql}/ArrayGetTest.java[tags=hql-array-get-example]
----
====
[[hql-array-set-functions]]
===== `array_set()`
Returns an array copy with the given element placed at the given 1-based index, filling up prior slots with `null` if necessary.
[[hql-array-set-example]]
====
[source, JAVA, indent=0]
----
include::{array-example-dir-hql}/ArraySetTest.java[tags=hql-array-set-example]
----
====
[[hql-user-defined-functions]]
==== Native and user-defined functions

View File

@ -473,6 +473,7 @@ public class CockroachLegacyDialect extends Dialect {
functionFactory.arrayContainsAllNullable_operator();
functionFactory.arrayContainsAnyNullable_operator();
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionContributions.getFunctionRegistry().register(
"trunc",

View File

@ -381,6 +381,7 @@ public class H2LegacyDialect extends Dialect {
functionFactory.arrayContainsAllNullable_h2();
functionFactory.arrayContainsAnyNullable_h2();
functionFactory.arrayGet_h2();
functionFactory.arraySet_h2();
}
else {
// Use group_concat until 2.x as listagg was buggy

View File

@ -259,6 +259,7 @@ public class HSQLLegacyDialect extends Dialect {
functionFactory.arrayContainsAllNullable_hsql();
functionFactory.arrayContainsAnyNullable_hsql();
functionFactory.arrayGet_unnest();
functionFactory.arraySet_hsql();
}
@Override

View File

@ -295,6 +295,7 @@ public class OracleLegacyDialect extends Dialect {
functionFactory.arrayContainsAllNullable_oracle();
functionFactory.arrayContainsAnyNullable_oracle();
functionFactory.arrayGet_oracle();
functionFactory.arraySet_oracle();
}
@Override

View File

@ -593,6 +593,7 @@ public class PostgreSQLLegacyDialect extends Dialect {
functionFactory.arrayContainsAllNullable_operator();
functionFactory.arrayContainsAnyNullable_operator();
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
functionFactory.makeDateTimeTimestamp();

View File

@ -460,6 +460,7 @@ public class CockroachDialect extends Dialect {
functionFactory.arrayContainsAllNullable_operator();
functionFactory.arrayContainsAnyNullable_operator();
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionContributions.getFunctionRegistry().register(
"trunc",

View File

@ -321,6 +321,7 @@ public class H2Dialect extends Dialect {
functionFactory.arrayContainsAllNullable_h2();
functionFactory.arrayContainsAnyNullable_h2();
functionFactory.arrayGet_h2();
functionFactory.arraySet_h2();
}
@Override

View File

@ -199,6 +199,7 @@ public class HSQLDialect extends Dialect {
functionFactory.arrayContainsAllNullable_hsql();
functionFactory.arrayContainsAnyNullable_hsql();
functionFactory.arrayGet_unnest();
functionFactory.arraySet_hsql();
}
@Override

View File

@ -343,6 +343,36 @@ public class OracleArrayJdbcType extends ArrayJdbcType {
false
)
);
database.addAuxiliaryDatabaseObject(
new NamedAuxiliaryDatabaseObject(
arrayTypeName + "_set",
database.getDefaultNamespace(),
new String[]{
"create or replace function " + arrayTypeName + "_set(arr in " + arrayTypeName +
", idx in number, elem in " + getRawTypeName( elementType ) + ") return " + arrayTypeName + " deterministic is " +
"res " + arrayTypeName + ":=" + arrayTypeName + "(); begin " +
"if arr is not null then " +
"for i in 1 .. arr.count loop " +
"res.extend; " +
"res(i) := arr(i); " +
"end loop; " +
"for i in arr.count+1 .. idx loop " +
"res.extend; " +
"end loop; " +
"else " +
"for i in 1 .. idx loop " +
"res.extend; " +
"end loop; " +
"end if; " +
"res(idx) := elem; " +
"return res; " +
"end;"
},
new String[] { "drop function " + arrayTypeName + "_set" },
emptySet(),
false
)
);
}
protected String createOrReplaceConcatFunction(String arrayTypeName) {

View File

@ -324,6 +324,7 @@ public class OracleDialect extends Dialect {
functionFactory.arrayContainsAllNullable_oracle();
functionFactory.arrayContainsAnyNullable_oracle();
functionFactory.arrayGet_oracle();
functionFactory.arraySet_oracle();
}
@Override

View File

@ -641,6 +641,7 @@ public class PostgreSQLDialect extends Dialect {
functionFactory.arrayContainsAllNullable_operator();
functionFactory.arrayContainsAnyNullable_operator();
functionFactory.arrayGet_bracket();
functionFactory.arraySet_unnest();
functionFactory.makeDateTimeTimestamp();
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions

View File

@ -22,15 +22,19 @@ import org.hibernate.dialect.function.array.ArrayContainsQuantifiedOperatorFunct
import org.hibernate.dialect.function.array.ArrayContainsOperatorFunction;
import org.hibernate.dialect.function.array.ArrayContainsQuantifiedUnnestFunction;
import org.hibernate.dialect.function.array.ArrayGetUnnestFunction;
import org.hibernate.dialect.function.array.ArraySetUnnestFunction;
import org.hibernate.dialect.function.array.ElementViaArrayArgumentReturnTypeResolver;
import org.hibernate.dialect.function.array.H2ArrayContainsQuantifiedEmulation;
import org.hibernate.dialect.function.array.H2ArraySetFunction;
import org.hibernate.dialect.function.array.HSQLArrayPositionFunction;
import org.hibernate.dialect.function.array.HSQLArraySetFunction;
import org.hibernate.dialect.function.array.OracleArrayConcatFunction;
import org.hibernate.dialect.function.array.OracleArrayContainsAllFunction;
import org.hibernate.dialect.function.array.OracleArrayContainsAnyFunction;
import org.hibernate.dialect.function.array.OracleArrayGetFunction;
import org.hibernate.dialect.function.array.OracleArrayLengthFunction;
import org.hibernate.dialect.function.array.OracleArrayPositionFunction;
import org.hibernate.dialect.function.array.OracleArraySetFunction;
import org.hibernate.dialect.function.array.PostgreSQLArrayConcatFunction;
import org.hibernate.dialect.function.array.PostgreSQLArrayPositionFunction;
import org.hibernate.dialect.function.array.CastingArrayConstructorFunction;
@ -2976,4 +2980,32 @@ public class CommonFunctionFactory {
public void arrayGet_oracle() {
functionRegistry.register( "array_get", new OracleArrayGetFunction() );
}
/**
* H2 array_set() function
*/
public void arraySet_h2() {
functionRegistry.register( "array_set", new H2ArraySetFunction() );
}
/**
* HSQL array_set() function
*/
public void arraySet_hsql() {
functionRegistry.register( "array_set", new HSQLArraySetFunction() );
}
/**
* HSQL, CockroachDB and PostgreSQL array_set() function
*/
public void arraySet_unnest() {
functionRegistry.register( "array_set", new ArraySetUnnestFunction() );
}
/**
* Oracle array_set() function
*/
public void arraySet_oracle() {
functionRegistry.register( "array_set", new OracleArraySetFunction() );
}
}

View File

@ -0,0 +1,64 @@
/*
* 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 set function by using {@code unnest}.
*/
public class ArraySetUnnestFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
public ArraySetUnnestFunction() {
super(
"array_set",
StandardArgumentsValidators.composite(
new ArrayAndElementArgumentValidator( 0, 2 ),
new ArgumentTypesValidator( null, ANY, INTEGER, ANY )
),
ArrayViaArgumentReturnTypeResolver.DEFAULT_INSTANCE,
StandardFunctionArgumentTypeResolvers.composite(
StandardFunctionArgumentTypeResolvers.IMPLIED_RESULT_TYPE,
StandardFunctionArgumentTypeResolvers.invariant( ANY, INTEGER, ANY ),
new ArrayAndElementArgumentTypeResolver( 0, 2 )
)
);
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression indexExpression = (Expression) sqlAstArguments.get( 1 );
final Expression elementExpression = (Expression) sqlAstArguments.get( 2 );
sqlAppender.append( "(select array_agg(case when i.idx=");
indexExpression.accept( walker );
sqlAppender.append(" then " );
elementExpression.accept( walker );
sqlAppender.append(" else t.val end) from generate_series(1,greatest(coalesce(cardinality(" );
arrayExpression.accept( walker );
sqlAppender.append( "),0)," );
indexExpression.accept( walker );
sqlAppender.append( ")) i(idx) left join unnest(" );
arrayExpression.accept( walker );
sqlAppender.append( ") with ordinality t(val, idx) on i.idx=t.idx)" );
}
}

View File

@ -0,0 +1,70 @@
/*
* 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 java.util.function.Supplier;
import org.hibernate.metamodel.mapping.BasicValuedMapping;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.model.domain.DomainType;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.hibernate.sql.ast.tree.SqlAstNode;
import org.hibernate.type.BasicPluralType;
import org.hibernate.type.spi.TypeConfiguration;
/**
* A {@link FunctionReturnTypeResolver} that resolves the array type based on an argument.
* The inferred type and implied type have precedence though.
*/
public class ArrayViaArgumentReturnTypeResolver implements FunctionReturnTypeResolver {
public static final FunctionReturnTypeResolver DEFAULT_INSTANCE = new ArrayViaArgumentReturnTypeResolver( 0 );
private final int arrayIndex;
private ArrayViaArgumentReturnTypeResolver(int arrayIndex) {
this.arrayIndex = arrayIndex;
}
@Override
public ReturnableType<?> resolveFunctionReturnType(
ReturnableType<?> impliedType,
Supplier<MappingModelExpressible<?>> inferredTypeSupplier,
List<? extends SqmTypedNode<?>> arguments,
TypeConfiguration typeConfiguration) {
final MappingModelExpressible<?> inferredType = inferredTypeSupplier.get();
if ( inferredType != null ) {
if ( inferredType instanceof ReturnableType<?> ) {
return (ReturnableType<?>) inferredType;
}
else if ( inferredType instanceof BasicValuedMapping ) {
return (ReturnableType<?>) ( (BasicValuedMapping) inferredType ).getJdbcMapping();
}
}
if ( impliedType != null ) {
return impliedType;
}
final SqmExpressible<?> expressible = arguments.get( arrayIndex ).getExpressible();
final DomainType<?> type;
if ( expressible != null && ( type = expressible.getSqmType() ) instanceof BasicPluralType<?, ?> ) {
return (ReturnableType<?>) type;
}
return null;
}
@Override
public BasicValuedMapping resolveFunctionReturnType(
Supplier<BasicValuedMapping> impliedTypeAccess,
List<? extends SqlAstNode> arguments) {
return null;
}
}

View File

@ -0,0 +1,57 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/
package org.hibernate.dialect.function.array;
import java.util.List;
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
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;
/**
* H2 array_set function.
*/
public class H2ArraySetFunction extends ArraySetUnnestFunction {
public H2ArraySetFunction() {
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression indexExpression = (Expression) sqlAstArguments.get( 1 );
final Expression elementExpression = (Expression) sqlAstArguments.get( 2 );
sqlAppender.append( "(select array_agg(case when i.idx=");
indexExpression.accept( walker );
sqlAppender.append(" then " );
elementExpression.accept( walker );
sqlAppender.append(" when " );
arrayExpression.accept( walker );
sqlAppender.append(" is not null and i.idx<=cardinality(");
arrayExpression.accept( walker );
sqlAppender.append(") then array_get(");
arrayExpression.accept( walker );
sqlAppender.append(",i.idx) end) from system_range(1," );
sqlAppender.append( Integer.toString( getMaximumArraySize() ) );
sqlAppender.append( ") i(idx) where i.idx<=greatest(case when ");
arrayExpression.accept( walker );
sqlAppender.append(" is not null then cardinality(" );
arrayExpression.accept( walker );
sqlAppender.append( ") else 0 end," );
indexExpression.accept( walker );
sqlAppender.append( "))" );
}
protected int getMaximumArraySize() {
return 1000;
}
}

View File

@ -0,0 +1,48 @@
/*
* 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;
/**
* HSQLDB array_set function.
*/
public class HSQLArraySetFunction extends ArraySetUnnestFunction {
public HSQLArraySetFunction() {
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 );
final Expression indexExpression = (Expression) sqlAstArguments.get( 1 );
final Expression elementExpression = (Expression) sqlAstArguments.get( 2 );
sqlAppender.append( "(select array_agg(case when i.idx=");
indexExpression.accept( walker );
sqlAppender.append(" then " );
elementExpression.accept( walker );
sqlAppender.append(" else t.val end) from unnest(sequence_array(1,greatest(cardinality(" );
arrayExpression.accept( walker );
sqlAppender.append( ")," );
indexExpression.accept( walker );
sqlAppender.append( "),1)) i(idx) left join unnest(" );
arrayExpression.accept( walker );
sqlAppender.append( ") with ordinality t(val, idx) on i.idx=t.idx)" );
}
protected int getMaximumArraySize() {
return 1000;
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.metamodel.mapping.JdbcMappingContainer;
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_set function.
*/
public class OracleArraySetFunction extends ArraySetUnnestFunction {
public OracleArraySetFunction() {
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> sqlAstArguments,
SqlAstTranslator<?> walker) {
JdbcMappingContainer expressionType = null;
for ( SqlAstNode sqlAstArgument : sqlAstArguments ) {
expressionType = ( (Expression) sqlAstArgument ).getExpressionType();
if ( expressionType != null ) {
break;
}
}
final String arrayTypeName = ArrayTypeHelper.getArrayTypeName( expressionType, walker );
sqlAppender.append( arrayTypeName );
sqlAppender.append( "_set(" );
sqlAstArguments.get( 0 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 1 ).accept( walker );
sqlAppender.append( ',' );
sqlAstArguments.get( 2 ).accept( walker );
sqlAppender.append( ')' );
}
}

View File

@ -0,0 +1,104 @@
/*
* 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;
/**
* @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 ArraySetTest {
@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 testSet(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-array-set-example[]
List<Tuple> results = em.createQuery( "select e.id, array_set(e.theArray, 1, 'xyz') from EntityWithArrays e order by e.id", Tuple.class )
.getResultList();
//end::hql-array-set-example[]
assertEquals( 3, results.size() );
assertEquals( 1L, results.get( 0 ).get( 0 ) );
assertArrayEquals( new String[] { "xyz" }, results.get( 0 ).get( 1, String[].class ) );
assertEquals( 2L, results.get( 1 ).get( 0 ) );
assertArrayEquals( new String[] { "xyz", null, "def" }, results.get( 1 ).get( 1, String[].class ) );
assertEquals( 3L, results.get( 2 ).get( 0 ) );
assertArrayEquals( new String[] { "xyz" }, results.get( 2 ).get( 1, String[].class ) );
} );
}
@Test
public void testSetNullElement(SessionFactoryScope scope) {
scope.inSession( em -> {
List<Tuple> results = em.createQuery( "select e.id, array_set(e.theArray, 1, null) from EntityWithArrays e order by e.id", Tuple.class )
.getResultList();
assertEquals( 3, results.size() );
assertEquals( 1L, results.get( 0 ).get( 0 ) );
assertArrayEquals( new String[] { null }, results.get( 0 ).get( 1, String[].class ) );
assertEquals( 2L, results.get( 1 ).get( 0 ) );
assertArrayEquals( new String[] { null, null, "def" }, results.get( 1 ).get( 1, String[].class ) );
assertEquals( 3L, results.get( 2 ).get( 0 ) );
assertArrayEquals( new String[] { null }, results.get( 2 ).get( 1, String[].class ) );
} );
}
@Test
public void testSetFillNulls(SessionFactoryScope scope) {
scope.inSession( em -> {
List<Tuple> results = em.createQuery( "select e.id, array_set(e.theArray, 3, 'aaa') from EntityWithArrays e order by e.id", Tuple.class )
.getResultList();
assertEquals( 3, results.size() );
assertEquals( 1L, results.get( 0 ).get( 0 ) );
assertArrayEquals( new String[] { null, null, "aaa" }, results.get( 0 ).get( 1, String[].class ) );
assertEquals( 2L, results.get( 1 ).get( 0 ) );
assertArrayEquals( new String[] { "abc", null, "aaa" }, results.get( 1 ).get( 1, String[].class ) );
assertEquals( 3L, results.get( 2 ).get( 0 ) );
assertArrayEquals( new String[] { null, null, "aaa" }, results.get( 2 ).get( 1, String[].class ) );
} );
}
}