From 6bef368208c8925d0e77a33c73c8aadc7ab3c038 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 2 Jun 2022 16:09:44 +0200 Subject: [PATCH] HHH-3356 Support for normal and lateral subquery in from clause --- .../org/hibernate/grammars/hql/HqlLexer.g4 | 1 + .../org/hibernate/grammars/hql/HqlParser.g4 | 14 +- .../metamodel/model/domain/TupleType.java | 12 + ...mousTupleBasicEntityIdentifierMapping.java | 69 +++ .../AnonymousTupleBasicValuedModelPart.java | 302 ++++++++++ ...onymousTupleEmbeddableValuedModelPart.java | 472 +++++++++++++++ .../AnonymousTupleEntityValuedModelPart.java | 536 ++++++++++++++++++ ...ymousTuplePersistentSingularAttribute.java | 122 ++++ .../AnonymousTupleSimpleSqmPathSource.java | 81 +++ .../internal/AnonymousTupleSqmPathSource.java | 105 ++++ .../AnonymousTupleTableGroupProducer.java | 298 ++++++++++ .../domain/internal/AnonymousTupleType.java | 198 +++++++ .../query/criteria/JpaDerivedFrom.java | 29 + .../query/criteria/JpaDerivedJoin.java | 18 + .../query/criteria/JpaDerivedRoot.java | 17 + .../org/hibernate/query/criteria/JpaFrom.java | 18 + .../org/hibernate/query/criteria/JpaRoot.java | 1 + .../query/criteria/JpaSelectCriteria.java | 30 + .../hibernate/query/criteria/JpaSubQuery.java | 43 +- .../query/hql/internal/QuerySplitter.java | 45 +- .../hql/internal/SemanticQueryBuilder.java | 85 ++- .../hql/internal/SqmPathRegistryImpl.java | 15 - .../query/sqm/SemanticQueryWalker.java | 6 + .../sqm/StrictJpaComplianceViolation.java | 1 + .../query/sqm/internal/QuerySqmImpl.java | 6 +- .../query/sqm/internal/SqmTreePrinter.java | 32 ++ .../internal/cte/CteInsertHandler.java | 2 +- .../mutation/spi/AbstractMutationHandler.java | 2 +- .../sqm/spi/BaseSemanticQueryWalker.java | 23 +- .../sqm/sql/BaseSqmToSqlAstConverter.java | 260 +++++++-- .../sqm/tree/domain/AbstractSqmFrom.java | 47 +- .../sqm/tree/domain/AbstractSqmPath.java | 14 +- .../sqm/tree/domain/SqmCorrelatedRoot.java | 2 +- .../query/sqm/tree/domain/SqmDerivedRoot.java | 133 +++++ .../query/sqm/tree/domain/SqmTreatedRoot.java | 2 +- .../query/sqm/tree/from/SqmDerivedJoin.java | 183 ++++++ .../query/sqm/tree/from/SqmRoot.java | 23 +- .../tree/select/AbstractSqmSelectQuery.java | 27 + .../sqm/tree/select/SqmSelectStatement.java | 14 + .../query/sqm/tree/select/SqmSubQuery.java | 200 +++++++ .../ast/tree/from/DelegatingTableGroup.java | 3 +- .../ast/tree/from/QueryPartTableGroup.java | 12 +- .../query/SubQueryInFromEmbeddedIdTests.java | 328 +++++++++++ .../query/SubQueryInFromIdClassTests.java | 309 ++++++++++ .../orm/test/query/SubQueryInFromTests.java | 345 +++++++++++ .../testing/orm/domain/contacts/Address.java | 10 +- .../testing/orm/domain/contacts/Contact.java | 12 + 47 files changed, 4404 insertions(+), 103 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicEntityIdentifierMapping.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicValuedModelPart.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEmbeddableValuedModelPart.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEntityValuedModelPart.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTuplePersistentSingularAttribute.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSimpleSqmPathSource.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSqmPathSource.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleTableGroupProducer.java create mode 100644 hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedFrom.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedRoot.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index e93e09175276..17ed4e542069 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -207,6 +207,7 @@ IS : [iI] [sS]; JOIN : [jJ] [oO] [iI] [nN]; KEY : [kK] [eE] [yY]; LAST : [lL] [aA] [sS] [tT]; +LATERAL : [lL] [aA] [tT] [eE] [rR] [aA] [lL]; LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG]; LEFT : [lL] [eE] [fF] [tT]; LIKE : [lL] [iI] [kK] [eE]; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 4753d4d9373e..a5a7fa8ead21 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -169,14 +169,15 @@ fromClause * The declaration of a root entity in 'from' clause, along with its joins */ entityWithJoins - : rootEntity (join | crossJoin | jpaCollectionJoin)* + : fromRoot (join | crossJoin | jpaCollectionJoin)* ; /** * A root entity declaration in the 'from' clause, with optional identification variable */ -rootEntity - : entityName variable? +fromRoot + : entityName variable? # RootEntity + | LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? # RootSubquery ; /** @@ -212,7 +213,7 @@ jpaCollectionJoin * A 'join', with an optional 'on' or 'with' clause */ join - : joinType JOIN FETCH? joinPath joinRestriction? + : joinType JOIN FETCH? joinTarget joinRestriction? ; /** @@ -226,8 +227,9 @@ joinType /** * The joined path, with an optional identification variable */ -joinPath - : path variable? +joinTarget + : path variable? #JoinPath + | LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? #JoinSubquery ; /** diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/TupleType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/TupleType.java index ee0c694a801d..566939201ad9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/TupleType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/TupleType.java @@ -8,7 +8,11 @@ import java.util.List; +import org.hibernate.Incubating; import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableGroupProducer; /** * Describes any structural type without a direct java type representation. @@ -23,4 +27,12 @@ public interface TupleType extends SqmExpressible { SqmExpressible get(int index); SqmExpressible get(String componentName); + + @Incubating + default TableGroupProducer resolveTableGroupProducer( + String aliasStem, + List sqlSelections, + FromClauseAccess fromClauseAccess) { + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicEntityIdentifierMapping.java new file mode 100644 index 000000000000..13aae78b0bed --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicEntityIdentifierMapping.java @@ -0,0 +1,69 @@ +/* + * 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.metamodel.model.domain.internal; + +import org.hibernate.Incubating; +import org.hibernate.engine.spi.IdentifierValue; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.query.sqm.SqmExpressible; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleBasicEntityIdentifierMapping extends AnonymousTupleBasicValuedModelPart + implements BasicEntityIdentifierMapping { + + private final BasicEntityIdentifierMapping delegate; + + public AnonymousTupleBasicEntityIdentifierMapping( + String selectionExpression, + SqmExpressible expressible, + JdbcMapping jdbcMapping, + BasicEntityIdentifierMapping delegate) { + super( delegate.getAttributeName(), selectionExpression, expressible, jdbcMapping ); + this.delegate = delegate; + } + + @Override + public IdentifierValue getUnsavedStrategy() { + return delegate.getUnsavedStrategy(); + } + + @Override + public Object getIdentifier(Object entity, SharedSessionContractImplementor session) { + return delegate.getIdentifier( entity, session ); + } + + @Override + public Object getIdentifier(Object entity) { + return delegate.getIdentifier( entity ); + } + + @Override + public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { + delegate.setIdentifier( entity, id, session ); + } + + @Override + public Object instantiate() { + return delegate.instantiate(); + } + + @Override + public PropertyAccess getPropertyAccess() { + return delegate.getPropertyAccess(); + } + + @Override + public String getAttributeName() { + return getPartName(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicValuedModelPart.java new file mode 100644 index 000000000000..b6a00efc504b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleBasicValuedModelPart.java @@ -0,0 +1,302 @@ +/* + * 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.metamodel.model.domain.internal; + +import java.util.function.BiConsumer; + +import org.hibernate.Incubating; +import org.hibernate.engine.FetchStyle; +import org.hibernate.engine.FetchTiming; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.mapping.IndexedConsumer; +import org.hibernate.metamodel.mapping.BasicValuedModelPart; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.FetchOptions; +import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.basic.BasicFetch; +import org.hibernate.sql.results.graph.basic.BasicResult; +import org.hibernate.type.descriptor.java.JavaType; + +import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleBasicValuedModelPart implements ModelPart, MappingType, BasicValuedModelPart { + + private static final FetchOptions FETCH_OPTIONS = FetchOptions.valueOf( FetchTiming.IMMEDIATE, FetchStyle.JOIN ); + private final String partName; + private final String selectionExpression; + private final SqmExpressible expressible; + private final JdbcMapping jdbcMapping; + + public AnonymousTupleBasicValuedModelPart( + String partName, + String selectionExpression, + SqmExpressible expressible, + JdbcMapping jdbcMapping) { + this.partName = partName; + this.selectionExpression = selectionExpression; + this.expressible = expressible; + this.jdbcMapping = jdbcMapping; + } + + @Override + public MappingType getPartMappingType() { + return this; + } + + @Override + public JavaType getJavaType() { + return expressible.getExpressibleJavaType(); + } + + @Override + public JavaType getMappedJavaType() { + return expressible.getExpressibleJavaType(); + } + + @Override + public String getPartName() { + return partName; + } + + @Override + public NavigableRole getNavigableRole() { + return null; + } + + @Override + public EntityMappingType findContainingEntityMapping() { + return null; + } + + @Override + public JdbcMapping getJdbcMapping() { + return jdbcMapping; + } + + @Override + public String getContainingTableExpression() { + return ""; + } + + @Override + public String getSelectionExpression() { + return selectionExpression; + } + + @Override + public String getCustomReadExpression() { + return null; + } + + @Override + public String getCustomWriteExpression() { + return null; + } + + @Override + public boolean isFormula() { + return false; + } + + @Override + public String getColumnDefinition() { + return null; + } + + @Override + public Long getLength() { + return null; + } + + @Override + public Integer getPrecision() { + return null; + } + + @Override + public Integer getScale() { + return null; + } + + @Override + public MappingType getMappedType() { + return this; + } + + @Override + public String getFetchableName() { + return partName; + } + + @Override + public FetchOptions getMappedFetchOptions() { + return FETCH_OPTIONS; + } + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + final SqlSelection sqlSelection = resolveSqlSelection( + navigablePath, + tableGroup, + null, + creationState.getSqlAstCreationState() + ); + + //noinspection unchecked + return new BasicResult( + sqlSelection.getValuesArrayPosition(), + resultVariable, + getJavaType(), + null, + navigablePath + ); + } + + private SqlSelection resolveSqlSelection( + NavigablePath navigablePath, + TableGroup tableGroup, + FetchParent fetchParent, + SqlAstCreationState creationState) { + final SqlExpressionResolver expressionResolver = creationState.getSqlExpressionResolver(); + final Expression expression = expressionResolver.resolveSqlExpression( + createColumnReferenceKey( tableGroup.getPrimaryTableReference(), getSelectionExpression() ), + sqlAstProcessingState -> new ColumnReference( + tableGroup.resolveTableReference( navigablePath, "" ), + this, + creationState.getCreationContext().getSessionFactory() + ) + ); + return expressionResolver.resolveSqlSelection( + expression, + getJdbcMapping().getJavaTypeDescriptor(), + fetchParent, + creationState.getCreationContext().getSessionFactory().getTypeConfiguration() + ); + } + + @Override + public BasicFetch generateFetch( + FetchParent fetchParent, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + boolean selected, + String resultVariable, + DomainResultCreationState creationState) { + final SqlAstCreationState sqlAstCreationState = creationState.getSqlAstCreationState(); + final TableGroup tableGroup = sqlAstCreationState.getFromClauseAccess().getTableGroup( + fetchParent.getNavigablePath() + ); + + assert tableGroup != null; + + final SqlSelection sqlSelection = resolveSqlSelection( + fetchablePath, + tableGroup, + fetchParent, + creationState.getSqlAstCreationState() + ); + + return new BasicFetch<>( + sqlSelection.getValuesArrayPosition(), + fetchParent, + fetchablePath, + this, + null, + fetchTiming, + creationState + ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + resolveSqlSelection( navigablePath, tableGroup, null, creationState.getSqlAstCreationState() ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + selectionConsumer.accept( + resolveSqlSelection( navigablePath, tableGroup, null, creationState.getSqlAstCreationState() ), + getJdbcMapping() + ); + } + + @Override + public int forEachDisassembledJdbcValue( + Object value, + Clause clause, + int offset, + JdbcValuesConsumer valuesConsumer, + SharedSessionContractImplementor session) { + valuesConsumer.consume( offset, value, getJdbcMapping() ); + return getJdbcTypeCount(); + } + + @Override + public int forEachJdbcType(int offset, IndexedConsumer action) { + action.accept( offset, getJdbcMapping() ); + return getJdbcTypeCount(); + } + + @Override + public void breakDownJdbcValues(Object domainValue, JdbcValueConsumer valueConsumer, SharedSessionContractImplementor session) { + valueConsumer.consume( domainValue, this ); + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + return value; + } + + @Override + public int forEachJdbcValue(Object value, Clause clause, int offset, JdbcValuesConsumer valuesConsumer, SharedSessionContractImplementor session) { + valuesConsumer.consume( offset, value, getJdbcMapping() ); + return getJdbcTypeCount(); + } + + @Override + public int forEachSelectable(int offset, SelectableConsumer consumer) { + consumer.accept( offset, this ); + return getJdbcTypeCount(); + } + + @Override + public int forEachJdbcType(IndexedConsumer action) { + action.accept( 0, getJdbcMapping() ); + return getJdbcTypeCount(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEmbeddableValuedModelPart.java new file mode 100644 index 000000000000..9fd31b8e4af0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEmbeddableValuedModelPart.java @@ -0,0 +1,472 @@ +/* + * 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.metamodel.model.domain.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.hibernate.Incubating; +import org.hibernate.engine.FetchStyle; +import org.hibernate.engine.FetchTiming; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.mapping.IndexedConsumer; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectableMappings; +import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; +import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.metamodel.spi.EmbeddableRepresentationStrategy; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAstCreationContext; +import org.hibernate.sql.ast.spi.SqlAstCreationState; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.StandardVirtualTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableGroupProducer; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.FetchOptions; +import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValuedModelPart, EmbeddableMappingType { + + private static final FetchOptions FETCH_OPTIONS = FetchOptions.valueOf( FetchTiming.IMMEDIATE, FetchStyle.JOIN ); + + private final Map modelParts; + private final DomainType domainType; + private final String componentName; + private final EmbeddableValuedModelPart existingModelPartContainer; + + public AnonymousTupleEmbeddableValuedModelPart( + Map modelParts, + DomainType domainType, + String componentName, + EmbeddableValuedModelPart existingModelPartContainer) { + this.modelParts = modelParts; + this.domainType = domainType; + this.componentName = componentName; + this.existingModelPartContainer = existingModelPartContainer; + } + + @Override + public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { + return modelParts.get( name ); + } + + @Override + public void visitSubParts(Consumer consumer, EntityMappingType treatTargetType) { + modelParts.values().forEach( consumer ); + } + + @Override + public MappingType getPartMappingType() { + return this; + } + + @Override + public JavaType getJavaType() { + return domainType.getExpressibleJavaType(); + } + + @Override + public String getPartName() { + return componentName; + } + + @Override + public int getJdbcTypeCount() { + return existingModelPartContainer.getJdbcTypeCount(); + } + + @Override + public EmbeddableMappingType getEmbeddableTypeDescriptor() { + return this; + } + + @Override + public EmbeddableValuedModelPart getEmbeddedValueMapping() { + return this; + } + + @Override + public EmbeddableRepresentationStrategy getRepresentationStrategy() { + return existingModelPartContainer.getEmbeddableTypeDescriptor() + .getRepresentationStrategy(); + } + + @Override + public boolean isCreateEmptyCompositesEnabled() { + return false; + } + + @Override + public EmbeddableMappingType createInverseMappingType( + EmbeddedAttributeMapping valueMapping, + TableGroupProducer declaringTableGroupProducer, + SelectableMappings selectableMappings, + MappingModelCreationProcess creationProcess) { + throw new UnsupportedOperationException(); + } + + @Override + public int getNumberOfAttributeMappings() { + return modelParts.size(); + } + + @Override + public AttributeMapping getAttributeMapping(int position) { + throw new UnsupportedOperationException(); + } + + @Override + public List getAttributeMappings() { + throw new UnsupportedOperationException(); + } + + @Override + public void visitAttributeMappings(Consumer action) { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] getValues(Object instance) { + return existingModelPartContainer.getEmbeddableTypeDescriptor() + .getValues( instance ); + } + + @Override + public Object getValue(Object instance, int position) { + return existingModelPartContainer.getEmbeddableTypeDescriptor() + .getAttributeMapping( position ) + .getValue( instance ); + } + + @Override + public void setValues(Object instance, Object[] resolvedValues) { + existingModelPartContainer.getEmbeddableTypeDescriptor() + .setValues( instance, resolvedValues ); + } + + @Override + public void setValue(Object instance, int position, Object value) { + existingModelPartContainer.getEmbeddableTypeDescriptor() + .getAttributeMapping( position ) + .setValue( instance, value ); + } + + @Override + public SelectableMapping getSelectable(int columnIndex) { + final List results = new ArrayList<>(); + forEachSelectable( (index, selection) -> results.add( selection ) ); + return results.get( columnIndex ); + } + + @Override + public List getJdbcMappings() { + final List results = new ArrayList<>(); + forEachSelectable( (index, selection) -> results.add( selection.getJdbcMapping() ) ); + return results; + } + + @Override + public int forEachSelectable(SelectableConsumer consumer) { + return forEachSelectable( 0, consumer ); + } + + @Override + public int forEachSelectable(int offset, SelectableConsumer consumer) { + int span = 0; + for ( ModelPart mapping : modelParts.values() ) { + span += mapping.forEachSelectable( offset + span, consumer ); + } + return span; + } + + @Override + public String getContainingTableExpression() { + return ""; + } + + @Override + public SqlTuple toSqlExpression( + TableGroup tableGroup, + Clause clause, + SqmToSqlAstConverter walker, + SqlAstCreationState sqlAstCreationState) { + final List columnReferences = CollectionHelper.arrayList( getJdbcTypeCount() ); + final NavigablePath navigablePath = tableGroup.getNavigablePath().append( componentName ); + final TableReference tableReference = tableGroup.resolveTableReference( navigablePath, getContainingTableExpression() ); + for ( ModelPart modelPart : modelParts.values() ) { + modelPart.forEachSelectable( + (columnIndex, selection) -> { + final Expression columnReference = sqlAstCreationState.getSqlExpressionResolver() + .resolveSqlExpression( + SqlExpressionResolver.createColumnReferenceKey( + tableReference, + selection.getSelectionExpression() + ), + sqlAstProcessingState -> new ColumnReference( + tableReference.getIdentificationVariable(), + selection, + sqlAstCreationState.getCreationContext().getSessionFactory() + ) + ); + + columnReferences.add( columnReference.getColumnReference() ); + } + ); + } + + return new SqlTuple( columnReferences, this ); + } + + @Override + public JavaType getMappedJavaType() { + return existingModelPartContainer.getJavaType(); + } + + @Override + public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) { + return SqlAstJoinType.INNER; + } + + @Override + public boolean isSimpleJoinPredicate(Predicate predicate) { + return predicate == null; + } + + @Override + public TableGroupJoin createTableGroupJoin( + NavigablePath navigablePath, + TableGroup lhs, + String explicitSourceAlias, + SqlAstJoinType requestedJoinType, + boolean fetched, + boolean addsPredicate, + SqlAliasBaseGenerator aliasBaseGenerator, + SqlExpressionResolver sqlExpressionResolver, + FromClauseAccess fromClauseAccess, + SqlAstCreationContext creationContext) { + final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final TableGroup tableGroup = createRootTableGroupJoin( + navigablePath, + lhs, + explicitSourceAlias, + requestedJoinType, + fetched, + null, + aliasBaseGenerator, + sqlExpressionResolver, + fromClauseAccess, + creationContext + ); + + return new TableGroupJoin( navigablePath, joinType, tableGroup ); + } + + @Override + public TableGroup createRootTableGroupJoin( + NavigablePath navigablePath, + TableGroup lhs, + String explicitSourceAlias, + SqlAstJoinType sqlAstJoinType, + boolean fetched, + Consumer predicateConsumer, + SqlAliasBaseGenerator aliasBaseGenerator, + SqlExpressionResolver sqlExpressionResolver, + FromClauseAccess fromClauseAccess, + SqlAstCreationContext creationContext) { + return new StandardVirtualTableGroup( + navigablePath, + this, + lhs, + fetched + ); + } + + @Override + public String getSqlAliasStem() { + return getPartName(); + } + + @Override + public String getFetchableName() { + return getPartName(); + } + + @Override + public FetchOptions getMappedFetchOptions() { + return FETCH_OPTIONS; + } + + @Override + public Fetch generateFetch( + FetchParent fetchParent, + NavigablePath fetchablePath, + FetchTiming fetchTiming, + boolean selected, + String resultVariable, + DomainResultCreationState creationState) { + throw new UnsupportedOperationException( "AnonymousTupleEmbeddableValuedModelPart is not fetchable!" ); + } + + @Override + public int getNumberOfFetchables() { + return modelParts.size(); + } + + @Override + public NavigableRole getNavigableRole() { + return null; + } + + @Override + public EntityMappingType findContainingEntityMapping() { + return null; + } + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + return new EmbeddableResultImpl<>( + navigablePath, + this, + resultVariable, + creationState + ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + for ( ModelPart mapping : modelParts.values() ) { + mapping.applySqlSelections( navigablePath, tableGroup, creationState ); + } + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + for ( ModelPart mapping : modelParts.values() ) { + mapping.applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); + } + } + + @Override + public void breakDownJdbcValues( + Object domainValue, + JdbcValueConsumer valueConsumer, + SharedSessionContractImplementor session) { + final Object[] values = (Object[]) domainValue; + assert values.length == modelParts.size(); + + int i = 0; + for ( ModelPart mapping : modelParts.values() ) { + final Object attributeValue = values[ i ]; + mapping.breakDownJdbcValues( attributeValue, valueConsumer, session ); + i++; + } + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + final Object[] values = (Object[]) value; + final Object[] result = new Object[ modelParts.size() ]; + int i = 0; + for ( ModelPart mapping : modelParts.values() ) { + Object o = values[i]; + result[i] = mapping.disassemble( o, session ); + i++; + } + + return result; + } + + @Override + public int forEachDisassembledJdbcValue( + Object value, + Clause clause, + int offset, + JdbcValuesConsumer valuesConsumer, + SharedSessionContractImplementor session) { + final Object[] values = (Object[]) value; + int span = 0; + int i = 0; + for ( ModelPart mapping : modelParts.values() ) { + span += mapping.forEachDisassembledJdbcValue( values[i], clause, span + offset, valuesConsumer, session ); + i++; + } + return span; + } + + @Override + public int forEachJdbcValue( + Object value, + Clause clause, + int offset, + JdbcValuesConsumer consumer, + SharedSessionContractImplementor session) { + final Object[] values = (Object[]) value; + int span = 0; + int i = 0; + for ( ModelPart attributeMapping : modelParts.values() ) { + final Object o = values[i]; + span += attributeMapping.forEachJdbcValue( o, clause, span + offset, consumer, session ); + i++; + } + return span; + } + + @Override + public int forEachJdbcType(int offset, IndexedConsumer action) { + int span = 0; + for ( ModelPart attributeMapping : modelParts.values() ) { + span += attributeMapping.forEachJdbcType( span + offset, action ); + } + return span; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEntityValuedModelPart.java new file mode 100644 index 000000000000..c6e50d7c0ff4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleEntityValuedModelPart.java @@ -0,0 +1,536 @@ +/* + * 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.metamodel.model.domain.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.hibernate.Incubating; +import org.hibernate.engine.FetchStyle; +import org.hibernate.engine.FetchTiming; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.loader.ast.spi.MultiNaturalIdLoader; +import org.hibernate.loader.ast.spi.NaturalIdLoader; +import org.hibernate.mapping.IndexedConsumer; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.EntityRowIdMapping; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.metamodel.mapping.EntityVersionMapping; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.NaturalIdMapping; +import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; +import org.hibernate.sql.ast.spi.SqlAstCreationContext; +import org.hibernate.sql.ast.spi.SqlExpressionResolver; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.DelegatingTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.FetchOptions; +import org.hibernate.type.descriptor.java.JavaType; + +import static java.util.Objects.requireNonNullElse; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleEntityValuedModelPart implements EntityValuedModelPart, EntityMappingType, + TableGroupJoinProducer { + + private static final FetchOptions FETCH_OPTIONS = FetchOptions.valueOf( FetchTiming.IMMEDIATE, FetchStyle.JOIN ); + + private final EntityIdentifierMapping identifierMapping; + private final Map modelParts; + private final DomainType domainType; + private final String componentName; + private final EntityValuedModelPart delegate; + + public AnonymousTupleEntityValuedModelPart( + EntityIdentifierMapping identifierMapping, + Map modelParts, + DomainType domainType, + String componentName, + EntityValuedModelPart delegate) { + this.identifierMapping = identifierMapping; + this.modelParts = modelParts; + this.domainType = domainType; + this.componentName = componentName; + this.delegate = delegate; + } + + @Override + public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { + if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { + if ( ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName().equals( name ) ) { + return identifierMapping; + } + } + else { + final ModelPart subPart = ( (CompositeIdentifierMapping) identifierMapping ).getPartMappingType().findSubPart( + name, + treatTargetType + ); + if ( subPart != null ) { + return subPart; + } + } + return delegate.findSubPart( name, treatTargetType ); + } + + @Override + public void visitSubParts(Consumer consumer, EntityMappingType treatTargetType) { + delegate.visitSubParts( consumer, treatTargetType ); + } + + @Override + public MappingType getPartMappingType() { + return this; + } + + @Override + public JavaType getJavaType() { + return domainType.getExpressibleJavaType(); + } + + @Override + public String getPartName() { + return componentName; + } + + @Override + public int getJdbcTypeCount() { + return delegate.getJdbcTypeCount(); + } + + @Override + public int getNumberOfAttributeMappings() { + return delegate.getEntityMappingType().getNumberOfAttributeMappings(); + } + + @Override + public AttributeMapping getAttributeMapping(int position) { + return delegate.getEntityMappingType().getAttributeMapping( position ); + } + + @Override + public List getAttributeMappings() { + return delegate.getEntityMappingType().getAttributeMappings(); + } + + @Override + public void visitAttributeMappings(Consumer action) { + delegate.getEntityMappingType().visitAttributeMappings( action ); + } + + @Override + public Object[] getValues(Object instance) { + return delegate.getEntityMappingType().getValues( instance ); + } + + @Override + public Object getValue(Object instance, int position) { + return delegate.getEntityMappingType() + .getAttributeMapping( position ) + .getValue( instance ); + } + + @Override + public void setValues(Object instance, Object[] resolvedValues) { + delegate.getEntityMappingType().setValues( instance, resolvedValues ); + } + + @Override + public void setValue(Object instance, int position, Object value) { + delegate.getEntityMappingType() + .getAttributeMapping( position ) + .setValue( instance, value ); + } + + @Override + public List getJdbcMappings() { + final List results = new ArrayList<>(); + forEachSelectable( (index, selection) -> results.add( selection.getJdbcMapping() ) ); + return results; + } + + @Override + public int forEachSelectable(SelectableConsumer consumer) { + return forEachSelectable( 0, consumer ); + } + + @Override + public int forEachSelectable(int offset, SelectableConsumer consumer) { + return identifierMapping.forEachSelectable( offset, consumer ); + } + + @Override + public JavaType getMappedJavaType() { + return delegate.getJavaType(); + } + + @Override + public TableGroupJoin createTableGroupJoin( + NavigablePath navigablePath, + TableGroup lhs, + String explicitSourceAlias, + SqlAstJoinType requestedJoinType, + boolean fetched, + boolean addsPredicate, + SqlAliasBaseGenerator aliasBaseGenerator, + SqlExpressionResolver sqlExpressionResolver, + FromClauseAccess fromClauseAccess, + SqlAstCreationContext creationContext) { + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); + final List predicateContainer = Arrays.asList(new Predicate[1]); + final TableGroup intermediateTableGroup = delegate.getEntityMappingType().createRootTableGroup( + lhs.canUseInnerJoins(), + navigablePath.append( "{intermediate}" ), + explicitSourceAlias, + () -> p -> predicateContainer.set( 0, Predicate.combinePredicates( predicateContainer.get( 0 ), p ) ), + aliasBaseGenerator.createSqlAliasBase( getSqlAliasStem() ), + sqlExpressionResolver, + fromClauseAccess, + creationContext + ); + + final TableGroupJoin tableGroupJoin = ( (TableGroupJoinProducer) delegate ).createTableGroupJoin( + navigablePath, + intermediateTableGroup, + explicitSourceAlias, + joinType, + fetched, + addsPredicate, + aliasBaseGenerator, + sqlExpressionResolver, + fromClauseAccess, + creationContext + ); + intermediateTableGroup.addTableGroupJoin( tableGroupJoin ); + final Expression lhsExpr, rhsExpr; + final TableReference lhsTableReference = lhs.resolveTableReference( navigablePath, "" ); + if ( identifierMapping instanceof CompositeIdentifierMapping ) { + final List lhsTuples = new ArrayList<>(); + final List rhsTuples = new ArrayList<>(); + identifierMapping.forEachSelectable( + (i, selectableMapping) -> lhsTuples.add( + new ColumnReference( + lhsTableReference, + selectableMapping, + null + ) + ) + ); + delegate.getEntityMappingType().getIdentifierMapping().forEachSelectable( + (i, selectableMapping) -> rhsTuples.add( + new ColumnReference( + intermediateTableGroup.resolveTableReference( selectableMapping.getContainingTableExpression() ), + selectableMapping, + null + ) + ) + ); + lhsExpr = new SqlTuple( lhsTuples, identifierMapping ); + rhsExpr = new SqlTuple( rhsTuples, delegate.getEntityMappingType().getIdentifierMapping() ); + } + else { + lhsExpr = new ColumnReference( + lhsTableReference, + (SelectableMapping) identifierMapping, + null + ); + final SelectableMapping delegateIdentifierMapping = (SelectableMapping) delegate.getEntityMappingType() + .getIdentifierMapping(); + rhsExpr = new ColumnReference( + intermediateTableGroup.resolveTableReference( intermediateTableGroup.getNavigablePath(), delegateIdentifierMapping.getContainingTableExpression() ), + delegateIdentifierMapping, + null + ); + } + final Predicate idEqualityPredicate = new ComparisonPredicate( lhsExpr, ComparisonOperator.EQUAL, rhsExpr ); + final Predicate predicate = Predicate.combinePredicates( predicateContainer.get( 0 ), idEqualityPredicate ); + return new TableGroupJoin( + navigablePath, + joinType, + // We need a special table group that delegates table reference resolving to the actual table group + new DelegatingTableGroup() { + @Override + protected TableGroup getTableGroup() { + return intermediateTableGroup; + } + + @Override + public TableReference resolveTableReference( + NavigablePath navigablePath, + String tableExpression, + boolean allowFkOptimization) { + return tableGroupJoin.getJoinedGroup().resolveTableReference( navigablePath, tableExpression, allowFkOptimization ); + } + + @Override + public TableReference getTableReference( + NavigablePath navigablePath, + String tableExpression, + boolean allowFkOptimization, + boolean resolve) { + return tableGroupJoin.getJoinedGroup().getTableReference( + navigablePath, + tableExpression, + allowFkOptimization, + resolve + ); + } + }, + predicate + ); + } + + @Override + public TableGroup createRootTableGroupJoin( + NavigablePath navigablePath, + TableGroup lhs, + String explicitSourceAlias, + SqlAstJoinType sqlAstJoinType, + boolean fetched, + Consumer predicateConsumer, + SqlAliasBaseGenerator aliasBaseGenerator, + SqlExpressionResolver sqlExpressionResolver, + FromClauseAccess fromClauseAccess, + SqlAstCreationContext creationContext) { + return ( (TableGroupJoinProducer) delegate ).createRootTableGroupJoin( + navigablePath, + lhs, + explicitSourceAlias, + sqlAstJoinType, + fetched, + predicateConsumer, + aliasBaseGenerator, + sqlExpressionResolver, + fromClauseAccess, + creationContext + ); + } + + @Override + public String getSqlAliasStem() { + return getPartName(); + } + + @Override + public int getNumberOfFetchables() { + return delegate.getNumberOfFetchables(); + } + + @Override + public NavigableRole getNavigableRole() { + return delegate.getNavigableRole(); + } + + @Override + public EntityMappingType findContainingEntityMapping() { + return this; + } + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + return null; +// return new EmbeddableResultImpl<>( +// navigablePath, +// this, +// resultVariable, +// creationState +// ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + identifierMapping.applySqlSelections( navigablePath, tableGroup, creationState ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + identifierMapping.applySqlSelections( navigablePath, tableGroup, creationState, selectionConsumer ); + } + + @Override + public void breakDownJdbcValues( + Object domainValue, + JdbcValueConsumer valueConsumer, + SharedSessionContractImplementor session) { + delegate.breakDownJdbcValues( domainValue, valueConsumer, session ); + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + return delegate.disassemble( value, session ); + } + + @Override + public int forEachDisassembledJdbcValue( + Object value, + Clause clause, + int offset, + JdbcValuesConsumer valuesConsumer, + SharedSessionContractImplementor session) { + return delegate.forEachDisassembledJdbcValue( value, clause, offset, valuesConsumer, session ); + } + + @Override + public int forEachJdbcValue( + Object value, + Clause clause, + int offset, + JdbcValuesConsumer consumer, + SharedSessionContractImplementor session) { + return delegate.forEachJdbcValue( value, clause, offset, consumer, session ); + } + + @Override + public int forEachJdbcType(int offset, IndexedConsumer action) { + return delegate.forEachJdbcType( offset, action ); + } + + @Override + public EntityPersister getEntityPersister() { + return delegate.getEntityMappingType().getEntityPersister(); + } + + @Override + public String getEntityName() { + return delegate.getEntityMappingType().getEntityName(); + } + + @Override + public void visitQuerySpaces(Consumer querySpaceConsumer) { + delegate.getEntityMappingType().visitQuerySpaces( querySpaceConsumer ); + } + + @Override + public AttributeMapping findDeclaredAttributeMapping(String name) { + return delegate.getEntityMappingType().findDeclaredAttributeMapping( name ); + } + + @Override + public Collection getDeclaredAttributeMappings() { + return delegate.getEntityMappingType().getDeclaredAttributeMappings(); + } + + @Override + public void visitDeclaredAttributeMappings(Consumer action) { + delegate.getEntityMappingType().visitDeclaredAttributeMappings( action ); + } + + @Override + public EntityIdentifierMapping getIdentifierMapping() { + return identifierMapping; + } + + @Override + public EntityDiscriminatorMapping getDiscriminatorMapping() { + return delegate.getEntityMappingType().getDiscriminatorMapping(); + } + + @Override + public Object getDiscriminatorValue() { + return delegate.getEntityMappingType().getDiscriminatorValue(); + } + + @Override + public String getSubclassForDiscriminatorValue(Object value) { + return delegate.getEntityMappingType().getSubclassForDiscriminatorValue( value ); + } + + @Override + public EntityVersionMapping getVersionMapping() { + return delegate.getEntityMappingType().getVersionMapping(); + } + + @Override + public NaturalIdMapping getNaturalIdMapping() { + return delegate.getEntityMappingType().getNaturalIdMapping(); + } + + @Override + public EntityRowIdMapping getRowIdMapping() { + return delegate.getEntityMappingType().getRowIdMapping(); + } + + @Override + public void visitConstraintOrderedTables(ConstraintOrderedTableConsumer consumer) { + delegate.getEntityMappingType().visitConstraintOrderedTables( consumer ); + } + + @Override + public NaturalIdLoader getNaturalIdLoader() { + return delegate.getEntityMappingType().getNaturalIdLoader(); + } + + @Override + public MultiNaturalIdLoader getMultiNaturalIdLoader() { + return delegate.getEntityMappingType().getMultiNaturalIdLoader(); + } + + @Override + public EntityMappingType getEntityMappingType() { + return this; + } + + @Override + public SqlAstJoinType getDefaultSqlAstJoinType(TableGroup parentTableGroup) { + return delegate instanceof TableGroupJoinProducer + ? ( (TableGroupJoinProducer) delegate ).getDefaultSqlAstJoinType( parentTableGroup ) + : null; + } + + @Override + public boolean isSimpleJoinPredicate(Predicate predicate) { + return delegate instanceof TableGroupJoinProducer + ? ( (TableGroupJoinProducer) delegate ).isSimpleJoinPredicate( predicate ) + : false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTuplePersistentSingularAttribute.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTuplePersistentSingularAttribute.java new file mode 100644 index 000000000000..c4ce384329cc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTuplePersistentSingularAttribute.java @@ -0,0 +1,122 @@ +/* + * 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.metamodel.model.domain.internal; + +import java.lang.reflect.Member; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.AttributeClassification; +import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.query.hql.spi.SqmCreationState; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; +import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTuplePersistentSingularAttribute extends AnonymousTupleSqmPathSource implements + SingularPersistentAttribute { + + private final SingularPersistentAttribute delegate; + + public AnonymousTuplePersistentSingularAttribute( + String localPathName, + SqmPath path, + SingularPersistentAttribute delegate) { + super( localPathName, path ); + this.delegate = delegate; + } + + @Override + public SqmJoin createSqmJoin( + SqmFrom lhs, + SqmJoinType joinType, + String alias, + boolean fetched, + SqmCreationState creationState) { + return new SqmSingularJoin<>( + lhs, + this, + alias, + joinType, + fetched, + creationState.getCreationContext().getNodeBuilder() + ); + } + + @Override + public SimpleDomainType getType() { + return delegate.getType(); + } + + @Override + public ManagedDomainType getDeclaringType() { + return delegate.getDeclaringType(); + } + + @Override + public boolean isId() { + return delegate.isId(); + } + + @Override + public boolean isVersion() { + return delegate.isVersion(); + } + + @Override + public boolean isOptional() { + return delegate.isOptional(); + } + + @Override + public JavaType getAttributeJavaType() { + return delegate.getAttributeJavaType(); + } + + @Override + public AttributeClassification getAttributeClassification() { + return delegate.getAttributeClassification(); + } + + @Override + public SimpleDomainType getKeyGraphType() { + return delegate.getKeyGraphType(); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public PersistentAttributeType getPersistentAttributeType() { + return delegate.getPersistentAttributeType(); + } + + @Override + public Member getJavaMember() { + return delegate.getJavaMember(); + } + + @Override + public boolean isAssociation() { + return delegate.isAssociation(); + } + + @Override + public boolean isCollection() { + return delegate.isCollection(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSimpleSqmPathSource.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSimpleSqmPathSource.java new file mode 100644 index 000000000000..48370fefcef0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSimpleSqmPathSource.java @@ -0,0 +1,81 @@ +/* + * 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.metamodel.model.domain.internal; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.spi.NavigablePath; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleSimpleSqmPathSource implements SqmPathSource { + private final String localPathName; + private final DomainType domainType; + private final BindableType jpaBindableType; + + public AnonymousTupleSimpleSqmPathSource( + String localPathName, + DomainType domainType, + BindableType jpaBindableType) { + this.localPathName = localPathName; + this.domainType = domainType; + this.jpaBindableType = jpaBindableType; + } + + @Override + public Class getBindableJavaType() { + return domainType.getBindableJavaType(); + } + + @Override + public String getPathName() { + return localPathName; + } + + @Override + public DomainType getSqmPathType() { + return domainType; + } + + @Override + public BindableType getBindableType() { + return jpaBindableType; + } + + @Override + public JavaType getExpressibleJavaType() { + return domainType.getExpressibleJavaType(); + } + + @Override + public SqmPathSource findSubPathSource(String name) { + throw new IllegalStateException( "Basic paths cannot be dereferenced" ); + } + + @Override + public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { + final NavigablePath navigablePath; + if ( intermediatePathSource == null ) { + navigablePath = lhs.getNavigablePath().append( getPathName() ); + } + else { + navigablePath = lhs.getNavigablePath().append( intermediatePathSource.getPathName() ).append( getPathName() ); + } + return new SqmBasicValuedSimplePath<>( + navigablePath, + this, + lhs, + lhs.nodeBuilder() + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSqmPathSource.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSqmPathSource.java new file mode 100644 index 000000000000..5aea35954d93 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleSqmPathSource.java @@ -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.metamodel.model.domain.internal; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.spi.NavigablePath; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleSqmPathSource implements SqmPathSource { + private final String localPathName; + private final SqmPath path; + + public AnonymousTupleSqmPathSource( + String localPathName, + SqmPath path) { + this.localPathName = localPathName; + this.path = path; + } + + @Override + public Class getBindableJavaType() { + return path.getNodeJavaType().getJavaTypeClass(); + } + + @Override + public String getPathName() { + return localPathName; + } + + @Override + public DomainType getSqmPathType() { + //noinspection unchecked + return (DomainType) path.getNodeType().getSqmPathType(); + } + + @Override + public BindableType getBindableType() { + return path.getNodeType().getBindableType(); + } + + @Override + public JavaType getExpressibleJavaType() { + return path.getNodeJavaType(); + } + + @Override + public SqmPathSource findSubPathSource(String name) { + return path.getNodeType().findSubPathSource( name ); + } + + @Override + public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { + final NavigablePath navigablePath; + if ( intermediatePathSource == null ) { + navigablePath = lhs.getNavigablePath().append( getPathName() ); + } + else { + navigablePath = lhs.getNavigablePath().append( intermediatePathSource.getPathName() ).append( getPathName() ); + } + final SqmPathSource nodeType = path.getNodeType(); + if ( nodeType instanceof BasicSqmPathSource ) { + return new SqmBasicValuedSimplePath<>( + navigablePath, + this, + lhs, + lhs.nodeBuilder() + ); + } + else if ( nodeType instanceof EmbeddedSqmPathSource ) { + return new SqmEmbeddedValuedSimplePath<>( + navigablePath, + this, + lhs, + lhs.nodeBuilder() + ); + } + else if ( nodeType instanceof EntitySqmPathSource || nodeType instanceof EntityDomainType + || nodeType instanceof PersistentAttribute && nodeType.getSqmPathType() instanceof EntityDomainType ) { + return new SqmEntityValuedSimplePath<>( + navigablePath, + this, + lhs, + lhs.nodeBuilder() + ); + } + + throw new UnsupportedOperationException( "Unsupported path source: " + nodeType ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleTableGroupProducer.java new file mode 100644 index 000000000000..39f3c1a31432 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleTableGroupProducer.java @@ -0,0 +1,298 @@ +/* + * 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.metamodel.model.domain.internal; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.hibernate.Incubating; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.mapping.IndexedConsumer; +import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.LazyTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupProducer; +import org.hibernate.sql.results.graph.DomainResult; +import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.descriptor.java.JavaType; + +import jakarta.persistence.metamodel.Attribute; + +/** + * The table group producer for an anonymous tuple type. + * + * Model part names are determined based on the tuple type component names. + * The kind and type of the model parts is based on the type of the underlying selection. + * + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleTableGroupProducer implements TableGroupProducer, MappingType { + + private final String aliasStem; + private final JavaType javaTypeDescriptor; + private final Map modelParts; + + public AnonymousTupleTableGroupProducer( + AnonymousTupleType tupleType, + String aliasStem, + List sqlSelections, + FromClauseAccess fromClauseAccess) { + this.aliasStem = aliasStem; + this.javaTypeDescriptor = tupleType.getExpressibleJavaType(); + final int componentCount = tupleType.componentCount(); + final Map modelParts = CollectionHelper.linkedMapOfSize( componentCount ); + int selectionIndex = 0; + for ( int i = 0; i < componentCount; i++ ) { + final SqmExpressible sqmExpressible = tupleType.get( i ); + final String partName = tupleType.getComponentName( i ); + final SqlSelection sqlSelection = sqlSelections.get( i ); + final ModelPart modelPart; + if ( sqmExpressible instanceof SqmPath ) { + final SqmPath sqmPath = (SqmPath) sqmExpressible; + final TableGroup tableGroup = fromClauseAccess.findTableGroup( sqmPath.getNavigablePath() ); + modelPart = createModelPart( + sqmExpressible, + sqmPath.getNodeType().getSqmPathType(), + sqlSelections, + selectionIndex, + partName, + partName, + tableGroup == null ? null : getModelPart( tableGroup ) + ); + } + else { + modelPart = new AnonymousTupleBasicValuedModelPart( + partName, + partName, + sqmExpressible, + sqlSelection.getExpressionType() + .getJdbcMappings() + .get( 0 ) + ); + } + modelParts.put( partName, modelPart ); + selectionIndex += modelPart.getJdbcTypeCount(); + } + this.modelParts = modelParts; + } + + private ModelPart getModelPart(TableGroup tableGroup) { + if ( tableGroup instanceof LazyTableGroup && ( (LazyTableGroup) tableGroup ).getUnderlyingTableGroup() != null ) { + return ( (LazyTableGroup) tableGroup ).getUnderlyingTableGroup().getModelPart(); + } + return tableGroup.getModelPart(); + } + + private ModelPart createModelPart( + SqmExpressible sqmExpressible, + DomainType domainType, + List sqlSelections, + int selectionIndex, + String selectionExpression, + String partName, + ModelPart existingModelPart) { + if ( domainType instanceof EntityDomainType ) { + final EntityValuedModelPart existingModelPartContainer = (EntityValuedModelPart) existingModelPart; + final EntityIdentifierMapping identifierMapping = existingModelPartContainer.getEntityMappingType() + .getIdentifierMapping(); + final EntityIdentifierMapping newIdentifierMapping; + if ( identifierMapping instanceof SingleAttributeIdentifierMapping ) { + if ( identifierMapping.getPartMappingType() instanceof ManagedMappingType ) { + // todo: implement + throw new UnsupportedOperationException("Support for embedded id in anonymous tuples is not yet implemented"); + } + else { + newIdentifierMapping = new AnonymousTupleBasicEntityIdentifierMapping( + selectionExpression + "_" + ( (SingleAttributeIdentifierMapping) identifierMapping ).getAttributeName(), + sqmExpressible, + sqlSelections.get( selectionIndex ) + .getExpressionType() + .getJdbcMappings() + .get( 0 ), + (BasicEntityIdentifierMapping) identifierMapping + ); + } + } + else { + // todo: implement + throw new UnsupportedOperationException("Support for id-class in anonymous tuples is not yet implemented"); + } + return new AnonymousTupleEntityValuedModelPart( + newIdentifierMapping, + null, + domainType, + selectionExpression, + existingModelPartContainer + ); + } + else if ( domainType instanceof ManagedDomainType ) { + //noinspection unchecked + final Set> attributes = (Set>) ( (ManagedDomainType) domainType ).getAttributes(); + final Map modelParts = CollectionHelper.linkedMapOfSize( attributes.size() ); + final EmbeddableValuedModelPart modelPartContainer = (EmbeddableValuedModelPart) existingModelPart; + for ( Attribute attribute : attributes ) { + if ( !( attribute instanceof SingularPersistentAttribute ) ) { + throw new IllegalArgumentException( "Only embeddables without collections are supported!" ); + } + final DomainType attributeType = ( (SingularPersistentAttribute) attribute ).getType(); + final ModelPart modelPart = createModelPart( + sqmExpressible, + attributeType, + sqlSelections, + selectionIndex, + selectionExpression + "_" + attribute.getName(), + attribute.getName(), + modelPartContainer.findSubPart( attribute.getName(), null ) + ); + modelParts.put( modelPart.getPartName(), modelPart ); + } + return new AnonymousTupleEmbeddableValuedModelPart( modelParts, domainType, selectionExpression, modelPartContainer ); + } + else { + return new AnonymousTupleBasicValuedModelPart( + partName, + selectionExpression, + sqmExpressible, + sqlSelections.get( selectionIndex ) + .getExpressionType() + .getJdbcMappings() + .get( 0 ) + ); + } + } + + @Override + public MappingType getPartMappingType() { + return this; + } + + @Override + public JavaType getMappedJavaType() { + return javaTypeDescriptor; + } + + @Override + public String getPartName() { + return null; + } + + @Override + public NavigableRole getNavigableRole() { + return null; + } + + @Override + public EntityMappingType findContainingEntityMapping() { + return null; + } + + @Override + public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { + return modelParts.get( name ); + } + + @Override + public void visitSubParts(Consumer consumer, EntityMappingType treatTargetType) { + for ( ModelPart modelPart : modelParts.values() ) { + consumer.accept( modelPart ); + } + } + + @Override + public String getSqlAliasStem() { + return aliasStem; + } + + @Override + public JavaType getJavaType() { + return javaTypeDescriptor; + } + + //-------------------------------- + // Support for using the anonymous tuple as table reference directly somewhere is not yet implemented + //-------------------------------- + + @Override + public DomainResult createDomainResult( + NavigablePath navigablePath, + TableGroup tableGroup, + String resultVariable, + DomainResultCreationState creationState) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + + @Override + public void applySqlSelections( + NavigablePath navigablePath, + TableGroup tableGroup, + DomainResultCreationState creationState, + BiConsumer selectionConsumer) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + + @Override + public void breakDownJdbcValues( + Object domainValue, + JdbcValueConsumer valueConsumer, + SharedSessionContractImplementor session) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + + @Override + public Object disassemble(Object value, SharedSessionContractImplementor session) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + + @Override + public int forEachDisassembledJdbcValue( + Object value, + Clause clause, + int offset, + JdbcValuesConsumer valuesConsumer, + SharedSessionContractImplementor session) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + + @Override + public int forEachJdbcType(int offset, IndexedConsumer action) { + throw new UnsupportedOperationException( "Not yet implemented" ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleType.java new file mode 100644 index 000000000000..e24b2acf6999 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AnonymousTupleType.java @@ -0,0 +1,198 @@ +/* + * 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.metamodel.model.domain.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.metamodel.UnsupportedMappingException; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.metamodel.model.domain.TupleType; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.select.SqmSelectClause; +import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.from.TableGroupProducer; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.ObjectArrayJavaType; + +import jakarta.persistence.TupleElement; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleType implements TupleType, DomainType, ReturnableType, SqmPathSource { + + private final ObjectArrayJavaType javaTypeDescriptor; + private final SqmExpressible[] components; + private final Map componentIndexMap; + + public AnonymousTupleType(SqmSubQuery subQuery) { + this( extractSqmExpressibles( subQuery ) ); + } + + public AnonymousTupleType(SqmExpressible[] components) { + this.components = components; + this.javaTypeDescriptor = new ObjectArrayJavaType( getTypeDescriptors( components ) ); + final Map map = CollectionHelper.linkedMapOfSize( components.length ); + for ( int i = 0; i < components.length; i++ ) { + final String alias; + final SqmExpressible component = components[i]; + if ( component instanceof TupleElement ) { + alias = ( (TupleElement) component ).getAlias(); + } + else { + alias = null; + } + if ( alias == null ) { + throw new IllegalArgumentException( "Component at index " + i + " has no alias, but alias is required!" ); + } + map.put( alias, i ); + } + this.componentIndexMap = map; + } + + private static SqmExpressible[] extractSqmExpressibles(SqmSubQuery subQuery) { + final SqmSelectClause selectClause = subQuery.getQuerySpec().getSelectClause(); + if ( selectClause == null || selectClause.getSelectionItems().isEmpty() ) { + throw new IllegalArgumentException( "Sub query has no selection items!" ); + } + // todo: right now, we "snapshot" the state of the sub query when creating this type, but maybe we shouldn't? + // i.e. what if the sub query changes later on? Or should we somehow mark the sub query to signal, + // that changes to the select clause are invalid after a certain point? + //noinspection SuspiciousToArrayCall + return selectClause.getSelectionItems().toArray( new SqmExpressible[0] ); + } + + private static JavaType[] getTypeDescriptors(SqmExpressible[] components) { + final JavaType[] typeDescriptors = new JavaType[components.length]; + for ( int i = 0; i < components.length; i++ ) { + typeDescriptors[i] = components[i].getExpressibleJavaType(); + } + return typeDescriptors; + } + + @Override + public TableGroupProducer resolveTableGroupProducer( + String aliasStem, + List sqlSelections, + FromClauseAccess fromClauseAccess) { + return new AnonymousTupleTableGroupProducer( this, aliasStem, sqlSelections, fromClauseAccess ); + } + + @Override + public int componentCount() { + return components.length; + } + + @Override + public String getComponentName(int index) { + return ( (TupleElement) components[index] ).getAlias(); + } + + @Override + public List getComponentNames() { + return new ArrayList<>( componentIndexMap.keySet() ); + } + + @Override + public SqmExpressible get(int index) { + return components[index]; + } + + @Override + public SqmExpressible get(String componentName) { + final Integer index = componentIndexMap.get( componentName ); + return index == null ? null : components[index]; + } + + @Override + public SqmPathSource findSubPathSource(String name) { + final Integer index = componentIndexMap.get( name ); + if ( index == null ) { + return null; + } + final SqmExpressible component = components[index]; + if ( component instanceof SqmPath ) { + final SqmPath sqmPath = (SqmPath) component; + if ( sqmPath.getNodeType() instanceof SingularPersistentAttribute ) { + //noinspection unchecked,rawtypes + return new AnonymousTuplePersistentSingularAttribute( name, sqmPath, (SingularPersistentAttribute) sqmPath.getNodeType() ); + } + else { + return new AnonymousTupleSqmPathSource<>( name, sqmPath ); + } + } + else { + return new AnonymousTupleSimpleSqmPathSource<>( + name, + (DomainType) component, + BindableType.SINGULAR_ATTRIBUTE + ); + } + } + + @Override + public JavaType getExpressibleJavaType() { + //noinspection unchecked + return (JavaType) javaTypeDescriptor; + } + + @Override + public BindableType getBindableType() { + return BindableType.ENTITY_TYPE; + } + + @Override + public PersistenceType getPersistenceType() { + return PersistenceType.ENTITY; + } + + @Override + public String getPathName() { + return "tuple" + System.identityHashCode( this ); + } + + @Override + public DomainType getSqmPathType() { + return this; + } + + @Override + public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { + throw new UnsupportedMappingException( + "AnonymousTupleType cannot be used to create an SqmPath - that would be an SqmFrom which are created directly" + ); + } + + @Override + public Class getBindableJavaType() { + //noinspection unchecked + return (Class) javaTypeDescriptor.getJavaType(); + } + + @Override + public Class getJavaType() { + return getBindableJavaType(); + } + + @Override + public String toString() { + return "AnonymousTupleType" + Arrays.toString( components ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedFrom.java new file mode 100644 index 000000000000..cc746be78671 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedFrom.java @@ -0,0 +1,29 @@ +/* + * 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.criteria; + +import org.hibernate.Incubating; + +/** + * @author Christian Beikov + */ +@Incubating +public interface JpaDerivedFrom extends JpaFrom { + + /** + * The sub query part for this derived from node. + */ + JpaSubQuery getQueryPart(); + + /** + * Specifies whether the sub query part can access previous from node aliases. + * Normally, sub queries in the from clause are unable to access other from nodes, + * but when specifying them as lateral, they are allowed to do so. + * Refer to the SQL standard definition of LATERAL for more details. + */ + boolean isLateral(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java new file mode 100644 index 000000000000..7e9fbf610ac7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedJoin.java @@ -0,0 +1,18 @@ +/* + * 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.criteria; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; + +/** + * @author Christian Beikov + */ +@Incubating +public interface JpaDerivedJoin extends JpaDerivedFrom, SqmQualifiedJoin, JpaJoinedFrom { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedRoot.java new file mode 100644 index 000000000000..5611844050d8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaDerivedRoot.java @@ -0,0 +1,17 @@ +/* + * 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.criteria; + +import org.hibernate.Incubating; + +/** + * @author Christian Beikov + */ +@Incubating +public interface JpaDerivedRoot extends JpaDerivedFrom, JpaRoot { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java index 10945b9cb4ae..0a6fe8d6e13e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java @@ -6,10 +6,12 @@ */ package org.hibernate.query.criteria; +import org.hibernate.Incubating; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.sqm.tree.SqmJoinType; import jakarta.persistence.criteria.From; +import jakarta.persistence.criteria.Subquery; /** * API extension to the JPA {@link From} contract @@ -27,4 +29,20 @@ public interface JpaFrom extends JpaPath, JpaFetchParent, From JpaEntityJoin join(Class entityJavaType, SqmJoinType joinType); JpaEntityJoin join(EntityDomainType entity, SqmJoinType joinType); + + @Incubating + JpaDerivedJoin join(Subquery subquery); + + @Incubating + JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType); + + @Incubating + JpaDerivedJoin joinLateral(Subquery subquery); + + @Incubating + JpaDerivedJoin joinLateral(Subquery subquery, SqmJoinType joinType); + + @Incubating + JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType, boolean lateral); + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaRoot.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaRoot.java index e43cdbca2650..607861f58d4e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaRoot.java @@ -17,5 +17,6 @@ public interface JpaRoot extends JpaFrom, Root { @Override EntityDomainType getModel(); + // todo: deprecate and remove? EntityDomainType getManagedType(); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java index 3bcfacee4024..bcbd770fcf1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java @@ -10,6 +10,7 @@ import jakarta.persistence.criteria.AbstractQuery; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Subquery; import jakarta.persistence.metamodel.EntityType; /** @@ -28,6 +29,35 @@ public interface JpaSelectCriteria extends AbstractQuery, JpaCriteriaBase */ JpaQueryPart getQueryPart(); + /** + * Create and add a query root corresponding to the given sub query, + * forming a cartesian product with any existing roots. + * + * @param subquery the sub query + * @return query root corresponding to the given sub query + */ + JpaDerivedRoot from(Subquery subquery); + + /** + * Create and add a query root corresponding to the given lateral sub query, + * forming a cartesian product with any existing roots. + * + * @param subquery the sub query + * @return query root corresponding to the given sub query + */ + JpaDerivedRoot fromLateral(Subquery subquery); + + /** + * Create and add a query root corresponding to the given sub query, + * forming a cartesian product with any existing roots. + * If the sub query is marked as lateral, it may access previous from elements. + * + * @param subquery the sub query + * @param lateral whether to allow access to previous from elements in the sub query + * @return query root corresponding to the given sub query + */ + JpaDerivedRoot from(Subquery subquery, boolean lateral); + @Override JpaSelectCriteria distinct(boolean distinct); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java index b24475b594c6..f4800fcc70b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSubQuery.java @@ -9,12 +9,12 @@ import java.util.List; import java.util.Set; import jakarta.persistence.criteria.Expression; -import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Order; import jakarta.persistence.criteria.Predicate; -import jakarta.persistence.criteria.SetJoin; +import jakarta.persistence.criteria.Selection; import jakarta.persistence.criteria.Subquery; -import org.hibernate.query.sqm.tree.domain.SqmSetJoin; +import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; @@ -24,12 +24,49 @@ */ public interface JpaSubQuery extends Subquery, JpaSelectCriteria, JpaExpression { + JpaSubQuery multiselect(Selection... selections); + + JpaSubQuery multiselect(List> selectionList); + SqmCrossJoin correlate(SqmCrossJoin parentCrossJoin); SqmEntityJoin correlate(SqmEntityJoin parentEntityJoin); Set> getCorrelatedSqmJoins(); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Limit/Offset/Fetch clause + + JpaExpression getOffset(); + + JpaSubQuery offset(JpaExpression offset); + + JpaSubQuery offset(Number offset); + + JpaExpression getFetch(); + + JpaSubQuery fetch(JpaExpression fetch); + + JpaSubQuery fetch(JpaExpression fetch, FetchClauseType fetchClauseType); + + JpaSubQuery fetch(Number fetch); + + JpaSubQuery fetch(Number fetch, FetchClauseType fetchClauseType); + + FetchClauseType getFetchClauseType(); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Order by clause + + List getOrderList(); + + JpaSubQuery orderBy(Order... o); + + JpaSubQuery orderBy(List o); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Covariant overrides + @Override JpaSubQuery distinct(boolean distinct); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java index cf48cb332b3e..0faf2ef7b13b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QuerySplitter.java @@ -15,6 +15,8 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.spi.NavigablePath; import org.hibernate.query.hql.spi.SqmCreationOptions; import org.hibernate.query.hql.spi.SqmCreationProcessingState; @@ -373,7 +375,7 @@ public SqmRoot visitRootPath(SqmRoot sqmRoot) { pathSource = mappedDescriptor; } else { - pathSource = sqmRoot.getReferencedPathSource(); + pathSource = sqmRoot.getModel(); } final SqmRoot copy = new SqmRoot<>( pathSource, @@ -390,6 +392,26 @@ public SqmRoot visitRootPath(SqmRoot sqmRoot) { return copy; } + @Override + public SqmDerivedRoot visitRootDerived(SqmDerivedRoot sqmRoot) { + SqmFrom sqmFrom = sqmFromCopyMap.get( sqmRoot ); + if ( sqmFrom != null ) { + return (SqmDerivedRoot) sqmFrom; + } + final SqmDerivedRoot copy = new SqmDerivedRoot<>( + (SqmSubQuery) sqmRoot.getQueryPart().accept( this ), + sqmRoot.getExplicitAlias(), + sqmRoot.isLateral() + ); + getProcessingStateStack().getCurrent().getPathRegistry().register( copy ); + sqmFromCopyMap.put( sqmRoot, copy ); + sqmPathCopyMap.put( sqmRoot.getNavigablePath(), copy ); + if ( currentFromClauseCopy != null ) { + currentFromClauseCopy.addRoot( copy ); + } + return copy; + } + @Override public SqmCrossJoin visitCrossJoin(SqmCrossJoin join) { final SqmFrom sqmFrom = sqmFromCopyMap.get( join ); @@ -464,6 +486,27 @@ public SqmEntityJoin visitQualifiedEntityJoin(SqmEntityJoin join) { return copy; } + @Override + public SqmDerivedJoin visitQualifiedDerivedJoin(SqmDerivedJoin join) { + SqmFrom sqmFrom = sqmFromCopyMap.get( join ); + if ( sqmFrom != null ) { + return (SqmDerivedJoin) sqmFrom; + } + final SqmRoot sqmRoot = (SqmRoot) sqmFromCopyMap.get( join.findRoot() ); + final SqmDerivedJoin copy = new SqmDerivedJoin( + (SqmSubQuery) join.getQueryPart().accept( this ), + join.getExplicitAlias(), + join.getSqmJoinType(), + join.isLateral(), + sqmRoot + ); + getProcessingStateStack().getCurrent().getPathRegistry().register( copy ); + sqmFromCopyMap.put( join, copy ); + sqmPathCopyMap.put( join.getNavigablePath(), copy ); + sqmRoot.addSqmJoin( copy ); + return copy; + } + @Override public SqmBasicValuedSimplePath visitBasicValuedPath(SqmBasicValuedSimplePath path) { final SqmPathRegistry pathRegistry = getProcessingStateStack().getCurrent().getPathRegistry(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index bf809ca5c766..bf5d9378ebda 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -105,6 +105,7 @@ 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.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; @@ -147,6 +148,7 @@ import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; @@ -437,11 +439,11 @@ public SqmInsertStatement visitInsertStatement(HqlParser.InsertStatementConte dmlTargetIndex + 1 ); final SqmRoot root = visitTargetEntity( dmlTargetContext ); - if ( root.getReferencedPathSource() instanceof SqmPolymorphicRootDescriptor ) { + if ( root.getModel() instanceof SqmPolymorphicRootDescriptor ) { throw new SemanticException( String.format( "Target type '%s' in insert statement is not an entity", - root.getReferencedPathSource().getHibernateEntityName() + root.getModel().getHibernateEntityName() ) ); } @@ -525,11 +527,11 @@ public SqmUpdateStatement visitUpdateStatement(HqlParser.UpdateStatementConte final int dmlTargetIndex = versioned ? 2 : 1; final HqlParser.TargetEntityContext dmlTargetContext = (HqlParser.TargetEntityContext) ctx.getChild( dmlTargetIndex ); final SqmRoot root = visitTargetEntity( dmlTargetContext ); - if ( root.getReferencedPathSource() instanceof SqmPolymorphicRootDescriptor ) { + if ( root.getModel() instanceof SqmPolymorphicRootDescriptor ) { throw new SemanticException( String.format( "Target type '%s' in update statement is not an entity", - root.getReferencedPathSource().getHibernateEntityName() + root.getModel().getHibernateEntityName() ) ); } @@ -1540,7 +1542,7 @@ public SqmFromClause visitFromClause(HqlParser.FromClauseContext parserFromClaus @Override public SqmRoot visitEntityWithJoins(HqlParser.EntityWithJoinsContext parserSpace) { - final SqmRoot sqmRoot = visitRootEntity( (HqlParser.RootEntityContext) parserSpace.getChild( 0 ) ); + final SqmRoot sqmRoot = (SqmRoot) parserSpace.getChild( 0 ).accept( this ); final SqmFromClause fromClause = currentQuerySpec().getFromClause(); // Correlations are implicitly added to the from clause if ( !( sqmRoot instanceof SqmCorrelation ) ) { @@ -1647,6 +1649,41 @@ public SqmRoot visitRootEntity(HqlParser.RootEntityContext ctx) { return sqmRoot; } + @Override + public SqmRoot visitRootSubquery(HqlParser.RootSubqueryContext ctx) { + if ( getCreationOptions().useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + "The JPA specification does not support subqueries in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature.", + StrictJpaComplianceViolation.Type.FROM_SUBQUERY + ); + } + final ParseTree firstChild = ctx.getChild( 0 ); + final boolean lateral = ( (TerminalNode) firstChild ).getSymbol().getType() == HqlParser.LATERAL; + final int subqueryIndex = lateral ? 2 : 1; + final SqmSubQuery subQuery = (SqmSubQuery) ctx.getChild( subqueryIndex ).accept( this ); + + final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 ); + final HqlParser.VariableContext identificationVariableDefContext; + if ( lastChild instanceof HqlParser.VariableContext ) { + identificationVariableDefContext = (HqlParser.VariableContext) lastChild; + } + else { + identificationVariableDefContext = null; + } + final String alias = applyJpaCompliance( + visitVariable( identificationVariableDefContext ) + ); + + final SqmCreationProcessingState processingState = processingStateStack.getCurrent(); + final SqmPathRegistry pathRegistry = processingState.getPathRegistry(); + final SqmRoot sqmRoot = new SqmDerivedRoot<>( subQuery, alias, true ); + + pathRegistry.register( sqmRoot ); + + return sqmRoot; + } + @Override public String visitVariable(HqlParser.VariableContext ctx) { if ( ctx == null ) { @@ -1775,10 +1812,11 @@ protected void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot sqmR break; } - final HqlParser.JoinPathContext qualifiedJoinPathContext = parserJoin.joinPath(); + final HqlParser.JoinTargetContext qualifiedJoinTargetContext = parserJoin.joinTarget(); + final ParseTree lastChild = qualifiedJoinTargetContext.getChild( qualifiedJoinTargetContext.getChildCount() - 1 ); final HqlParser.VariableContext identificationVariableDefContext; - if ( qualifiedJoinPathContext.getChildCount() > 1 ) { - identificationVariableDefContext = (HqlParser.VariableContext) qualifiedJoinPathContext.getChild( 1 ); + if ( lastChild instanceof HqlParser.VariableContext ) { + identificationVariableDefContext = (HqlParser.VariableContext) lastChild; } else { identificationVariableDefContext = null; @@ -1799,13 +1837,36 @@ protected void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot sqmR this ) ); - try { - //noinspection unchecked - final SqmQualifiedJoin join = (SqmQualifiedJoin) qualifiedJoinPathContext.getChild( 0 ).accept( this ); + final SqmQualifiedJoin join; + if ( qualifiedJoinTargetContext instanceof HqlParser.JoinPathContext ) { + //noinspection unchecked + join = (SqmQualifiedJoin) qualifiedJoinTargetContext.getChild( 0 ).accept( this ); + } + else { + if ( fetch ) { + throw new SemanticException( "fetch not allowed for subquery join" ); + } + if ( getCreationOptions().useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + "The JPA specification does not support subqueries in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature.", + StrictJpaComplianceViolation.Type.FROM_SUBQUERY + ); + } + final TerminalNode terminalNode = (TerminalNode) qualifiedJoinTargetContext.getChild( 0 ); + final boolean lateral = terminalNode.getSymbol().getType() == HqlParser.LATERAL; + final int subqueryIndex = lateral ? 2 : 1; + final DotIdentifierConsumer identifierConsumer = dotIdentifierConsumerStack.pop(); + final SqmSubQuery subQuery = (SqmSubQuery) qualifiedJoinTargetContext.getChild( subqueryIndex ).accept( this ); + dotIdentifierConsumerStack.push( identifierConsumer ); + //noinspection unchecked,rawtypes + join = new SqmDerivedJoin( subQuery, alias, joinType, lateral, sqmRoot ); + processingStateStack.getCurrent().getPathRegistry().register( join ); + } final HqlParser.JoinRestrictionContext qualifiedJoinRestrictionContext = parserJoin.joinRestriction(); - if ( join instanceof SqmEntityJoin ) { + if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin ) { sqmRoot.addSqmJoin( join ); } else if ( join instanceof SqmAttributeJoin ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java index 2457e9ea3cc8..3492ea370f5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java @@ -320,20 +320,5 @@ private void checkResultVariable(SqmAliasedNode selection) { ) ); } - - final SqmFrom registeredFromElement = sqmFromByAlias.get( alias ); - if ( registeredFromElement != null ) { - if ( !registeredFromElement.equals( selection.getSelectableNode() ) ) { - throw new AliasCollisionException( - String.format( - Locale.ENGLISH, - "Alias [%s] used in select-clause [%s] also used in from-clause [%s]", - alias, - selection.getSelectableNode(), - registeredFromElement - ) - ); - } - } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index 81e5e97240db..27c0ee9b7ec3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -16,6 +16,7 @@ import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; @@ -61,6 +62,7 @@ import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -128,6 +130,8 @@ public interface SemanticQueryWalker { T visitRootPath(SqmRoot sqmRoot); + T visitRootDerived(SqmDerivedRoot sqmRoot); + T visitCrossJoin(SqmCrossJoin joinedFromElement); T visitPluralPartJoin(SqmPluralPartJoin joinedFromElement); @@ -136,6 +140,8 @@ public interface SemanticQueryWalker { T visitQualifiedAttributeJoin(SqmAttributeJoin joinedFromElement); + T visitQualifiedDerivedJoin(SqmDerivedJoin joinedFromElement); + T visitBasicValuedPath(SqmBasicValuedSimplePath path); T visitEmbeddableValuedPath(SqmEmbeddedValuedSimplePath path); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java index dc29bac82bf5..49bf6d9c8095 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java @@ -27,6 +27,7 @@ public enum Type { TUPLES( "use of tuples/row value constructors" ), COLLATIONS( "use of collations" ), SUBQUERY_ORDER_BY( "use of ORDER BY clause in subquery" ), + FROM_SUBQUERY( "use of subquery in FROM clause" ), SET_OPERATIONS( "use of set operations" ), LIMIT_OFFSET_CLAUSE( "use of LIMIT/OFFSET clause" ), IDENTIFICATION_VARIABLE_NOT_DECLARED_IN_FROM_CLAUSE( "use of an alias not declared in the FROM clause" ), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index fa201fe50ff7..0acd3d4930b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -731,7 +731,7 @@ private NonSelectQueryPlan buildDeleteQueryPlan() { } private NonSelectQueryPlan buildConcreteDeleteQueryPlan(@SuppressWarnings("rawtypes") SqmDeleteStatement sqmDelete) { - final EntityDomainType entityDomainType = sqmDelete.getTarget().getReferencedPathSource(); + final EntityDomainType entityDomainType = sqmDelete.getTarget().getModel(); final String entityNameToDelete = entityDomainType.getHibernateEntityName(); final EntityPersister entityDescriptor = getSessionFactory().getRuntimeMetamodels() .getMappingMetamodel() @@ -759,7 +759,7 @@ private NonSelectQueryPlan buildUpdateQueryPlan() { //noinspection rawtypes final SqmUpdateStatement sqmUpdate = (SqmUpdateStatement) getSqmStatement(); - final String entityNameToUpdate = sqmUpdate.getTarget().getReferencedPathSource().getHibernateEntityName(); + final String entityNameToUpdate = sqmUpdate.getTarget().getModel().getHibernateEntityName(); final EntityPersister entityDescriptor = getSessionFactory().getRuntimeMetamodels() .getMappingMetamodel() .getEntityDescriptor( entityNameToUpdate ); @@ -777,7 +777,7 @@ private NonSelectQueryPlan buildInsertQueryPlan() { //noinspection rawtypes final SqmInsertStatement sqmInsert = (SqmInsertStatement) getSqmStatement(); - final String entityNameToInsert = sqmInsert.getTarget().getReferencedPathSource().getHibernateEntityName(); + final String entityNameToInsert = sqmInsert.getTarget().getModel().getHibernateEntityName(); final EntityPersister entityDescriptor = getSessionFactory().getRuntimeMetamodels() .getMappingMetamodel() .getEntityDescriptor( entityNameToInsert ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java index 4285f177a14b..503da82338e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java @@ -20,6 +20,7 @@ import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; @@ -65,6 +66,7 @@ import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; @@ -501,6 +503,18 @@ public Object visitRootPath(SqmRoot sqmRoot) { return null; } + @Override + public Object visitRootDerived(SqmDerivedRoot sqmRoot) { + processStanza( + "derived", + "`" + sqmRoot.getNavigablePath() + "`", + () -> { + processJoins( sqmRoot ); + } + ); + return null; + } + private void processJoins(SqmFrom sqmFrom) { if ( !sqmFrom.hasJoins() ) { return; @@ -586,6 +600,24 @@ public Object visitQualifiedAttributeJoin(SqmAttributeJoin joinedFromElement) { return null; } + @Override + public Object visitQualifiedDerivedJoin(SqmDerivedJoin joinedFromElement) { + if ( inJoinPredicate ) { + logWithIndentation( "-> [joined-path] - `%s`", joinedFromElement.getNavigablePath() ); + } + else { + processStanza( + "derived", + "`" + joinedFromElement.getNavigablePath() + "`", + () -> { + processJoinPredicate( joinedFromElement ); + processJoins( joinedFromElement ); + } + ); + } + return null; + } + @Override public Object visitBasicValuedPath(SqmBasicValuedSimplePath path) { logWithIndentation( "-> [basic-path] - `%s`", path.getNavigablePath() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java index 6d22804449eb..853835435612 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java @@ -128,7 +128,7 @@ public CteInsertHandler( this.sessionFactory = sessionFactory; final String entityName = this.sqmStatement.getTarget() - .getReferencedPathSource() + .getModel() .getHibernateEntityName(); this.entityDescriptor = sessionFactory.getRuntimeMetamodels().getEntityMappingType( entityName ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/spi/AbstractMutationHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/spi/AbstractMutationHandler.java index 88ff79d7e4ff..7c6e66800183 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/spi/AbstractMutationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/spi/AbstractMutationHandler.java @@ -27,7 +27,7 @@ public AbstractMutationHandler( this.sessionFactory = sessionFactory; final String entityName = sqmDeleteOrUpdateStatement.getTarget() - .getReferencedPathSource() + .getModel() .getHibernateEntityName(); this.entityDescriptor = sessionFactory.getRuntimeMetamodels().getEntityMappingType( entityName ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java index 02ce83d59276..1f04097d42a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java @@ -19,6 +19,7 @@ import org.hibernate.query.sqm.tree.domain.SqmAnyValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; @@ -67,6 +68,7 @@ import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -235,7 +237,7 @@ public Object visitQuerySpec(SqmQuerySpec querySpec) { @Override public Object visitFromClause(SqmFromClause fromClause) { - fromClause.visitRoots( this::visitRootPath ); + fromClause.visitRoots( root -> root.accept( this ) ); return fromClause; } @@ -246,6 +248,14 @@ public Object visitRootPath(SqmRoot sqmRoot) { return sqmRoot; } + @Override + public Object visitRootDerived(SqmDerivedRoot sqmRoot) { + sqmRoot.getQueryPart().accept( this ); + sqmRoot.visitReusablePaths( path -> path.accept( this ) ); + sqmRoot.visitSqmJoins( sqmJoin -> sqmJoin.accept( this ) ); + return sqmRoot; + } + @Override public Object visitCrossJoin(SqmCrossJoin joinedFromElement) { joinedFromElement.visitReusablePaths( path -> path.accept( this ) ); @@ -280,6 +290,17 @@ public Object visitQualifiedAttributeJoin(SqmAttributeJoin joinedFromElemen return joinedFromElement; } + @Override + public Object visitQualifiedDerivedJoin(SqmDerivedJoin joinedFromElement) { + joinedFromElement.getQueryPart().accept( this ); + joinedFromElement.visitReusablePaths( path -> path.accept( this ) ); + joinedFromElement.visitSqmJoins( sqmJoin -> sqmJoin.accept( this ) ); + if ( joinedFromElement.getJoinPredicate() != null ) { + joinedFromElement.getJoinPredicate().accept( this ); + } + return joinedFromElement; + } + @Override public Object visitBasicValuedPath(SqmBasicValuedSimplePath path) { return path; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 66526d243aba..7d9ec1d9e72f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -91,10 +91,14 @@ import org.hibernate.metamodel.model.convert.internal.OrdinalEnumValueConverter; import org.hibernate.metamodel.model.convert.spi.BasicValueConverter; import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.metamodel.model.domain.TupleType; import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource; import org.hibernate.metamodel.model.domain.internal.CompositeSqmPathSource; import org.hibernate.metamodel.model.domain.internal.DiscriminatorSqmPath; @@ -162,6 +166,7 @@ import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedRootJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; @@ -215,6 +220,7 @@ import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; @@ -325,6 +331,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; +import org.hibernate.sql.ast.tree.from.TableGroupProducer; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.VirtualTableGroup; import org.hibernate.sql.ast.tree.insert.InsertStatement; @@ -371,7 +378,6 @@ import org.hibernate.sql.results.internal.StandardEntityGraphTraversalStateImpl; import org.hibernate.type.BasicType; import org.hibernate.type.JavaObjectType; -import org.hibernate.type.NullType; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.java.BasicJavaType; import org.hibernate.type.descriptor.java.EnumJavaType; @@ -386,6 +392,7 @@ import org.jboss.logging.Logger; import jakarta.persistence.TemporalType; +import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.Type; @@ -2403,7 +2410,7 @@ protected void consumeFromClauseCorrelatedRoot(SqmRoot sqmRoot) { return; } else { - final EntityPersister entityDescriptor = resolveEntityPersister( sqmRoot.getReferencedPathSource() ); + final EntityPersister entityDescriptor = resolveEntityPersister( sqmRoot.getModel() ); final TableGroup parentTableGroup = fromClauseIndex.findTableGroupOnParents( sqmRoot.getCorrelationParent().getNavigablePath() ); @@ -2486,31 +2493,79 @@ protected void consumeFromClauseRoot(SqmRoot sqmRoot) { log.tracef( "Already resolved SqmRoot [%s] to TableGroup", sqmRoot ); } final QuerySpec currentQuerySpec = currentQuerySpec(); - final TableGroup tableGroup; if ( sqmRoot.isCorrelated() ) { return; } - final EntityPersister entityDescriptor = resolveEntityPersister( sqmRoot.getReferencedPathSource() ); - tableGroup = entityDescriptor.createRootTableGroup( - true, - sqmRoot.getNavigablePath(), - sqmRoot.getExplicitAlias(), - () -> predicate -> additionalRestrictions = SqlAstTreeHelper.combinePredicates( - additionalRestrictions, - predicate - ), - this, - creationContext - ); + final TableGroup tableGroup; + if ( sqmRoot instanceof SqmDerivedRoot ) { + final SqmDerivedRoot derivedRoot = (SqmDerivedRoot) sqmRoot; + final QueryPart queryPart = (QueryPart) derivedRoot.getQueryPart().accept( this ); + final TupleType tupleType = (TupleType) sqmRoot.getNodeType(); + final List sqlSelections = queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + final TableGroupProducer tableGroupProducer = tupleType.resolveTableGroupProducer( + derivedRoot.getExplicitAlias(), + sqlSelections, + lastPoppedFromClauseIndex + ); + final int componentCount = tupleType.componentCount(); + final Set compatibleTableExpressions = new HashSet<>(); + // The empty table expression is the default we use for derived model parts + // todo: maybe rethink this as we have to collect table expressions anyway? + compatibleTableExpressions.add( "" ); + final List columnNames = new ArrayList<>( componentCount ); + for ( int i = 0; i < componentCount; i++ ) { + final SqmExpressible sqmExpressible = tupleType.get( i ); + final String componentName = tupleType.getComponentName( i ); + if ( sqmExpressible instanceof SqmPath ) { + addColumnNames( + columnNames, + ( (SqmPath) sqmExpressible ).getNodeType().getSqmPathType(), + componentName + ); + } + else { + columnNames.add( componentName ); + } + } + final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( + derivedRoot.getExplicitAlias() == null ? "derived" : derivedRoot.getExplicitAlias() + ); + final String identifierVariable = sqlAliasBase.generateNewAlias(); + tableGroup = new QueryPartTableGroup( + derivedRoot.getNavigablePath(), + tableGroupProducer, + queryPart, + identifierVariable, + columnNames, + compatibleTableExpressions, + derivedRoot.isLateral(), + true, + creationContext.getSessionFactory() + ); + } + else { + final EntityPersister entityDescriptor = resolveEntityPersister( sqmRoot.getModel() ); + tableGroup = entityDescriptor.createRootTableGroup( + true, + sqmRoot.getNavigablePath(), + sqmRoot.getExplicitAlias(), + () -> predicate -> additionalRestrictions = SqlAstTreeHelper.combinePredicates( + additionalRestrictions, + predicate + ), + this, + creationContext + ); - entityDescriptor.applyBaseRestrictions( - currentQuerySpec::applyPredicate, - tableGroup, - true, - getLoadQueryInfluencers().getEnabledFilters(), - null, - this - ); + entityDescriptor.applyBaseRestrictions( + currentQuerySpec::applyPredicate, + tableGroup, + true, + getLoadQueryInfluencers().getEnabledFilters(), + null, + this + ); + } log.tracef( "Resolved SqmRoot [%s] to new TableGroup [%s]", sqmRoot, tableGroup ); @@ -2656,6 +2711,9 @@ else if ( sqmJoin instanceof SqmCrossJoin ) { else if ( sqmJoin instanceof SqmEntityJoin ) { return consumeEntityJoin( ( (SqmEntityJoin) sqmJoin ), lhsTableGroup, transitive ); } + else if ( sqmJoin instanceof SqmDerivedJoin ) { + return consumeDerivedJoin( ( (SqmDerivedJoin) sqmJoin ), lhsTableGroup, transitive ); + } else if ( sqmJoin instanceof SqmPluralPartJoin ) { return consumePluralPartJoin( ( (SqmPluralPartJoin) sqmJoin ), ownerTableGroup, transitive ); } @@ -2888,6 +2946,112 @@ private TableGroup consumeEntityJoin(SqmEntityJoin sqmJoin, TableGroup lhsTab return tableGroup; } + private TableGroup consumeDerivedJoin(SqmDerivedJoin sqmJoin, TableGroup parentTableGroup, boolean transitive) { + final QueryPart queryPart = (QueryPart) sqmJoin.getQueryPart().accept( this ); + final TupleType tupleType = (TupleType) sqmJoin.getNodeType(); + final List sqlSelections = queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + final TableGroupProducer tableGroupProducer = tupleType.resolveTableGroupProducer( + sqmJoin.getExplicitAlias(), + sqlSelections, + lastPoppedFromClauseIndex + ); + final int componentCount = tupleType.componentCount(); + final Set compatibleTableExpressions = new HashSet<>(); + // instead of doing this, let's add proper joins based on PK +// tableGroupProducer.visitSubParts( +// modelPart -> { +// if ( modelPart.getPartMappingType() instanceof EntityMappingType ) { +// ( (EntityMappingType) modelPart.getPartMappingType() ).visitQuerySpaces( compatibleTableExpressions::add ); +// } +// }, +// null +// ); + // The empty table expression is the default we use for derived model parts + // todo: maybe rethink this as we have to collect table expressions anyway? + compatibleTableExpressions.add( "" ); + final List columnNames = new ArrayList<>( componentCount ); + for ( int i = 0; i < componentCount; i++ ) { + final SqmExpressible sqmExpressible = tupleType.get( i ); + final String componentName = tupleType.getComponentName( i ); + if ( sqmExpressible instanceof SqmPath ) { + addColumnNames( + columnNames, + ( (SqmPath) sqmExpressible ).getNodeType().getSqmPathType(), + componentName + ); + } + else { + columnNames.add( componentName ); + } + } + final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( + sqmJoin.getExplicitAlias() == null ? "derived" : sqmJoin.getExplicitAlias() + ); + final String identifierVariable = sqlAliasBase.generateNewAlias(); + final QueryPartTableGroup queryPartTableGroup = new QueryPartTableGroup( + sqmJoin.getNavigablePath(), + tableGroupProducer, + queryPart, + identifierVariable, + columnNames, + compatibleTableExpressions, + sqmJoin.isLateral(), + false, + creationContext.getSessionFactory() + ); + getFromClauseIndex().register( sqmJoin, queryPartTableGroup ); + + final TableGroupJoin tableGroupJoin = new TableGroupJoin( + queryPartTableGroup.getNavigablePath(), + sqmJoin.getSqmJoinType().getCorrespondingSqlJoinType(), + queryPartTableGroup, + null + ); + + // add any additional join restrictions + if ( sqmJoin.getJoinPredicate() != null ) { + final SqmJoin oldJoin = currentlyProcessingJoin; + currentlyProcessingJoin = sqmJoin; + tableGroupJoin.applyPredicate( visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ) ); + currentlyProcessingJoin = oldJoin; + } + + // Note that we add the entity join after processing the predicate because implicit joins needed in there + // can be just ordered right before the entity join without changing the semantics + parentTableGroup.addTableGroupJoin( tableGroupJoin ); + if ( transitive ) { + consumeExplicitJoins( sqmJoin, queryPartTableGroup ); + } + return queryPartTableGroup; + } + + private void addColumnNames(List columnNames, DomainType domainType, String componentName) { + if ( domainType instanceof EntityDomainType ) { + final EntityDomainType entityDomainType = (EntityDomainType) domainType; + final SingularPersistentAttribute idAttribute = entityDomainType.findIdAttribute(); + final String idPath; + if ( idAttribute == null ) { + idPath = componentName; + } + else { + idPath = componentName + "_" + idAttribute.getName(); + } + addColumnNames( columnNames, entityDomainType.getIdType(), idPath ); + } + else if ( domainType instanceof ManagedDomainType ) { + for ( Attribute attribute : ( (ManagedDomainType) domainType ).getAttributes() ) { + if ( !( attribute instanceof SingularPersistentAttribute ) ) { + throw new IllegalArgumentException( "Only embeddables without collections are supported!" ); + } + final DomainType attributeType = ( (SingularPersistentAttribute) attribute ).getType(); + addColumnNames( columnNames, attributeType, componentName + "_" + attribute.getName() ); + } + } + else { + columnNames.add( componentName ); + } + } + private TableGroup consumePluralPartJoin(SqmPluralPartJoin sqmJoin, TableGroup lhsTableGroup, boolean transitive) { final PluralTableGroup pluralTableGroup = (PluralTableGroup) lhsTableGroup; final TableGroup tableGroup = getPluralPartTableGroup( pluralTableGroup, sqmJoin.getReferencedPathSource() ); @@ -3184,6 +3348,17 @@ public Expression visitRootPath(SqmRoot sqmRoot) { throw new InterpretationException( "SqmRoot not yet resolved to TableGroup" ); } + @Override + public Object visitRootDerived(SqmDerivedRoot sqmRoot) { + final TableGroup resolved = getFromClauseAccess().findTableGroup( sqmRoot.getNavigablePath() ); + if ( resolved != null ) { + log.tracef( "SqmDerivedRoot [%s] resolved to existing TableGroup [%s]", sqmRoot, resolved ); + return visitTableGroup( resolved, sqmRoot ); + } + + throw new InterpretationException( "SqmDerivedRoot not yet resolved to TableGroup" ); + } + @Override public Expression visitQualifiedAttributeJoin(SqmAttributeJoin sqmJoin) { final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); @@ -3195,6 +3370,17 @@ public Expression visitQualifiedAttributeJoin(SqmAttributeJoin sqmJoin) { throw new InterpretationException( "SqmAttributeJoin not yet resolved to TableGroup" ); } + @Override + public Expression visitQualifiedDerivedJoin(SqmDerivedJoin sqmJoin) { + final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); + if ( existing != null ) { + log.tracef( "SqmDerivedJoin [%s] resolved to existing TableGroup [%s]", sqmJoin, existing ); + return visitTableGroup( existing, sqmJoin ); + } + + throw new InterpretationException( "SqmDerivedJoin not yet resolved to TableGroup" ); + } + @Override public Expression visitCrossJoin(SqmCrossJoin sqmJoin) { final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); @@ -3301,9 +3487,7 @@ else if ( modelPart instanceof EntityCollectionPart ) { else if ( modelPart instanceof EntityMappingType ) { resultModelPart = ( (EntityMappingType) modelPart ).getIdentifierMapping(); interpretationModelPart = modelPart; - // todo: I think this will always be null anyways because EntityMappingType will only be the model part - // of a TableGroup if that is a root TableGroup, so check if we can just switch to null - parentGroupToUse = findTableGroup( tableGroup.getNavigablePath().getParent() ); + parentGroupToUse = null; } else { resultModelPart = modelPart; @@ -4112,15 +4296,15 @@ protected Expression createLateralJoinExpression( creationContext ) ); - final String compatibleTableExpression; + final Set compatibleTableExpressions; if ( modelPart instanceof BasicValuedModelPart ) { - compatibleTableExpression = ( (BasicValuedModelPart) modelPart ).getContainingTableExpression(); + compatibleTableExpressions = Collections.singleton( ( (BasicValuedModelPart) modelPart ).getContainingTableExpression() ); } else if ( modelPart instanceof EmbeddableValuedModelPart ) { - compatibleTableExpression = ( (EmbeddableValuedModelPart) modelPart ).getContainingTableExpression(); + compatibleTableExpressions = Collections.singleton( ( (EmbeddableValuedModelPart) modelPart ).getContainingTableExpression() ); } else { - compatibleTableExpression = null; + compatibleTableExpressions = Collections.emptySet(); } lateralTableGroup = new QueryPartTableGroup( queryPath, @@ -4128,7 +4312,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart ) { subQuerySpec, identifierVariable, columnNames, - compatibleTableExpression, + compatibleTableExpressions, true, false, creationContext.getSessionFactory() @@ -4857,15 +5041,21 @@ else if ( paramType instanceof EntityDomainType ) { if ( paramSqmType instanceof SqmPath ) { final SqmPath sqmPath = (SqmPath) paramSqmType; final NavigablePath navigablePath = sqmPath.getNavigablePath(); + final ModelPart modelPart; if ( navigablePath.getParent() != null ) { final TableGroup tableGroup = getFromClauseAccess().getTableGroup( navigablePath.getParent() ); - return tableGroup.getModelPart().findSubPart( + modelPart = tableGroup.getModelPart().findSubPart( navigablePath.getLocalName(), null ); } - - return getFromClauseAccess().getTableGroup( navigablePath ).getModelPart(); + else { + modelPart = getFromClauseAccess().getTableGroup( navigablePath ).getModelPart(); + } + if ( modelPart instanceof PluralAttributeMapping ) { + return ( (PluralAttributeMapping) modelPart ).getElementDescriptor(); + } + return modelPart; } if ( paramSqmType instanceof BasicValuedMapping ) { @@ -7066,7 +7256,7 @@ private static JdbcMappingContainer highestPrecedence(JdbcMappingContainer type1 return type1; } - private class GlobalCteContainer implements CteContainer { + private static class GlobalCteContainer implements CteContainer { private final Map cteStatements; private boolean recursive; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 0b3ef74c9c37..360d1801dbc3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -24,6 +24,9 @@ import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SetPersistentAttribute; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; +import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.spi.NavigablePath; import org.hibernate.query.SemanticException; import org.hibernate.query.criteria.JpaEntityJoin; @@ -45,6 +48,7 @@ import jakarta.persistence.criteria.Fetch; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Subquery; import jakarta.persistence.metamodel.CollectionAttribute; import jakarta.persistence.metamodel.ListAttribute; import jakarta.persistence.metamodel.MapAttribute; @@ -99,7 +103,7 @@ protected AbstractSqmFrom( */ protected AbstractSqmFrom( NavigablePath navigablePath, - EntityDomainType entityType, + SqmPathSource entityType, String alias, NodeBuilder nodeBuilder) { super( navigablePath, entityType, null, nodeBuilder ); @@ -555,6 +559,47 @@ public JpaEntityJoin join(EntityDomainType entity, SqmJoinType joinTyp return join; } + @Override + public JpaDerivedJoin join(Subquery subquery) { + return join( subquery, SqmJoinType.INNER, false, null ); + } + + @Override + public JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType) { + return join( subquery, joinType, false, null ); + } + + @Override + public JpaDerivedJoin joinLateral(Subquery subquery) { + return join( subquery, SqmJoinType.INNER, true, null ); + } + + @Override + public JpaDerivedJoin joinLateral(Subquery subquery, SqmJoinType joinType) { + return join( subquery, joinType, true, null ); + } + + @Override + public JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType, boolean lateral) { + return join( subquery, joinType, lateral, null ); + } + + public JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType, boolean lateral, String alias) { + validateComplianceFromSubQuery(); + final JpaDerivedJoin join = new SqmDerivedJoin<>( (SqmSubQuery) subquery, alias, joinType, lateral, findRoot() ); + //noinspection unchecked + addSqmJoin( (SqmJoin) join ); + return join; + } + + private void validateComplianceFromSubQuery() { + if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support subqueries in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } + @Override public Set> getFetches() { //noinspection unchecked diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java index abc4177ae4d2..0b86c8677805 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java @@ -18,17 +18,19 @@ import org.hibernate.metamodel.mapping.EntityDiscriminatorMapping; import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.spi.NavigablePath; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.AbstractSqmExpression; import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.type.descriptor.java.JavaType; /** * @author Steve Ebersole */ -public abstract class AbstractSqmPath extends AbstractSqmExpression implements SqmPath { +public abstract class AbstractSqmPath extends AbstractSqmExpression implements SqmPath, SqmExpressible { private final NavigablePath navigablePath; private final SqmPath lhs; @@ -69,6 +71,16 @@ public SqmPathSource getReferencedPathSource() { return (SqmPathSource) super.getNodeType(); } + @Override + public Class getBindableJavaType() { + return getJavaTypeDescriptor().getJavaTypeClass(); + } + + @Override + public JavaType getExpressibleJavaType() { + return getJavaTypeDescriptor(); + } + @Override public NavigablePath getNavigablePath() { return navigablePath; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java index f4c754ab3fe9..87f907c86567 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java @@ -21,7 +21,7 @@ public class SqmCorrelatedRoot extends SqmRoot implements SqmPathWrapper correlationParent) { super( correlationParent.getNavigablePath(), - correlationParent.getReferencedPathSource(), + correlationParent.getModel(), correlationParent.getExplicitAlias(), correlationParent.nodeBuilder() ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java new file mode 100644 index 000000000000..150736e2741e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java @@ -0,0 +1,133 @@ +/* + * 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.Incubating; +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.internal.AnonymousTupleType; +import org.hibernate.query.PathException; +import org.hibernate.query.criteria.JpaDerivedRoot; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.spi.NavigablePath; + +/** + * @author Christian Beikov + */ +@Incubating +public class SqmDerivedRoot extends SqmRoot implements JpaDerivedRoot { + + private final SqmSubQuery subQuery; + private final boolean lateral; + + public SqmDerivedRoot( + SqmSubQuery subQuery, + String alias, + boolean lateral) { + this( + SqmCreationHelper.buildRootNavigablePath( "<>", alias ), + subQuery, + lateral, + new AnonymousTupleType<>( subQuery ), + alias + ); + } + + protected SqmDerivedRoot( + NavigablePath navigablePath, + SqmSubQuery subQuery, + boolean lateral, + SqmPathSource pathSource, + String alias) { + super( + navigablePath, + pathSource, + alias, + true, + subQuery.nodeBuilder() + ); + this.subQuery = subQuery; + this.lateral = lateral; + } + + @Override + public SqmDerivedRoot copy(SqmCopyContext context) { + final SqmDerivedRoot existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmDerivedRoot path = context.registerCopy( + this, + new SqmDerivedRoot<>( + getNavigablePath(), + getQueryPart().copy( context ), + isLateral(), + getReferencedPathSource(), + getExplicitAlias() + ) + ); + copyTo( path, context ); + return path; + } + + @Override + public SqmSubQuery getQueryPart() { + return subQuery; + } + + @Override + public boolean isLateral() { + return lateral; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitRootDerived( this ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public EntityDomainType getModel() { + // Or should we throw an exception instead? + return null; + } + + @Override + public SqmCorrelatedRoot createCorrelation() { + // todo: implement + throw new NotYetImplementedFor6Exception( getClass()); +// return new SqmCorrelatedRoot<>( this ); + } + + @Override + public SqmTreatedRoot treatAs(Class treatJavaType) throws PathException { + throw new UnsupportedOperationException( "Derived roots can not be treated!" ); + } + + @Override + public SqmTreatedRoot treatAs(EntityDomainType treatTarget) throws PathException { + throw new UnsupportedOperationException( "Derived roots can not be treated!" ); + } + + @Override + public SqmFrom treatAs(Class treatJavaType, String alias) { + throw new UnsupportedOperationException( "Derived roots can not be treated!" ); + } + + @Override + public SqmFrom treatAs(EntityDomainType treatTarget, String alias) { + throw new UnsupportedOperationException( "Derived roots can not be treated!" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmTreatedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmTreatedRoot.java index f65e884b5b18..318aceae3fa3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmTreatedRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmTreatedRoot.java @@ -76,7 +76,7 @@ public SqmPathSource getNodeType() { @Override public EntityDomainType getReferencedPathSource() { - return getManagedType(); + return getTreatTarget(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java new file mode 100644 index 000000000000..b45813c0305a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java @@ -0,0 +1,183 @@ +/* + * 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.from; + +import org.hibernate.Incubating; +import org.hibernate.NotYetImplementedFor6Exception; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.internal.AnonymousTupleType; +import org.hibernate.query.PathException; +import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.domain.AbstractSqmJoin; +import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmTreatedEntityJoin; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; +import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.spi.NavigablePath; + +/** + * @author Christian Beikov + */ +@Incubating +public class SqmDerivedJoin extends AbstractSqmJoin implements JpaDerivedJoin { + private final SqmSubQuery subQuery; + private final boolean lateral; + private SqmPredicate joinPredicate; + + public SqmDerivedJoin( + SqmSubQuery subQuery, + String alias, + SqmJoinType joinType, + boolean lateral, + SqmRoot sqmRoot) { + this( + SqmCreationHelper.buildRootNavigablePath( "<>", alias ), + subQuery, + lateral, + new AnonymousTupleType<>( subQuery ), + alias, + validateJoinType( joinType, lateral ), + sqmRoot + ); + } + + protected SqmDerivedJoin( + NavigablePath navigablePath, + SqmSubQuery subQuery, + boolean lateral, + SqmPathSource pathSource, + String alias, + SqmJoinType joinType, + SqmRoot sqmRoot) { + super( + navigablePath, + pathSource, + sqmRoot, + alias, + joinType, + sqmRoot.nodeBuilder() + ); + this.subQuery = subQuery; + this.lateral = lateral; + } + + private static SqmJoinType validateJoinType(SqmJoinType joinType, boolean lateral) { + if ( lateral ) { + switch ( joinType ) { + case LEFT: + case INNER: + break; + default: + throw new IllegalArgumentException( "Lateral joins can only be left or inner. Illegal join type: " + joinType ); + } + } + return joinType; + } + + @Override + public SqmDerivedJoin copy(SqmCopyContext context) { + final SqmDerivedJoin existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmDerivedJoin path = context.registerCopy( + this, + new SqmDerivedJoin<>( + getNavigablePath(), + subQuery, + lateral, + getReferencedPathSource(), + getExplicitAlias(), + getSqmJoinType(), + findRoot().copy( context ) + ) + ); + copyTo( path, context ); + return path; + } + + protected void copyTo(SqmDerivedJoin target, SqmCopyContext context) { + super.copyTo( target, context ); + target.joinPredicate = joinPredicate == null ? null : joinPredicate.copy( context ); + } + + public SqmRoot getRoot() { + return (SqmRoot) super.getLhs(); + } + + @Override + public SqmRoot findRoot() { + return getRoot(); + } + + @Override + public SqmSubQuery getQueryPart() { + return subQuery; + } + + @Override + public boolean isLateral() { + return lateral; + } + + @Override + public SqmPath getLhs() { + // A derived-join has no LHS + return null; + } + + @Override + public SqmPredicate getJoinPredicate() { + return joinPredicate; + } + + @Override + public void setJoinPredicate(SqmPredicate predicate) { + this.joinPredicate = predicate; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitQualifiedDerivedJoin( this ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public SqmCorrelatedEntityJoin createCorrelation() { + // todo: implement + throw new NotYetImplementedFor6Exception(getClass()); +// return new SqmCorrelatedEntityJoin<>( this ); + } + + @Override + public SqmTreatedEntityJoin treatAs(Class treatJavaType) throws PathException { + throw new UnsupportedOperationException( "Derived joins can not be treated!" ); + } + @Override + public SqmTreatedEntityJoin treatAs(EntityDomainType treatTarget) throws PathException { + throw new UnsupportedOperationException( "Derived joins can not be treated!" ); + } + + @Override + public SqmFrom treatAs(Class treatJavaType, String alias) { + throw new UnsupportedOperationException( "Derived joins can not be treated!" ); + } + + @Override + public SqmFrom treatAs(EntityDomainType treatTarget, String alias) { + throw new UnsupportedOperationException( "Derived joins can not be treated!" ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmRoot.java index 07032e6798d6..47f9bff11f8a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmRoot.java @@ -59,11 +59,11 @@ public SqmRoot( protected SqmRoot( NavigablePath navigablePath, - EntityDomainType entityType, + SqmPathSource referencedNavigable, String alias, boolean allowJoins, NodeBuilder nodeBuilder) { - super( navigablePath, entityType, alias, nodeBuilder ); + super( navigablePath, referencedNavigable, alias, nodeBuilder ); this.allowJoins = allowJoins; } @@ -144,13 +144,8 @@ public SqmRoot findRoot() { return this; } - @Override - public EntityDomainType getReferencedPathSource() { - return (EntityDomainType) super.getReferencedPathSource(); - } - public String getEntityName() { - return getReferencedPathSource().getHibernateEntityName(); + return getModel().getHibernateEntityName(); } @Override @@ -169,9 +164,14 @@ public X accept(SemanticQueryWalker walker) { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // JPA + @Override + public EntityDomainType getModel() { + return (EntityDomainType) getReferencedPathSource(); + } + @Override public EntityDomainType getManagedType() { - return getReferencedPathSource(); + return getModel(); } @Override @@ -188,11 +188,6 @@ public boolean containsOnlyInnerJoins() { return !hasTreats(); } - @Override - public EntityDomainType getModel() { - return getReferencedPathSource(); - } - @Override public SqmTreatedRoot treatAs(Class treatJavaType) throws PathException { return treatAs( nodeBuilder().getDomainModel().entity( treatJavaType ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java index 37ef9cf881b9..25435fd28d10 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java @@ -20,12 +20,14 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.cte.SqmCteContainer; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; import jakarta.persistence.metamodel.EntityType; /** @@ -139,6 +141,24 @@ public SqmRoot from(Class entityClass) { } + @Override + public SqmDerivedRoot from(Subquery subquery) { + return from( subquery, false ); + } + + @Override + public SqmDerivedRoot fromLateral(Subquery subquery) { + return from( subquery, true ); + } + + @Override + public SqmDerivedRoot from(Subquery subquery, boolean lateral) { + validateComplianceFromSubQuery(); + final SqmDerivedRoot root = new SqmDerivedRoot<>( (SqmSubQuery) subquery, null, lateral ); + addRoot( root ); + return root; + } + private SqmRoot addRoot(SqmRoot root) { getQuerySpec().addRoot( root ); return root; @@ -149,6 +169,13 @@ public SqmRoot from(EntityType entityType) { return addRoot( new SqmRoot<>( (EntityDomainType) entityType, null, true, nodeBuilder() ) ); } + private void validateComplianceFromSubQuery() { + if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support subqueries in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Selection diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java index e7700324dccd..9f88cb30bedf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java @@ -401,12 +401,14 @@ public JpaExpression getOffset() { @Override public JpaCriteriaQuery offset(JpaExpression offset) { + validateComplianceFetchOffset(); getQueryPart().setOffset( offset ); return this; } @Override public JpaCriteriaQuery offset(Number offset) { + validateComplianceFetchOffset(); getQueryPart().setOffset( nodeBuilder().value( offset ) ); return this; } @@ -419,24 +421,28 @@ public JpaExpression getFetch() { @Override public JpaCriteriaQuery fetch(JpaExpression fetch) { + validateComplianceFetchOffset(); getQueryPart().setFetch( fetch ); return this; } @Override public JpaCriteriaQuery fetch(JpaExpression fetch, FetchClauseType fetchClauseType) { + validateComplianceFetchOffset(); getQueryPart().setFetch( fetch, fetchClauseType ); return this; } @Override public JpaCriteriaQuery fetch(Number fetch) { + validateComplianceFetchOffset(); getQueryPart().setFetch( nodeBuilder().value( fetch ) ); return this; } @Override public JpaCriteriaQuery fetch(Number fetch, FetchClauseType fetchClauseType) { + validateComplianceFetchOffset(); getQueryPart().setFetch( nodeBuilder().value( fetch ), fetchClauseType ); return this; } @@ -445,4 +451,12 @@ public JpaCriteriaQuery fetch(Number fetch, FetchClauseType fetchClauseType) public FetchClauseType getFetchClauseType() { return getQueryPart().getFetchClauseType(); } + + private void validateComplianceFetchOffset() { + if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support the fetch or offset clause. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index 7c508884f343..803392ce08e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -14,8 +14,11 @@ import java.util.Map; import java.util.Set; +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaOrder; import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.JpaSubQuery; +import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmExpressible; @@ -47,12 +50,14 @@ import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.type.descriptor.java.JavaType; +import jakarta.persistence.Tuple; import jakarta.persistence.criteria.AbstractQuery; import jakarta.persistence.criteria.CollectionJoin; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.ListJoin; import jakarta.persistence.criteria.MapJoin; +import jakarta.persistence.criteria.Order; import jakarta.persistence.criteria.PluralJoin; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; @@ -165,6 +170,90 @@ public SqmSubQuery select(Expression expression) { return this; } + @Override + @SuppressWarnings("unchecked") + public SqmSubQuery multiselect(Selection... selections) { + validateComplianceMultiselect(); + + final Selection resultSelection; + Class resultType = getResultType(); + if ( resultType == null || resultType == Object.class ) { + switch ( selections.length ) { + case 0: { + throw new IllegalArgumentException( + "empty selections passed to criteria query typed as Object" + ); + } + case 1: { + resultSelection = ( Selection ) selections[0]; + break; + } + default: { + setResultType( (Class) Object[].class ); + resultSelection = ( Selection ) nodeBuilder().array( selections ); + } + } + } + else if ( Tuple.class.isAssignableFrom( resultType ) ) { + resultSelection = ( Selection ) nodeBuilder().tuple( selections ); + } + else if ( resultType.isArray() ) { + resultSelection = nodeBuilder().array( resultType, selections ); + } + else { + resultSelection = nodeBuilder().construct( resultType, selections ); + } + final SqmQuerySpec querySpec = getQuerySpec(); + if ( querySpec.getSelectClause() == null ) { + querySpec.setSelectClause( new SqmSelectClause( false, 1, nodeBuilder() ) ); + } + querySpec.setSelection( (JpaSelection) resultSelection ); + return this; + } + + @Override + @SuppressWarnings("unchecked") + public SqmSubQuery multiselect(List> selectionList) { + validateComplianceMultiselect(); + + final Selection resultSelection; + final Class resultType = getResultType(); + final List> selections = (List>) (List) selectionList; + if ( resultType == null || resultType == Object.class ) { + switch ( selections.size() ) { + case 0: { + throw new IllegalArgumentException( + "empty selections passed to criteria query typed as Object" + ); + } + case 1: { + resultSelection = ( Selection ) selections.get( 0 ); + break; + } + default: { + setResultType( (Class) Object[].class ); + resultSelection = ( Selection ) nodeBuilder().array( selections ); + } + } + } + else if ( Tuple.class.isAssignableFrom( resultType ) ) { + resultSelection = ( Selection ) nodeBuilder().tuple( selections ); + } + else if ( resultType.isArray() ) { + resultSelection = nodeBuilder().array( resultType, selections ); + } + else { + resultSelection = nodeBuilder().construct( resultType, selections ); + } + final SqmQuerySpec querySpec = getQuerySpec(); + if ( querySpec.getSelectClause() == null ) { + querySpec.setSelectClause( new SqmSelectClause( false, 1, nodeBuilder() ) ); + } + querySpec.setSelection( (JpaSelection) resultSelection ); + + return this; + } + @Override public SqmExpression getSelection() { final SqmSelectClause selectClause = getQuerySpec().getSelectClause(); @@ -227,6 +316,117 @@ public SqmSubQuery having(Predicate... predicates) { return (SqmSubQuery) super.having( predicates ); } + @Override + public JpaExpression getOffset() { + //noinspection unchecked + return (JpaExpression) getQueryPart().getOffset(); + } + + @Override + public JpaSubQuery offset(JpaExpression offset) { + validateComplianceFetchOffset(); + getQueryPart().setOffset( offset ); + return this; + } + + @Override + public JpaSubQuery offset(Number offset) { + validateComplianceFetchOffset(); + getQueryPart().setOffset( nodeBuilder().value( offset ) ); + return this; + } + + @Override + public JpaExpression getFetch() { + //noinspection unchecked + return (JpaExpression) getQueryPart().getFetch(); + } + + @Override + public JpaSubQuery fetch(JpaExpression fetch) { + validateComplianceFetchOffset(); + getQueryPart().setFetch( fetch ); + return this; + } + + @Override + public JpaSubQuery fetch(JpaExpression fetch, FetchClauseType fetchClauseType) { + validateComplianceFetchOffset(); + getQueryPart().setFetch( fetch, fetchClauseType ); + return this; + } + + @Override + public JpaSubQuery fetch(Number fetch) { + validateComplianceFetchOffset(); + getQueryPart().setFetch( nodeBuilder().value( fetch ) ); + return this; + } + + @Override + public JpaSubQuery fetch(Number fetch, FetchClauseType fetchClauseType) { + validateComplianceFetchOffset(); + getQueryPart().setFetch( nodeBuilder().value( fetch ), fetchClauseType ); + return this; + } + + @Override + public FetchClauseType getFetchClauseType() { + return getQueryPart().getFetchClauseType(); + } + + @Override + public List getOrderList() { + //noinspection rawtypes,unchecked + return (List) getQueryPart().getSortSpecifications(); + } + + @Override + public JpaSubQuery orderBy(Order... orders) { + validateComplianceOrderBy(); + final SqmOrderByClause sqmOrderByClause = new SqmOrderByClause( orders.length ); + for ( Order order : orders ) { + sqmOrderByClause.addSortSpecification( (SqmSortSpecification) order ); + } + getQueryPart().setOrderByClause( sqmOrderByClause ); + return this; + } + + @Override + public JpaSubQuery orderBy(List orders) { + validateComplianceOrderBy(); + final SqmOrderByClause sqmOrderByClause = new SqmOrderByClause( orders.size() ); + for ( Order order : orders ) { + sqmOrderByClause.addSortSpecification( (SqmSortSpecification) order ); + } + getQueryPart().setOrderByClause( sqmOrderByClause ); + return this; + } + + private void validateComplianceMultiselect() { + if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support subqueries having multiple select items. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } + + private void validateComplianceOrderBy() { + if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support subqueries having an order by clause. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } + + private void validateComplianceFetchOffset() { + if ( nodeBuilder().getDomainModel().getJpaCompliance().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support subqueries having a fetch or offset clause. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } + @Override public SqmRoot correlate(Root parentRoot) { final SqmCorrelatedRoot correlated = ( (SqmRoot) parentRoot ).createCorrelation(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/DelegatingTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/DelegatingTableGroup.java index 4231f41457ca..32ed9684ea4d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/DelegatingTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/DelegatingTableGroup.java @@ -89,7 +89,8 @@ public TableReference getTableReference(String tableExpression) { public TableReference getTableReference( NavigablePath navigablePath, String tableExpression, - boolean allowFkOptimization, boolean resolve) { + boolean allowFkOptimization, + boolean resolve) { return getTableGroup().getTableReference( navigablePath, tableExpression, allowFkOptimization, resolve ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java index 78d744a47493..9903588108c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/QueryPartTableGroup.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -23,7 +24,7 @@ public class QueryPartTableGroup extends AbstractTableGroup { private final QueryPartTableReference queryPartTableReference; - private final String compatibleTableExpression; + private final Set compatibleTableExpressions; public QueryPartTableGroup( NavigablePath navigablePath, @@ -40,7 +41,7 @@ public QueryPartTableGroup( queryPart, sourceAlias, columnNames, - null, + Collections.emptySet(), lateral, canUseInnerJoins, sessionFactory @@ -53,7 +54,8 @@ public QueryPartTableGroup( QueryPart queryPart, String sourceAlias, List columnNames, - String compatibleTableExpression, boolean lateral, + Set compatibleTableExpressions, + boolean lateral, boolean canUseInnerJoins, SessionFactoryImplementor sessionFactory) { super( @@ -64,7 +66,7 @@ public QueryPartTableGroup( null, sessionFactory ); - this.compatibleTableExpression = compatibleTableExpression; + this.compatibleTableExpressions = compatibleTableExpressions; this.queryPartTableReference = new QueryPartTableReference( queryPart, sourceAlias, @@ -85,7 +87,7 @@ protected TableReference getTableReferenceInternal( String tableExpression, boolean allowFkOptimization, boolean resolve) { - if ( Objects.equals( tableExpression, compatibleTableExpression ) ) { + if ( compatibleTableExpressions.contains( tableExpression ) ) { return getPrimaryTableReference(); } for ( TableGroupJoin tableGroupJoin : getNestedTableGroupJoins() ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java new file mode 100644 index 000000000000..4c9967dc400b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromEmbeddedIdTests.java @@ -0,0 +1,328 @@ +/* + * 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.query; + +import java.util.List; +import java.util.function.Consumer; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.criteria.JpaSubQuery; +import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +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 jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = SubQueryInFromEmbeddedIdTests.Contact.class) +@SessionFactory +public class SubQueryInFromEmbeddedIdTests { + + @Test + @FailureExpected(reason = "Support for embedded id association selecting in from clause sub queries not yet supported") + public void testEntity(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.asc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + + cq.multiselect( root.get( "name" ), a.get( "contact" ).get( "id" ) ); + cq.where( root.get( "alternativeContact" ).isNotNull() ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.contact.id from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by address.name.first" + + "limit 1" + + ") a " + + "where c.alternativeContact is not null", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( 2, list.get( 0 ).get( 1, Contact.ContactId.class ).getId1() ); + assertEquals( 2, list.get( 0 ).get( 1, Contact.ContactId.class ).getId2() ); + } + ); + } + ); + } + + @Test + @FailureExpected(reason = "Support for embedded id association selecting in from clause sub queries not yet supported") + public void testEntityJoin(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.desc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + final SqmAttributeJoin alt = a.join( "contact" ); + + cq.multiselect( root.get( "name" ), alt.get( "name" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, alt.name from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first desc" + + "limit 1" + + ") a " + + "join a.contact alt", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + @FailureExpected(reason = "Support for embedded id association selecting in from clause sub queries not yet supported") + public void testEntityImplicit(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.desc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + + cq.multiselect( root.get( "name" ), a.get( "contact" ).get( "name" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.contact.name from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first desc" + + "limit 1" + + ") a", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Contact contact = new Contact( + 1, + new Contact.Name( "John", "Doe" ) + ); + final Contact alternativeContact = new Contact( + 2, + new Contact.Name( "Jane", "Doe" ) + ); + final Contact alternativeContact2 = new Contact( + 3, + new Contact.Name( "Granny", "Doe" ) + ); + alternativeContact.setAlternativeContact( alternativeContact2 ); + contact.setAlternativeContact( alternativeContact ); + session.persist( alternativeContact2 ); + session.persist( alternativeContact ); + session.persist( contact ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createQuery( "delete Contact" ).executeUpdate(); + } ); + } + + private void verifySame(T criteriaResult, T hqlResult, Consumer verifier) { + verifier.accept( criteriaResult ); + verifier.accept( hqlResult ); + } + + /** + * @author Steve Ebersole + */ + @Entity( name = "Contact") + @Table( name = "contacts" ) + @SecondaryTable( name="contact_supp" ) + public static class Contact { + private ContactId id; + private Name name; + + private Contact alternativeContact; + + public Contact() { + } + + public Contact(Integer id, Name name) { + this.id = new ContactId( id, id ); + this.name = name; + } + + @EmbeddedId + public ContactId getId() { + return id; + } + + public void setId(ContactId id) { + this.id = id; + } + + public Name getName() { + return name; + } + + public void setName(Name name) { + this.name = name; + } + + @ManyToOne(fetch = FetchType.LAZY) + public Contact getAlternativeContact() { + return alternativeContact; + } + + public void setAlternativeContact(Contact alternativeContact) { + this.alternativeContact = alternativeContact; + } + + @Embeddable + public static class ContactId { + private Integer id1; + private Integer id2; + + public ContactId() { + } + + public ContactId(Integer id1, Integer id2) { + this.id1 = id1; + this.id2 = id2; + } + + public Integer getId1() { + return id1; + } + + public void setId1(Integer id1) { + this.id1 = id1; + } + + public Integer getId2() { + return id2; + } + + public void setId2(Integer id2) { + this.id2 = id2; + } + } + + @Embeddable + public static class Name { + private String first; + private String last; + + public Name() { + } + + public Name(String first, String last) { + this.first = first; + this.last = last; + } + + @Column(name = "firstname") + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + @Column(name = "lastname") + public String getLast() { + return last; + } + + public void setLast(String last) { + this.last = last; + } + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java new file mode 100644 index 000000000000..4e69d5c6a9dc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromIdClassTests.java @@ -0,0 +1,309 @@ +/* + * 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.query; + +import java.util.function.Consumer; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.criteria.JpaSubQuery; +import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +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 jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = SubQueryInFromIdClassTests.Contact.class) +@SessionFactory +public class SubQueryInFromIdClassTests { + + @Test + @FailureExpected(reason = "Support for id class association selecting in from clause sub queries not yet supported") + public void testEntity(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.asc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + + cq.multiselect( root.get( "name" ), a.get( "contact" ).get( "id" ) ); + cq.where( root.get( "alternativeContact" ).isNotNull() ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.contact.id1, a.contact.id2 from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by address.name.first" + + "limit 1" + + ") a " + + "where c.alternativeContact is not null", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( 2, list.get( 0 ).get( 1, Integer.class ) ); + assertEquals( 2, list.get( 0 ).get( 2, Integer.class ) ); + } + ); + } + ); + } + + @Test + @FailureExpected(reason = "Support for id class association selecting in from clause sub queries not yet supported") + public void testEntityJoin(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.desc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + final SqmAttributeJoin alt = a.join( "contact" ); + + cq.multiselect( root.get( "name" ), alt.get( "name" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, alt.name from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first desc" + + "limit 1" + + ") a " + + "join a.contact alt", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + @FailureExpected(reason = "Support for id class association selecting in from clause sub queries not yet supported") + public void testEntityImplicit(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.desc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + + cq.multiselect( root.get( "name" ), a.get( "contact" ).get( "name" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.contact.name from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first desc" + + "limit 1" + + ") a", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Contact contact = new Contact( + 1, + new Contact.Name( "John", "Doe" ) + ); + final Contact alternativeContact = new Contact( + 2, + new Contact.Name( "Jane", "Doe" ) + ); + final Contact alternativeContact2 = new Contact( + 3, + new Contact.Name( "Granny", "Doe" ) + ); + alternativeContact.setAlternativeContact( alternativeContact2 ); + contact.setAlternativeContact( alternativeContact ); + session.persist( alternativeContact2 ); + session.persist( alternativeContact ); + session.persist( contact ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createQuery( "delete Contact" ).executeUpdate(); + } ); + } + + private void verifySame(T criteriaResult, T hqlResult, Consumer verifier) { + verifier.accept( criteriaResult ); + verifier.accept( hqlResult ); + } + + /** + * @author Steve Ebersole + */ + @Entity( name = "Contact") + @Table( name = "contacts" ) + @SecondaryTable( name="contact_supp" ) + public static class Contact { + private Integer id1; + private Integer id2; + private Name name; + + private Contact alternativeContact; + + public Contact() { + } + + public Contact(Integer id, Name name) { + this.id1 = id; + this.id2 = id; + this.name = name; + } + + @Id + public Integer getId1() { + return id1; + } + + public void setId1(Integer id1) { + this.id1 = id1; + } + + @Id + public Integer getId2() { + return id2; + } + + public void setId2(Integer id2) { + this.id2 = id2; + } + + public Name getName() { + return name; + } + + public void setName(Name name) { + this.name = name; + } + + @ManyToOne(fetch = FetchType.LAZY) + public Contact getAlternativeContact() { + return alternativeContact; + } + + public void setAlternativeContact(Contact alternativeContact) { + this.alternativeContact = alternativeContact; + } + + @Embeddable + public static class Name { + private String first; + private String last; + + public Name() { + } + + public Name(String first, String last) { + this.first = first; + this.last = last; + } + + @Column(name = "firstname") + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + @Column(name = "lastname") + public String getLast() { + return last; + } + + public void setLast(String last) { + this.last = last; + } + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java new file mode 100644 index 000000000000..1600e3054e1e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQueryInFromTests.java @@ -0,0 +1,345 @@ +/* + * 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.query; + +import java.time.LocalDate; +import java.util.List; +import java.util.function.Consumer; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.criteria.JpaSubQuery; +import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.domain.contacts.Address; +import org.hibernate.testing.orm.domain.contacts.Contact; +import org.hibernate.testing.orm.junit.DomainModel; +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 jakarta.persistence.Tuple; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.CONTACTS) +@SessionFactory +public class SubQueryInFromTests { + + @Test + public void testBasicRoot(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root subQueryRoot = subquery.from( Contact.class ); + + subquery.multiselect( subQueryRoot.get( "name" ).get( "first" ).alias( "firstName" ) ); + subquery.orderBy( cb.asc( subQueryRoot.get( "id" ) ) ); + subquery.fetch( 1 ); + + final JpaRoot root = cq.from( subquery ); + cq.multiselect( root.get( "firstName" ) ); + + final QueryImplementor query = session.createQuery( + "select a.firstName " + + "from (" + + "select c.name.first as firstName " + + "from Contact c " + + "order by c.id " + + "limit 1" + + ") a", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, String.class ) ); + } + ); + } + ); + } + + @Test + public void testBasic(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join address = correlatedRoot.join( "addresses" ); + + subquery.multiselect( address.get( "line1" ).alias( "address" ) ); + subquery.orderBy( cb.asc( address.get( "line1" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.INNER ); + + cq.multiselect( root.get( "name" ), a.get( "address" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.address from Contact c " + + "join lateral (" + + "select address.line1 as address " + + "from c.addresses address " + + "order by address.line1 " + + "limit 1" + + ") a", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Street 1", list.get( 0 ).get( 1, String.class ) ); + } + ); + } + ); + } + + @Test + public void testEmbedded(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join address = correlatedRoot.join( "addresses" ); + + subquery.multiselect( address.get( "postalCode" ).alias( "zip" ) ); + subquery.orderBy( cb.asc( address.get( "line1" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.INNER ); + + cq.multiselect( root.get( "name" ), a.get( "zip" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.zip from Contact c " + + "join lateral (" + + "select address.postalCode as zip " + + "from c.addresses address " + + "order by address.line1 " + + "limit 1" + + ") a", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( 1234, list.get( 0 ).get( 1, Address.PostalCode.class ).getZipCode() ); + } + ); + } + ); + } + + @Test + public void testEntity(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.asc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + + cq.multiselect( root.get( "name" ), a.get( "contact" ).get( "id" ) ); + cq.where( root.get( "alternativeContact" ).isNotNull() ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.contact.id from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first " + + "limit 1" + + ") a " + + "where c.alternativeContact is not null", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( 2, list.get( 0 ).get( 1, Integer.class ) ); + } + ); + } + ); + } + + @Test + public void testEntityJoin(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.desc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + final SqmAttributeJoin alt = a.join( "contact" ); + + cq.multiselect( root.get( "name" ), alt.get( "name" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, alt.name from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first desc " + + "limit 1" + + ") a " + + "join a.contact alt", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @Test + public void testEntityImplicit(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Contact.class ); + final JpaSubQuery subquery = cq.subquery( Tuple.class ); + final Root correlatedRoot = subquery.correlate( root ); + final Join alternativeContact = correlatedRoot.join( "alternativeContact" ); + + subquery.multiselect( alternativeContact.alias( "contact" ) ); + subquery.orderBy( cb.desc( alternativeContact.get( "name" ).get( "first" ) ) ); + subquery.fetch( 1 ); + + final JpaDerivedJoin a = root.joinLateral( subquery, SqmJoinType.LEFT ); + + cq.multiselect( root.get( "name" ), a.get( "contact" ).get( "name" ) ); + + final QueryImplementor query = session.createQuery( + "select c.name, a.contact.name from Contact c " + + "left join lateral (" + + "select alt as contact " + + "from c.alternativeContact alt " + + "order by alt.name.first desc " + + "limit 1" + + ") a", + Tuple.class + ); + verifySame( + session.createQuery( cq ).getResultList(), + query.getResultList(), + list -> { + assertEquals( 1, list.size() ); + assertEquals( "John", list.get( 0 ).get( 0, Contact.Name.class ).getFirst() ); + assertEquals( "Granny", list.get( 0 ).get( 1, Contact.Name.class ).getFirst() ); + } + ); + } + ); + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + final Contact contact = new Contact( + 1, + new Contact.Name( "John", "Doe" ), + Contact.Gender.MALE, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact alternativeContact = new Contact( + 2, + new Contact.Name( "Jane", "Doe" ), + Contact.Gender.FEMALE, + LocalDate.of( 1970, 1, 1 ) + ); + final Contact alternativeContact2 = new Contact( + 3, + new Contact.Name( "Granny", "Doe" ), + Contact.Gender.FEMALE, + LocalDate.of( 1970, 1, 1 ) + ); + alternativeContact.setAlternativeContact( alternativeContact2 ); + contact.setAlternativeContact( alternativeContact ); + contact.setAddresses( + List.of( + new Address( "Street 1", 1234 ), + new Address( "Street 2", 5678 ) + ) + ); + session.persist( alternativeContact2 ); + session.persist( alternativeContact ); + session.persist( contact ); + } ); + } + + private void verifySame(T criteriaResult, T hqlResult, Consumer verifier) { + verifier.accept( criteriaResult ); + verifier.accept( hqlResult ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createQuery( "delete Contact" ).executeUpdate(); + } ); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Address.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Address.java index 3d495eca06cd..f2ce6e3f5601 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Address.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Address.java @@ -16,7 +16,15 @@ public class Address { private Classification classification; private String line1; private String line2; - private PostalCode postalCode; + private PostalCode postalCode = new PostalCode(); + + public Address() { + } + + public Address(String line1, int zip) { + this.line1 = line1; + this.postalCode.setZipCode( zip ); + } public Classification getClassification() { return classification; diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java index c60f6810443c..0e998cc19aa8 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/domain/contacts/Contact.java @@ -13,7 +13,9 @@ import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OrderColumn; import jakarta.persistence.SecondaryTable; import jakarta.persistence.Table; @@ -33,6 +35,7 @@ public class Contact { private LocalDate birthDay; + private Contact alternativeContact; private List
addresses; private List phoneNumbers; @@ -81,6 +84,15 @@ public void setBirthDay(LocalDate birthDay) { this.birthDay = birthDay; } + @ManyToOne(fetch = FetchType.LAZY) + public Contact getAlternativeContact() { + return alternativeContact; + } + + public void setAlternativeContact(Contact alternativeContact) { + this.alternativeContact = alternativeContact; + } + @ElementCollection @CollectionTable( name = "contact_addresses" ) // NOTE : because of the @OrderColumn `addresses` is a List, while `phoneNumbers` is