HHH-15106 - fk() SQM function

This commit is contained in:
Steve Ebersole 2022-03-03 15:08:40 -06:00
parent 6e6cc5f06e
commit 362b4c0ac7
10 changed files with 451 additions and 1 deletions

View File

@ -139,6 +139,7 @@ ID : [iI][dD];
VERSION : [vV] [eE] [rR] [sS] [iI] [oO] [nN];
VERSIONED : [vV] [eE] [rR] [sS] [iI] [oO] [nN] [eE] [dD];
NATURALID : [nN] [aA] [tT] [uU] [rR] [aA] [lL] [iI] [dD];
FK : [fF] [kK];
ALL : [aA] [lL] [lL];
AND : [aA] [nN] [dD];

View File

@ -671,6 +671,7 @@ primaryExpression
| entityIdReference # EntityIdExpression
| entityVersionReference # EntityVersionExpression
| entityNaturalIdReference # EntityNaturalIdExpression
| toOneFkReference # ToOneFkExpression
| syntacticDomainPath pathContinuation? # SyntacticPathExpression
| function # FunctionExpression
| generalPathFragment # GeneralPathExpression
@ -737,6 +738,13 @@ entityNaturalIdReference
: NATURALID LEFT_PAREN path RIGHT_PAREN pathContinuation?
;
/**
* The special function 'fk()'
*/
toOneFkReference
: FK LEFT_PAREN path RIGHT_PAREN
;
/**
* A 'case' expression, which comes in two forms: "simple", and "searched"
*/

View File

@ -51,6 +51,7 @@ import org.hibernate.metamodel.internal.JpaMetaModelPopulationSetting;
import org.hibernate.metamodel.internal.JpaStaticMetaModelPopulationSetting;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess;
import org.hibernate.metamodel.model.domain.BasicDomainType;
import org.hibernate.metamodel.model.domain.EmbeddableDomainType;
import org.hibernate.metamodel.model.domain.EntityDomainType;
import org.hibernate.metamodel.model.domain.ManagedDomainType;
@ -774,6 +775,11 @@ public class MappingMetamodelImpl implements MappingMetamodelImplementor, Metamo
return (BasicType<?>) sqmExpressible;
}
if ( sqmExpressible instanceof BasicDomainType ) {
final BasicDomainType<?> domainType = (BasicDomainType<?>) sqmExpressible;
return getTypeConfiguration().getBasicTypeForJavaType( domainType.getExpressibleJavaType().getJavaTypeClass() );
}
if ( sqmExpressible instanceof BasicSqmPathSource<?> ) {
return getTypeConfiguration().getBasicTypeForJavaType(((BasicSqmPathSource<?>) sqmExpressible).getJavaType());
}

View File

