HHH-17859, HHH-17858 function() and column() functions

This commit is contained in:
Gavin King 2024-03-16 23:47:54 +01:00
parent 1ba67c2de9
commit 6441c60255
11 changed files with 192 additions and 31 deletions

View File

@ -11,7 +11,6 @@ import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator;
import org.hibernate.sql.ast.tree.Statement;
import org.hibernate.sql.ast.tree.cte.CteMaterialization;
import org.hibernate.sql.ast.tree.cte.CteStatement;
import org.hibernate.sql.ast.tree.expression.Any;
import org.hibernate.sql.ast.tree.expression.Every;
import org.hibernate.sql.ast.tree.expression.Expression;

View File

@ -158,6 +158,7 @@ BY : [bB] [yY];
CASE : [cC] [aA] [sS] [eE];
CAST : [cC] [aA] [sS] [tT];
COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE];
COLUMN : [cC] [oO] [lL] [uU] [mM] [nN];
CONFLICT : [cC] [oO] [nN] [fF] [lL] [iI] [cC] [tT];
CONSTRAINT : [cC] [oO] [nN] [sS] [tT] [rR] [aA] [iI] [nN] [tT];
COUNT : [cC] [oO] [uU] [nN] [tT];

View File

@ -1083,6 +1083,7 @@ function
| collectionAggregateFunction
| collectionFunctionMisuse
| jpaNonstandardFunction
| columnFunction
| genericFunction
;
@ -1090,7 +1091,7 @@ function
* A syntax for calling user-defined or native database functions, required by JPQL
*/
jpaNonstandardFunction
: FUNCTION LEFT_PAREN jpaNonstandardFunctionName (COMMA genericFunctionArguments)? RIGHT_PAREN
: FUNCTION LEFT_PAREN jpaNonstandardFunctionName (AS castTarget)? (COMMA genericFunctionArguments)? RIGHT_PAREN
;
/**
@ -1098,8 +1099,13 @@ jpaNonstandardFunction
*/
jpaNonstandardFunctionName
: STRING_LITERAL
| identifier
;
columnFunction
: COLUMN LEFT_PAREN path DOT jpaNonstandardFunctionName (AS castTarget)? RIGHT_PAREN
;
/**
* Any function invocation that follows the regular syntax
*
@ -1618,6 +1624,7 @@ rollup
| CASE
| CAST
| COLLATE
| COLUMN
| CONFLICT
| CONSTRAINT
| COUNT

View File

@ -0,0 +1,65 @@
/*
* 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;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.hql.HqlInterpretationException;
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.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.update.Assignable;
import org.hibernate.type.BasicType;
import org.hibernate.type.JavaObjectType;
import java.util.List;
/**
* @author Gavin King
*/
public class SqlColumn extends AbstractSqmSelfRenderingFunctionDescriptor {
private final String columnName;
public SqlColumn(String columnName, BasicType<?> type) {
super(
"column",
StandardArgumentsValidators.min( 1 ),
StandardFunctionReturnTypeResolvers.invariant( type == null ? JavaObjectType.INSTANCE : type ),
null
);
this.columnName = columnName;
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
final SqlAstNode sqlAstNode = arguments.get(0);
final ColumnReference reference;
if ( sqlAstNode instanceof Assignable ) {
final Assignable assignable = (Assignable) sqlAstNode;
reference = assignable.getColumnReferences().get(0);
}
else if ( sqlAstNode instanceof Expression ) {
final Expression expression = (Expression) sqlAstNode;
reference = expression.getColumnReference();
}
else {
throw new HqlInterpretationException( "path did not map to a column" );
}
sqlAppender.appendSql( reference.getQualifier() );
sqlAppender.appendSql( '.' );
sqlAppender.appendSql( columnName );
}
}

View File

