HHH-18089 Support bracket syntax with string types
This commit is contained in:
parent
c8aa4f39da
commit
8b5cdba5bc
|
@ -5333,15 +5333,48 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
|
|||
}
|
||||
else if ( ctx.simplePath() != null && ctx.slicedPathAccessFragment() != null ) {
|
||||
final List<HqlParser.ExpressionContext> slicedFragments = ctx.slicedPathAccessFragment().expression();
|
||||
return getFunctionDescriptor( "array_slice" ).generateSqmExpression(
|
||||
List.of(
|
||||
(SqmTypedNode<?>) visitSimplePath( ctx.simplePath() ),
|
||||
(SqmTypedNode<?>) slicedFragments.get( 0 ).accept( this ),
|
||||
(SqmTypedNode<?>) slicedFragments.get( 1 ).accept( this )
|
||||
),
|
||||
null,
|
||||
creationContext.getQueryEngine()
|
||||
);
|
||||
final SqmTypedNode<?> lhs = (SqmTypedNode<?>) visitSimplePath( ctx.simplePath() );
|
||||
final SqmExpressible<?> lhsExpressible = lhs.getExpressible();
|
||||
if ( lhsExpressible != null && lhsExpressible.getSqmType() instanceof BasicPluralType<?, ?> ) {
|
||||
return getFunctionDescriptor( "array_slice" ).generateSqmExpression(
|
||||
List.of(
|
||||
lhs,
|
||||
(SqmTypedNode<?>) slicedFragments.get( 0 ).accept( this ),
|
||||
(SqmTypedNode<?>) slicedFragments.get( 1 ).accept( this )
|
||||
),
|
||||
null,
|
||||
creationContext.getQueryEngine()
|
||||
);
|
||||
}
|
||||
else {
|
||||
final SqmExpression<?> start = (SqmExpression<?>) slicedFragments.get( 0 ).accept( this );
|
||||
final SqmExpression<?> end = (SqmExpression<?>) slicedFragments.get( 1 ).accept( this );
|
||||
return getFunctionDescriptor( "substring" ).generateSqmExpression(
|
||||
List.of(
|
||||
lhs,
|
||||
start,
|
||||
new SqmBinaryArithmetic<>(
|
||||
BinaryArithmeticOperator.ADD,
|
||||
new SqmBinaryArithmetic<>(
|
||||
BinaryArithmeticOperator.SUBTRACT,
|
||||
end,
|
||||
start,
|
||||
creationContext.getJpaMetamodel(),
|
||||
creationContext.getNodeBuilder()
|
||||
),
|
||||
new SqmLiteral<>(
|
||||
1,
|
||||
creationContext.getNodeBuilder().getIntegerType(),
|
||||
creationContext.getNodeBuilder()
|
||||
),
|
||||
creationContext.getJpaMetamodel(),
|
||||
creationContext.getNodeBuilder()
|
||||
)
|
||||
),
|
||||
null,
|
||||
creationContext.getQueryEngine()
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new ParsingException( "Illegal domain path '" + ctx.getText() + "'" );
|
||||
|
|
|
@ -25,10 +25,14 @@ import org.hibernate.query.sqm.tree.expression.NullSqmExpressible;
|
|||
import org.hibernate.sql.ast.tree.SqlAstNode;
|
||||
import org.hibernate.sql.ast.tree.expression.Expression;
|
||||
import org.hibernate.type.BasicType;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
import org.hibernate.type.spi.TypeConfiguration;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import static org.hibernate.type.SqlTypes.isCharacterOrClobType;
|
||||
import static org.hibernate.type.SqlTypes.isNumericType;
|
||||
|
||||
/**
|
||||
* @author Steve Ebersole
|
||||
*/
|
||||
|
@ -142,8 +146,7 @@ public class StandardFunctionReturnTypeResolvers {
|
|||
// Internal helpers
|
||||
|
||||
@Internal
|
||||
public static boolean isAssignableTo(
|
||||
ReturnableType<?> defined, ReturnableType<?> implied) {
|
||||
public static boolean isAssignableTo(ReturnableType<?> defined, ReturnableType<?> implied) {
|
||||
if ( implied == null ) {
|
||||
return false;
|
||||
}
|
||||
|
@ -152,19 +155,27 @@ public class StandardFunctionReturnTypeResolvers {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!(implied instanceof BasicType) || !(defined instanceof BasicType) ) {
|
||||
if ( !( implied instanceof BasicType ) || !( defined instanceof BasicType ) ) {
|
||||
return false;
|
||||
}
|
||||
return isAssignableTo(
|
||||
( (BasicType<?>) defined ).getJdbcMapping(),
|
||||
( (BasicType<?>) implied ).getJdbcMapping()
|
||||
);
|
||||
}
|
||||
|
||||
@Internal
|
||||
public static boolean isAssignableTo(JdbcMapping defined, JdbcMapping implied) {
|
||||
//This list of cases defines legal promotions from a SQL function return
|
||||
//type specified in the function template (i.e. in the Dialect) and a type
|
||||
//that is determined by how the function is used in the HQL query. In essence
|
||||
//the types are compatible if the map to the same JDBC type, of if they are
|
||||
//both numeric types.
|
||||
int impliedTypeCode = ((BasicType<?>) implied).getJdbcMapping().getJdbcType().getDefaultSqlTypeCode();
|
||||
int definedTypeCode = ((BasicType<?>) defined).getJdbcMapping().getJdbcType().getDefaultSqlTypeCode();
|
||||
int impliedTypeCode = implied.getJdbcType().getDefaultSqlTypeCode();
|
||||
int definedTypeCode = defined.getJdbcType().getDefaultSqlTypeCode();
|
||||
return impliedTypeCode == definedTypeCode
|
||||
|| isNumeric( impliedTypeCode ) && isNumeric( definedTypeCode );
|
||||
|| isNumericType( impliedTypeCode ) && isNumericType( definedTypeCode )
|
||||
|| isCharacterOrClobType( impliedTypeCode ) && isCharacterOrClobType( definedTypeCode );
|
||||
}
|
||||
|
||||
@Internal
|
||||
|
@ -202,27 +213,7 @@ public class StandardFunctionReturnTypeResolvers {
|
|||
//that is determined by how the function is used in the HQL query. In essence
|
||||
//the types are compatible if the map to the same JDBC type, of if they are
|
||||
//both numeric types.
|
||||
int impliedTypeCode = implied.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode();
|
||||
int definedTypeCode = defined.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode();
|
||||
return impliedTypeCode == definedTypeCode
|
||||
|| isNumeric( impliedTypeCode ) && isNumeric( definedTypeCode );
|
||||
|
||||
}
|
||||
|
||||
private static boolean isNumeric(int type) {
|
||||
switch ( type ) {
|
||||
case Types.SMALLINT:
|
||||
case Types.TINYINT:
|
||||
case Types.INTEGER:
|
||||
case Types.BIGINT:
|
||||
case Types.FLOAT:
|
||||
case Types.REAL:
|
||||
case Types.DOUBLE:
|
||||
case Types.NUMERIC:
|
||||
case Types.DECIMAL:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return isAssignableTo( defined.getJdbcMapping(), implied.getJdbcMapping() );
|
||||
}
|
||||
|
||||
public static ReturnableType<?> extractArgumentType(
|
||||
|
|
|
@ -3694,7 +3694,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
}
|
||||
if ( parentPath == null ) {
|
||||
if ( sqmPath instanceof SqmFunctionPath<?> ) {
|
||||
return visitFunctionPath( (SqmFunctionPath<?>) sqmPath );
|
||||
final SqmFunctionPath<?> functionPath = (SqmFunctionPath<?>) sqmPath;
|
||||
if ( functionPath.getReferencedPathSource() instanceof CompositeSqmPathSource<?> ) {
|
||||
return (TableGroup) visitFunctionPath( functionPath );
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -3794,7 +3797,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
if ( tableGroup == null ) {
|
||||
prepareReusablePath( path, () -> null );
|
||||
|
||||
if ( !( path instanceof SqmEntityValuedSimplePath<?>
|
||||
if ( path.getLhs() != null && !( path instanceof SqmEntityValuedSimplePath<?>
|
||||
|| path instanceof SqmEmbeddedValuedSimplePath<?>
|
||||
|| path instanceof SqmAnyValuedSimplePath<?>
|
||||
|| path instanceof SqmTreatedPath<?, ?> ) ) {
|
||||
|
@ -4537,20 +4540,25 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
|
|||
}
|
||||
|
||||
@Override
|
||||
public TableGroup visitFunctionPath(SqmFunctionPath<?> functionPath) {
|
||||
public Expression visitFunctionPath(SqmFunctionPath<?> functionPath) {
|
||||
final NavigablePath navigablePath = functionPath.getNavigablePath();
|
||||
TableGroup tableGroup = getFromClauseAccess().findTableGroup( navigablePath );
|
||||
if ( tableGroup == null ) {
|
||||
final Expression functionExpression = (Expression) functionPath.getFunction().accept( this );
|
||||
final EmbeddableMappingType embeddableMappingType = ( (AggregateJdbcType) functionExpression.getExpressionType()
|
||||
final JdbcType jdbcType = functionExpression.getExpressionType()
|
||||
.getSingleJdbcMapping()
|
||||
.getJdbcType() ).getEmbeddableMappingType();
|
||||
tableGroup = new EmbeddableFunctionTableGroup(
|
||||
navigablePath,
|
||||
embeddableMappingType,
|
||||
functionExpression
|
||||
);
|
||||
getFromClauseAccess().registerTableGroup( navigablePath, tableGroup );
|
||||
.getJdbcType();
|
||||
if ( jdbcType instanceof AggregateJdbcType ) {
|
||||
tableGroup = new EmbeddableFunctionTableGroup(
|
||||
navigablePath,
|
||||
( (AggregateJdbcType) jdbcType ).getEmbeddableMappingType(),
|
||||
functionExpression
|
||||
);
|
||||
getFromClauseAccess().registerTableGroup( navigablePath, tableGroup );
|
||||
}
|
||||
else {
|
||||
return functionExpression;
|
||||
}
|
||||
}
|
||||
return tableGroup;
|
||||
}
|
||||
|
|
|
@ -110,17 +110,30 @@ public class SqmBasicValuedSimplePath<T>
|
|||
if ( indexedPath != null ) {
|
||||
return indexedPath;
|
||||
}
|
||||
if ( !( getNodeType().getSqmPathType() instanceof BasicPluralType<?, ?> ) ) {
|
||||
throw new UnsupportedOperationException( "Index access is only supported for basic plural types." );
|
||||
}
|
||||
final DomainType<T> sqmPathType = getNodeType().getSqmPathType();
|
||||
final QueryEngine queryEngine = creationState.getCreationContext().getQueryEngine();
|
||||
final SelfRenderingSqmFunction<?> result = queryEngine.getSqmFunctionRegistry()
|
||||
.findFunctionDescriptor( "array_get" )
|
||||
.generateSqmExpression(
|
||||
asList( this, selector ),
|
||||
null,
|
||||
queryEngine
|
||||
);
|
||||
final SelfRenderingSqmFunction<?> result;
|
||||
if ( sqmPathType instanceof BasicPluralType<?, ?> ) {
|
||||
result = queryEngine.getSqmFunctionRegistry()
|
||||
.findFunctionDescriptor( "array_get" )
|
||||
.generateSqmExpression(
|
||||
asList( this, selector ),
|
||||
null,
|
||||
queryEngine
|
||||
);
|
||||
}
|
||||
else if ( sqmPathType.getRelationalJavaType().getJavaTypeClass() == String.class ) {
|
||||
result = queryEngine.getSqmFunctionRegistry()
|
||||
.findFunctionDescriptor( "substring" )
|
||||
.generateSqmExpression(
|
||||
asList( this, selector, nodeBuilder().literal( 1 ) ),
|
||||
nodeBuilder().getCharacterType(),
|
||||
queryEngine
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw new UnsupportedOperationException( "Index access is only supported for basic plural and string types, but got: " + sqmPathType );
|
||||
}
|
||||
final SqmFunctionPath<Object> path = new SqmFunctionPath<>( result );
|
||||
pathRegistry.register( path );
|
||||
return path;
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.hibernate.metamodel.model.domain.EntityDomainType;
|
|||
import org.hibernate.metamodel.model.domain.ListPersistentAttribute;
|
||||
import org.hibernate.metamodel.model.domain.MapPersistentAttribute;
|
||||
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
|
||||
import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource;
|
||||
import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource;
|
||||
import org.hibernate.query.NotIndexedCollectionException;
|
||||
import org.hibernate.query.hql.spi.SqmCreationState;
|
||||
|
@ -31,8 +32,11 @@ import org.hibernate.query.sqm.tree.from.SqmFrom;
|
|||
import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin;
|
||||
import org.hibernate.spi.NavigablePath;
|
||||
import org.hibernate.type.BasicPluralType;
|
||||
import org.hibernate.type.BasicType;
|
||||
|
||||
import jakarta.persistence.metamodel.Bindable;
|
||||
import jakarta.persistence.metamodel.ManagedType;
|
||||
import jakarta.persistence.metamodel.Type;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
|
@ -56,16 +60,34 @@ public class SqmFunctionPath<T> extends AbstractSqmPath<T> {
|
|||
private static <X> SqmPathSource<X> determinePathSource(NavigablePath navigablePath, SqmFunction<?> function) {
|
||||
//noinspection unchecked
|
||||
final SqmExpressible<X> nodeType = (SqmExpressible<X>) function.getNodeType();
|
||||
final EmbeddableDomainType<X> embeddableDomainType = function.nodeBuilder()
|
||||
final Class<X> bindableJavaType = nodeType.getBindableJavaType();
|
||||
final ManagedType<X> managedType = function.nodeBuilder()
|
||||
.getJpaMetamodel()
|
||||
.embeddable( nodeType.getBindableJavaType() );
|
||||
return new EmbeddedSqmPathSource<>(
|
||||
navigablePath.getFullPath(),
|
||||
null,
|
||||
embeddableDomainType,
|
||||
Bindable.BindableType.SINGULAR_ATTRIBUTE,
|
||||
false
|
||||
);
|
||||
.findManagedType( bindableJavaType );
|
||||
if ( managedType == null ) {
|
||||
final BasicType<X> basicType = function.nodeBuilder().getTypeConfiguration()
|
||||
.getBasicTypeForJavaType( bindableJavaType );
|
||||
return new BasicSqmPathSource<>(
|
||||
navigablePath.getFullPath(),
|
||||
null,
|
||||
basicType,
|
||||
basicType.getRelationalJavaType(),
|
||||
Bindable.BindableType.SINGULAR_ATTRIBUTE,
|
||||
false
|
||||
);
|
||||
}
|
||||
else if ( managedType.getPersistenceType() == Type.PersistenceType.EMBEDDABLE ) {
|
||||
return new EmbeddedSqmPathSource<>(
|
||||
navigablePath.getFullPath(),
|
||||
null,
|
||||
(EmbeddableDomainType<X>) managedType,
|
||||
Bindable.BindableType.SINGULAR_ATTRIBUTE,
|
||||
false
|
||||
);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException( "Unsupported return type for function: " + bindableJavaType.getName() );
|
||||
}
|
||||
}
|
||||
|
||||
public SqmFunction<?> getFunction() {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package org.hibernate.orm.test.hql;
|
||||
|
||||
import org.hibernate.testing.orm.domain.StandardDomainModel;
|
||||
import org.hibernate.testing.orm.domain.gambit.BasicEntity;
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.JiraKey;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
@DomainModel(standardModels = StandardDomainModel.GAMBIT)
|
||||
@SessionFactory
|
||||
@JiraKey("HHH-18089")
|
||||
public class StringBracketSyntaxTest {
|
||||
|
||||
@BeforeAll
|
||||
public void setUp(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
BasicEntity entity = new BasicEntity(1, "Hello World");
|
||||
session.persist( entity );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public void tearDown(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
session.createMutationQuery( "delete from BasicEntity" ).executeUpdate();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCharAtSyntax(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
Character firstChar = session.createQuery( "select e.data[1] from BasicEntity e", Character.class )
|
||||
.getSingleResult();
|
||||
assertThat( firstChar ).isEqualTo( 'H' );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubstringSyntax(SessionFactoryScope scope) {
|
||||
scope.inTransaction(
|
||||
session -> {
|
||||
String substring = session.createQuery( "select e.data[1:6] from BasicEntity e", String.class )
|
||||
.getSingleResult();
|
||||
assertThat( substring ).isEqualTo( "Hello " );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -140,3 +140,12 @@ Plenty of syntax sugar for array operations was added:
|
|||
|Overlaps predicate for overlaps check
|
||||
|===
|
||||
|
||||
[[string-syntax-sugar]]
|
||||
== Syntax sugar for string functions
|
||||
|
||||
The bracket syntax can now also be used for string typed expressions to select a single character by index,
|
||||
or obtain a substring by start and end index.
|
||||
|
||||
`stringPath[2]` is syntax sugar for `substring(stringPath, 2, 1)` and returns a `Character`.
|
||||
`stringPath[2:3]` is syntax sugar for `substring(stringPath, 2, 3-2+1)`,
|
||||
where `3-2+1` is the expression to determine the desired string length.
|
||||
|
|
Loading…
Reference in New Issue