@ -53,6 +53,7 @@ import org.hibernate.metamodel.model.domain.ManagedDomainType;
import org.hibernate.metamodel.model.domain.PersistentAttribute;
import org.hibernate.metamodel.model.domain.PluralPersistentAttribute;
import org.hibernate.metamodel.model.domain.SingularPersistentAttribute;
import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource;
import org.hibernate.query.PathException;
import org.hibernate.query.ReturnableType;
import org.hibernate.query.SemanticException;
@ -102,6 +103,8 @@ import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
import org.hibernate.query.sqm.tree.domain.AbstractSqmFrom;
import org.hibernate.query.sqm.tree.domain.SqmCorrelation;
import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmFkExpression;
import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction;
import org.hibernate.query.sqm.tree.domain.SqmListJoin;
import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference;
@ -191,6 +194,7 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.jboss.logging.Logger;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.metamodel.Bindable;
import jakarta.persistence.metamodel.SingularAttribute;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
@ -2275,6 +2279,34 @@ public class SemanticQueryBuilder<R> extends HqlParserBaseVisitor<Object> implem
throw new SemanticException( "Path does not resolve to an entity type '" + sqmPath.getNavigablePath().getFullPath() + "'" );
}
@Override
public Object visitToOneFkExpression(HqlParser.ToOneFkExpressionContext ctx) {
return visitToOneFkReference( (HqlParser.ToOneFkReferenceContext) ctx.getChild( 0 ) );
}
@Override
public SqmFkExpression<?> visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) {
final SqmPath<Object> sqmPath = consumeDomainPath( (HqlParser.PathContext) ctx.getChild( 2 ) );
final SqmPathSource<?> toOneReference = sqmPath.getReferencedPathSource();
final boolean validToOneRef = toOneReference.getBindableType() == Bindable.BindableType.SINGULAR_ATTRIBUTE
&& toOneReference instanceof EntitySqmPathSource;
if ( !validToOneRef ) {
throw new SemanticException(
String.format(
Locale.ROOT,
"`%s` used in `fk()` only supported for to-one mappings, but found `%s`",
sqmPath.getNavigablePath().getFullPath(),
toOneReference
)
);
}
return new SqmFkExpression( (SqmEntityValuedSimplePath<?>) sqmPath, creationContext.getNodeBuilder() );
}
@Override
public SqmMapEntryReference<?, ?> visitMapEntrySelection(HqlParser.MapEntrySelectionContext ctx) {
return new SqmMapEntryReference<>(

View File

@ -18,6 +18,7 @@ import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmCorrelation;
import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmFkExpression;
import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath;
import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference;
import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction;
@ -147,6 +148,8 @@ public interface SemanticQueryWalker<T> {
T visitPluralValuedPath(SqmPluralValuedSimplePath<?> path);
T visitFkExpression(SqmFkExpression<?> fkExpression);
T visitSelfInterpretingSqmPath(SelfInterpretingSqmPath<?> sqmPath);
T visitIndexedPluralAccessPath(SqmIndexedCollectionAccessPath<?> path);

View File

@ -23,6 +23,7 @@ import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmCorrelation;
import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmFkExpression;
import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath;
import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference;
import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction;
@ -614,6 +615,13 @@ public class SqmTreePrinter implements SemanticQueryWalker<Object> {
return null;
}
@Override
public Object visitFkExpression(SqmFkExpression<?> fkExpression) {
logWithIndentation( "-> [fk-ref] - `%s`", fkExpression.getToOnePath().getNavigablePath().getFullPath() );
return null;
}
@Override
public Object visitSelfInterpretingSqmPath(SelfInterpretingSqmPath<?> sqmPath) {
logWithIndentation( "-> [self-interpreting-path] - `%s`", sqmPath.getNavigablePath().getFullPath() );

View File

@ -21,6 +21,7 @@ import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmCorrelation;
import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmFkExpression;
import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath;
import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference;
import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction;
@ -309,6 +310,11 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker<Obj
return path;
}
@Override
public Object visitFkExpression(SqmFkExpression<?> fkExpression) {
return fkExpression;
}
@Override
public Object visitSelfInterpretingSqmPath(SelfInterpretingSqmPath<?> path) {
return path;

View File

@ -162,6 +162,7 @@ import org.hibernate.query.sqm.tree.domain.SqmCorrelation;
import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction;
import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath;
import org.hibernate.query.sqm.tree.domain.SqmFkExpression;
import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction;
import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath;
import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference;
@ -384,6 +385,7 @@ import static org.hibernate.query.sqm.TemporalUnit.EPOCH;
import static org.hibernate.query.sqm.TemporalUnit.NATIVE;
import static org.hibernate.query.sqm.TemporalUnit.SECOND;
import static org.hibernate.query.sqm.UnaryArithmeticOperator.UNARY_MINUS;
import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey;
import static org.hibernate.type.spi.TypeConfiguration.isDuration;
/**
@ -3261,7 +3263,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
);
final Expression expression = getSqlExpressionResolver().resolveSqlExpression(
SqlExpressionResolver.createColumnReferenceKey(
createColumnReferenceKey(
tableReference,
mapping.getSelectionExpression()
),
@ -3449,6 +3451,51 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
);
}
@Override
public Object visitFkExpression(SqmFkExpression<?> fkExpression) {
final EntityValuedPathInterpretation<?> toOneInterpretation = (EntityValuedPathInterpretation<?>) visitEntityValuedPath( fkExpression.getToOnePath() );
assert toOneInterpretation.getExpressionType() instanceof ToOneAttributeMapping;
final ToOneAttributeMapping toOneMapping = (ToOneAttributeMapping) toOneInterpretation.getExpressionType();
final ForeignKeyDescriptor fkDescriptor = toOneMapping.getForeignKeyDescriptor();
final TableGroup tableGroup = toOneInterpretation.getTableGroup();
final TableReference tableReference = tableGroup.resolveTableReference( fkDescriptor.getKeyTable() );
final ModelPart fkKeyPart = fkDescriptor.getKeyPart();
if ( fkKeyPart instanceof BasicValuedModelPart ) {
final BasicValuedModelPart basicFkPart = (BasicValuedModelPart) fkKeyPart;
return getSqlExpressionResolver().resolveSqlExpression(
createColumnReferenceKey( tableReference, basicFkPart.getSelectionExpression() ),
(sqlAstProcessingState) -> new ColumnReference(
tableReference,
basicFkPart,
creationContext.getSessionFactory()
)
);
}
else {
assert fkKeyPart instanceof EmbeddableValuedModelPart;
final EmbeddableValuedModelPart compositeFkPart = (EmbeddableValuedModelPart) fkKeyPart;
final List<JdbcMapping> jdbcMappings = compositeFkPart.getJdbcMappings();
final List<Expression> tupleElements = new ArrayList<>( jdbcMappings.size() );
compositeFkPart.forEachSelectable( (position, selectable) -> {
tupleElements.add(
getSqlExpressionResolver().resolveSqlExpression(
createColumnReferenceKey( tableReference, selectable.getSelectionExpression() ),
(sqlAstProcessingState) -> new ColumnReference(
tableReference,
selectable,
creationContext.getSessionFactory()
)
)
);
} );
return new SqlTuple( tupleElements, compositeFkPart );
}
}
@Override
public Object visitSelfInterpretingSqmPath(SelfInterpretingSqmPath<?> sqmPath) {
return prepareReusablePath(

View File

@ -0,0 +1,59 @@
/*
* 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.query.sqm.tree.domain;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.SemanticQueryWalker;
import org.hibernate.query.sqm.SqmExpressible;
import org.hibernate.query.sqm.tree.SqmCopyContext;
import org.hibernate.query.sqm.tree.expression.AbstractSqmExpression;
import org.hibernate.query.sqm.tree.expression.SqmExpression;
/**
* Reference to the key-side (as opposed to the target-side) of the
* foreign-key of a to-one association.
*
* @author Steve Ebersole
*/
public class SqmFkExpression<T> extends AbstractSqmExpression<T> {
private final SqmEntityValuedSimplePath<?> toOnePath;
public SqmFkExpression(SqmEntityValuedSimplePath<?> toOnePath, NodeBuilder criteriaBuilder) {
//noinspection unchecked
super( (SqmExpressible<? extends T>) toOnePath.getNodeType().getIdType(), criteriaBuilder );
this.toOnePath = toOnePath;
}
public SqmEntityValuedSimplePath<?> getToOnePath() {
return toOnePath;
}
@Override
public <X> X accept(SemanticQueryWalker<X> walker) {
return walker.visitFkExpression( this );
}
@Override
public void appendHqlString(StringBuilder sb) {
sb.append( "fk(" );
toOnePath.appendHqlString( sb );
sb.append( ')' );
}
@Override
public SqmExpression<T> copy(SqmCopyContext context) {
final SqmFkExpression<T> existing = context.getCopy( this );
if ( existing != null ) {
return existing;
}
return context.registerCopy(
this,
new SqmFkExpression<T>( toOnePath.copy( context ), nodeBuilder() )
);
}
}

View File

@ -0,0 +1,280 @@
/*
* 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.notfound;
import java.io.Serializable;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction;
import org.hibernate.query.SemanticException;
import org.hibernate.query.sqm.ParsingException;
import org.hibernate.testing.jdbc.SQLStatementInspector;
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.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for the new `{fk}` HQL token
*
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = { FkRefTests.Coin.class, FkRefTests.Currency.class } )
@SessionFactory( useCollectingStatementInspector = true )
public class FkRefTests {
@Test
@JiraKey( "HHH-15099" )
public void testSimplePredicateUse(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// there is a Coin which has a currency_fk = 1
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where fk(c.currency) = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ) ).isNotNull();
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
} );
statementInspector.clear();
// However, the "matching" Currency does not exist
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.id = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 0 );
} );
statementInspector.clear();
// check using `currency` as a naked "property-ref"
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where fk(currency) = 1";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ) ).isNotNull();
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 2 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
} );
}
/**
* Baseline test for {@link #testNullnessPredicateUse}. Here we use the
* normal "target" reference, which for a not-found mapping should trigger
* a join to the association table and use the fk-target column
*/
@Test
@JiraKey( "HHH-15099" )
public void testNullnessPredicateUseBaseline(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// there is one Coin (id=3) which has a null currency_fk, however its
// target is missing (broken "fk"). this should return no results
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where c.currency.id is null";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 0 );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( " join " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " left " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " inner " );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " cross " );
} );
}
/**
* It is not ideal that we render the join for these. Need to come back and address that
*/
@Test
@JiraKey( "HHH-15099" )
public void testNullnessPredicateUse(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
// there is one Coin (id=3) which has a null currency_fk
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where fk(c.currency) is null";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ) ).isNotNull();
assertThat( coins.get( 0 ).getId() ).isEqualTo( 3 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
} );
statementInspector.clear();
// check using `currency` as a naked "property-ref"
scope.inTransaction( (session) -> {
final String hql = "select c from Coin c where fk(currency) is null";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
assertThat( coins ).hasSize( 1 );
assertThat( coins.get( 0 ) ).isNotNull();
assertThat( coins.get( 0 ).getId() ).isEqualTo( 3 );
assertThat( coins.get( 0 ).getCurrency() ).isNull();
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( " join " );
} );
}
@Test
@JiraKey( "HHH-15099" )
public void testFkRefDereferenceNotAllowed(SessionFactoryScope scope) {
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
try {
final String hql = "select c from Coin c where fk(c.currency).something";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
}
catch (IllegalArgumentException expected) {
assertThat( expected.getCause() ).isInstanceOf( ParsingException.class );
}
} );
scope.inTransaction( (session) -> {
try {
final String hql = "select c from Coin c where fk(currency).something";
final List<Coin> coins = session.createQuery( hql, Coin.class ).getResultList();
}
catch (IllegalArgumentException expected) {
assertThat( expected.getCause() ).isInstanceOf( ParsingException.class );
}
} );
}
@BeforeEach
public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
Currency euro = new Currency( 1, "Euro" );
Coin fiveC = new Coin( 1, "Five cents", euro );
session.persist( euro );
session.persist( fiveC );
Currency usd = new Currency( 2, "USD" );
Coin penny = new Coin( 2, "Penny", usd );
session.persist( usd );
session.persist( penny );
Coin noCurrency = new Coin( 3, "N/A", null );
session.persist( noCurrency );
} );
// scope.inTransaction( (session) -> {
// session.createQuery( "delete Currency where id = 1" ).executeUpdate();
// } );
}
@AfterEach
public void cleanupTest(SessionFactoryScope scope) throws Exception {
scope.inTransaction( (session) -> {
session.createMutationQuery( "delete Coin" ).executeUpdate();
session.createMutationQuery( "delete Currency" ).executeUpdate();
} );
}
@Entity(name = "Coin")
public static class Coin {
private Integer id;
private String name;
private Currency currency;
public Coin() {
}
public Coin(Integer id, String name, Currency currency) {
this.id = id;
this.name = name;
this.currency = currency;
}
@Id
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@OneToOne(fetch = FetchType.LAZY)
@NotFound(action = NotFoundAction.IGNORE)
@JoinColumn( name = "currency_fk" )
public Currency getCurrency() {
return currency;
}
public void setCurrency(Currency currency) {
this.currency = currency;
}
}
@Entity(name = "Currency")
public static class Currency implements Serializable {
private Integer id;
private String name;
public Currency() {
}
public Currency(Integer id, String name) {
this.id = id;
this.name = name;
}
@Id
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}