@ -62,7 +62,7 @@ public interface ConnectionProvider extends Service, Wrapped {
* <p>
* Typically, this is only true in managed environments where a container tracks connections
* by transaction or thread.
*
* <p>
* Note that JTA semantic depends on the fact that the underlying connection provider does
* support aggressive release.
*

View File

@ -35,6 +35,7 @@ import java.util.Set;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.registry.classloading.spi.ClassLoadingException;
import org.hibernate.dialect.function.SqlColumn;
import org.hibernate.grammars.hql.HqlLexer;
import org.hibernate.grammars.hql.HqlParser;
import org.hibernate.grammars.hql.HqlParserBaseVisitor;
@ -58,7 +59,6 @@ import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor;
import org.hibernate.query.NullPrecedence;
import org.hibernate.query.ParameterLabelException;
import org.hibernate.query.PathException;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.SemanticException;
import org.hibernate.query.SortDirection;
import org.hibernate.query.SyntaxException;
@ -277,6 +277,9 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
private static final Logger log = Logger.getLogger( SemanticQueryBuilder.class );
private static final Set<String> JPA_STANDARD_FUNCTIONS;
private static final BasicTypeImpl<Object> OBJECT_BASIC_TYPE =
new BasicTypeImpl<>( new UnknownBasicJavaType<>(Object.class), ObjectJdbcType.INSTANCE );
static {
final Set<String> jpaStandardFunctions = new HashSet<>();
// Extracted from the BNF in JPA spec 4.14.
@ -3910,43 +3913,64 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Functions
private String toName(HqlParser.JpaNonstandardFunctionNameContext ctx) {
return ctx.STRING_LITERAL() == null
? ctx.identifier().getText().toLowerCase()
: unquoteStringLiteral( ctx.STRING_LITERAL().getText() ).toLowerCase();
}
@Override
public SqmExpression<?> visitJpaNonstandardFunction(HqlParser.JpaNonstandardFunctionContext ctx) {
final String functionName = unquoteStringLiteral( ctx.jpaNonstandardFunctionName().getText() ).toLowerCase();
final List<SqmTypedNode<?>> functionArguments;
if ( ctx.getChildCount() > 4 ) {
//noinspection unchecked
functionArguments = (List<SqmTypedNode<?>>) ctx.genericFunctionArguments().accept( this );
}
else {
functionArguments = emptyList();
}
final String functionName = toName( ctx.jpaNonstandardFunctionName() );
final HqlParser.GenericFunctionArgumentsContext argumentsContext = ctx.genericFunctionArguments();
@SuppressWarnings("unchecked")
final List<SqmTypedNode<?>> functionArguments =
argumentsContext == null
? emptyList()
: (List<SqmTypedNode<?>>) argumentsContext.accept(this);
final BasicType<?> returnableType = returnType( ctx.castTarget() );
SqmFunctionDescriptor functionTemplate = getFunctionDescriptor( functionName );
if (functionTemplate == null) {
if ( functionTemplate == null ) {
functionTemplate = new NamedSqmFunctionDescriptor(
functionName,
true,
null,
StandardFunctionReturnTypeResolvers.invariant(
new BasicTypeImpl<>(
new UnknownBasicJavaType<>( Object.class ),
ObjectJdbcType.INSTANCE
)
),
StandardFunctionReturnTypeResolvers.invariant(returnableType),
null
);
}
return functionTemplate.generateSqmExpression(
functionArguments,
null,
returnableType,
creationContext.getQueryEngine()
);
}
@Override
public SqmExpression<?> visitColumnFunction(HqlParser.ColumnFunctionContext ctx) {
final String columnName = toName( ctx.jpaNonstandardFunctionName() );
final SemanticPathPart semanticPathPart = visitPath( ctx.path() );
final BasicType<?> resultType = returnType( ctx.castTarget() );
return new SqlColumn( columnName, resultType ).generateSqmExpression(
(SqmTypedNode<?>) semanticPathPart,
resultType,
creationContext.getQueryEngine()
);
}
private BasicType<?> returnType(HqlParser.CastTargetContext castTarget) {
if ( castTarget == null ) {
return OBJECT_BASIC_TYPE;
}
else {
return (BasicType<?>) visitCastTarget( castTarget ).getType();
}
}
@Override
public String visitGenericFunctionName(HqlParser.GenericFunctionNameContext ctx) {
StringBuilder functionName = new StringBuilder( visitIdentifier( ctx.simplePath().identifier() ) );
final StringBuilder functionName = new StringBuilder( visitIdentifier( ctx.simplePath().identifier() ) );
for ( HqlParser.SimplePathElementContext sp: ctx.simplePath().simplePathElement() ) {
// allow function names of form foo.bar to be located in the registry
functionName.append('.').append( visitIdentifier( sp.identifier() ) );
@ -4474,9 +4498,8 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
final Integer scale = secondArg == null ? null : Integer.valueOf( secondArg.getText() );
return new SqmCastTarget<>(
(ReturnableType<?>)
creationContext.getTypeConfiguration()
.resolveCastTargetType( targetName ),
creationContext.getTypeConfiguration()
.resolveCastTargetType( targetName ),
//TODO: is there some way to interpret as length vs precision/scale here at this point?
length,
precision,

View File

@ -41,7 +41,6 @@ import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.SessionFactoryRegistry;
import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.metamodel.model.domain.DomainType;
import org.hibernate.metamodel.model.domain.JpaMetamodel;
import org.hibernate.metamodel.model.domain.SingularPersistentAttribute;
@ -468,7 +467,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext,
@Override
public <X, T> SqmExpression<X> cast(JpaExpression<T> expression, Class<X> castTargetJavaType) {
final BasicDomainType<X> type = getTypeConfiguration().standardBasicTypeForJavaType( castTargetJavaType );
final BasicType<X> type = getTypeConfiguration().standardBasicTypeForJavaType( castTargetJavaType );
return getFunctionDescriptor( "cast" ).generateSqmExpression(
asList( (SqmTypedNode<?>) expression, new SqmCastTarget<>( type, this ) ),
type,

View File

@ -27,6 +27,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
@ -323,7 +324,7 @@ public class TypeConfiguration implements SessionFactoryObserver, Serializable {
* <p>
* The type names are not case-sensitive.
*/
public BasicValuedMapping resolveCastTargetType(String name) {
public BasicType<?> resolveCastTargetType(String name) {
switch ( name.toLowerCase() ) {
case "string": return getBasicTypeForJavaType( String.class );
case "character": return getBasicTypeForJavaType( Character.class );
@ -346,6 +347,7 @@ public class TypeConfiguration implements SessionFactoryObserver, Serializable {
case "duration": return getBasicTypeForJavaType( Duration.class );
case "instant": return getBasicTypeForJavaType( Instant.class );
case "binary": return getBasicTypeForJavaType( byte[].class );
case "uuid": return getBasicTypeForJavaType( UUID.class );
//this one is very fragile ... works well for BIT or BOOLEAN columns only
//works OK, I suppose, for integer columns, but not at all for char columns
case "boolean": return getBasicTypeForJavaType( Boolean.class );
@ -353,7 +355,7 @@ public class TypeConfiguration implements SessionFactoryObserver, Serializable {
case "yesno": return basicTypeRegistry.getRegisteredType( StandardBasicTypes.YES_NO.getName() );
case "numericboolean": return basicTypeRegistry.getRegisteredType( StandardBasicTypes.NUMERIC_BOOLEAN.getName() );
default: {
final BasicType<Object> registeredBasicType = basicTypeRegistry.getRegisteredType( name );
final BasicType<?> registeredBasicType = basicTypeRegistry.getRegisteredType( name );
if ( registeredBasicType != null ) {
return registeredBasicType;
}

View File

@ -54,6 +54,7 @@ import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.hamcrest.Matchers;
@ -61,7 +62,6 @@ import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.isOneOf;
import static org.hibernate.cfg.QuerySettings.PORTABLE_INTEGER_DIVISION;
import static org.hibernate.testing.orm.domain.gambit.EntityOfBasics.Gender.FEMALE;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -92,6 +92,7 @@ public class FunctionTests {
entity.setTheTime( new Time( 20, 10, 8 ) );
entity.setTheDuration( Duration.of(3, ChronoUnit.SECONDS).plus( Duration.of(23,ChronoUnit.MILLIS) ) );
entity.setTheTimestamp( new Timestamp( 121, 4, 27, 13, 22, 50, 123456789 ) );
entity.setTheUuid( UUID.randomUUID() );
em.persist(entity);
EntityOfLists eol = new EntityOfLists(1,"");
@ -1280,10 +1281,14 @@ public class FunctionTests {
session -> {
assertThat( session.createQuery("select function('lower','HIBERNATE')", String.class).getSingleResult(),
equalTo("hibernate") );
assertThat( session.createQuery("select function(lower as String,'HIBERNATE')", String.class).getSingleResult(),
equalTo("hibernate") );
assertThat( session.createQuery("select 1 where function('lower','HIBERNATE') = 'hibernate'", Integer.class).getSingleResult(),
equalTo(1) );
assertThat( session.createQuery("select function('current_user')", String.class).getSingleResult().toLowerCase(),
isOneOf("hibernate_orm_test", "hibernateormtest", "sa", "hibernateormtest@%", "hibernate_orm_test@%", "root@%") );
assertThat( session.createQuery("select function(current_user as String)", String.class).getSingleResult().toLowerCase(),
isOneOf("hibernate_orm_test", "hibernateormtest", "sa", "hibernateormtest@%", "hibernate_orm_test@%", "root@%") );
assertThat( session.createQuery("select lower(function('current_user'))", String.class).getSingleResult(),
isOneOf("hibernate_orm_test", "hibernateormtest", "sa", "hibernateormtest@%", "hibernate_orm_test@%", "root@%") );
session.createQuery("select 1 where function('current_user') = 'hibernate_orm_test'", Integer.class).getSingleResultOrNull();
@ -2268,4 +2273,38 @@ public class FunctionTests {
.getSingleResultOrNull());
});
}
@Test
public void testColumnFunction(SessionFactoryScope scope) {
scope.inTransaction(s -> {
assertEquals("the string",
s.createSelectionQuery("select column(e.the_column) from EntityOfBasics e", String.class)
.getSingleResultOrNull());
assertEquals("the string",
s.createSelectionQuery("select column(e.'the_column') from EntityOfBasics e", String.class)
.getSingleResultOrNull());
s.createSelectionQuery("from EntityOfBasics e where column(e.the_column as String) = 'the string'", EntityOfBasics.class)
.getSingleResult();
});
}
@Test
public void testUUIDColumnFunction(SessionFactoryScope scope) {
scope.inTransaction(s -> {
byte[] bytes = s.createSelectionQuery("select column(e.theuuid as binary) from EntityOfBasics e", byte[].class)
.getSingleResultOrNull();
UUID uuid = s.createSelectionQuery("select column(e.theuuid as UUID) from EntityOfBasics e", UUID.class)
.getSingleResultOrNull();
});
}
@Test @RequiresDialect(PostgreSQLDialect.class)
public void testCtidColumnFunction(SessionFactoryScope scope) {
scope.inTransaction(s -> {
String string = s.createSelectionQuery("select column(e.ctid as String) from EntityOfBasics e", String.class)
.getSingleResultOrNull();
byte[] bytes = s.createSelectionQuery("select column(e.ctid as binary) from EntityOfBasics e", byte[].class)
.getSingleResultOrNull();
});
}
}

View File

@ -17,6 +17,8 @@ import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.UUID;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
@ -68,6 +70,7 @@ public class EntityOfBasics {
private Gender convertedGender;
private Gender ordinalGender;
private Duration theDuration;
private UUID theUuid;
private LocalDateTime theLocalDateTime;
private LocalDate theLocalDate;
@ -77,6 +80,8 @@ public class EntityOfBasics {
private MutableValue mutableValue;
private String theField = "the string";
public EntityOfBasics() {
}
@ -272,6 +277,15 @@ public class EntityOfBasics {
this.theDuration = theDuration;
}
@Column(name = "theuuid")
public UUID getTheUuid() {
return theUuid;
}
public void setTheUuid(UUID theUuid) {
this.theUuid = theUuid;
}
public Boolean isTheBoolean() {
return theBoolean;
}
@ -298,6 +312,15 @@ public class EntityOfBasics {
this.theStringBoolean = theStringBoolean;
}
@Column(name = "the_column")
public String getTheField() {
return theField;
}
public void setTheField(String theField) {
this.theField = theField;
}
@Convert( converter = MutableValueConverter.class )
public MutableValue getMutableValue() {
return mutableValue;

View File

@ -135,6 +135,9 @@ public abstract class MockSessionFactory
implements SessionFactoryImplementor, QueryEngine, RuntimeModelCreationContext, MetadataBuildingOptions,
BootstrapContext, MetadataBuildingContext, FunctionContributions, SessionFactoryOptions, JdbcTypeIndicators {
private static final BasicTypeImpl<Object> OBJECT_BASIC_TYPE =
new BasicTypeImpl<>(new UnknownBasicJavaType<>(Object.class), ObjectJdbcType.INSTANCE);
// static so other things can get at it
// TODO: make a static instance of this whole object instead!
static TypeConfiguration typeConfiguration;
@ -972,7 +975,7 @@ public abstract class MockSessionFactory
return (DomainType<?>) elementType;
}
else {
return new BasicTypeImpl<>(new UnknownBasicJavaType<>(Object.class), ObjectJdbcType.INSTANCE);
return OBJECT_BASIC_TYPE;
}
}