diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index f8ef6b3fb2..7f516f7e69 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -76,7 +76,7 @@ public class DB2LegacySqlAstTranslator extends Abstract } @Override - protected void renderTableReferenceJoins(TableGroup tableGroup) { + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { // When we are in a recursive CTE, we can't render joins on DB2... // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 if ( isInRecursiveQueryPart() ) { @@ -103,7 +103,7 @@ public class DB2LegacySqlAstTranslator extends Abstract } } else { - super.renderTableReferenceJoins( tableGroup ); + super.renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java index 7213cd1690..c95d269072 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java @@ -340,13 +340,9 @@ public class H2LegacySqlAstTranslator extends AbstractS final TableReference tableRef = tableGroup.getPrimaryTableReference(); // The H2 parser can't handle a sub-query as first element in a nested join // i.e. `join ( (select ...) alias join ... )`, so we have to introduce a dummy table reference - if ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) { - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( CollectionHelper.isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); - if ( realTableGroup ) { - appendSql( "dual cross join " ); - } + if ( getSqlBuffer().charAt( getSqlBuffer().length() - 1 ) == '(' + && ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) ) { + appendSql( "dual cross join " ); } return super.renderPrimaryTableReference( tableGroup, lockMode ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java index 8f7e958ee2..0c4fb51be2 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java @@ -12,20 +12,14 @@ import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; -import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; -import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; -import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; @@ -147,8 +141,7 @@ public class HSQLLegacySqlAstTranslator extends Abstrac protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -172,8 +165,7 @@ public class HSQLLegacySqlAstTranslator extends Abstrac protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -193,11 +185,11 @@ public class HSQLLegacySqlAstTranslator extends Abstrac } } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSearchedExpression caseSearchedExpression) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -205,7 +197,7 @@ public class HSQLLegacySqlAstTranslator extends Abstrac return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -214,11 +206,11 @@ public class HSQLLegacySqlAstTranslator extends Abstrac return false; } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSimpleExpression caseSimpleExpression) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -226,7 +218,7 @@ public class HSQLLegacySqlAstTranslator extends Abstrac return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -235,6 +227,13 @@ public class HSQLLegacySqlAstTranslator extends Abstrac return false; } + private boolean isStringLiteral( Expression expression ) { + if ( expression instanceof Literal ) { + return ( (Literal) expression ).getJdbcMapping().getJdbcType().isStringLike(); + } + return false; + } + @Override public boolean supportsFilterClause() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java index 4e057b1d74..7612266186 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java @@ -75,7 +75,7 @@ public class DB2SqlAstTranslator extends AbstractSqlAst } @Override - protected void renderTableReferenceJoins(TableGroup tableGroup) { + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { // When we are in a recursive CTE, we can't render joins on DB2... // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 if ( isInRecursiveQueryPart() ) { @@ -102,7 +102,7 @@ public class DB2SqlAstTranslator extends AbstractSqlAst } } else { - super.renderTableReferenceJoins( tableGroup ); + super.renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 4554d948da..093530fd5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -314,13 +314,9 @@ public class H2SqlAstTranslator extends SqlAstTranslato final TableReference tableRef = tableGroup.getPrimaryTableReference(); // The H2 parser can't handle a sub-query as first element in a nested join // i.e. `join ( (select ...) alias join ... )`, so we have to introduce a dummy table reference - if ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) { - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); - if ( realTableGroup ) { - appendSql( "dual cross join " ); - } + if ( getSqlBuffer().charAt( getSqlBuffer().length() - 1 ) == '(' + && ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) ) { + appendSql( "dual cross join " ); } return super.renderPrimaryTableReference( tableGroup, lockMode ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java index f458720b9b..aac2ecdc23 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java @@ -12,14 +12,12 @@ import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; import org.hibernate.sql.ast.tree.expression.Expression; @@ -152,8 +150,7 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -177,8 +174,7 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -198,11 +194,11 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs } } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSearchedExpression caseSearchedExpression) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -210,7 +206,7 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -219,11 +215,11 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs return false; } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSimpleExpression caseSimpleExpression) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -231,7 +227,7 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -240,6 +236,13 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs return false; } + private boolean isStringLiteral( Expression expression ) { + if ( expression instanceof Literal ) { + return ( (Literal) expression ).getJdbcMapping().getJdbcType().isStringLike(); + } + return false; + } + @Override public boolean supportsFilterClause() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java index 8f8d17ac31..3d99255d11 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AbstractCompositeIdentifierMapping.java @@ -48,6 +48,8 @@ import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Base implementation for composite identifier mappings * @@ -129,9 +131,9 @@ public abstract class AbstractCompositeIdentifierMapping public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -154,11 +156,11 @@ public abstract class AbstractCompositeIdentifierMapping public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java index e670909c01..9e0abf088b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java @@ -292,6 +292,14 @@ public interface EntityMappingType */ EntityIdentifierMapping getIdentifierMapping(); + /** + * Mapping details for the entity's identifier. This is shared across all + * entity mappings within an inheritance hierarchy. + */ + default EntityIdentifierMapping getIdentifierMappingForJoin() { + return getIdentifierMapping(); + } + /** * Mapping details for the entity's discriminator. This is shared across all * entity mappings within an inheritance hierarchy. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java index 08b66db39c..892754ab17 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java @@ -6,7 +6,9 @@ */ package org.hibernate.metamodel.mapping.internal; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -255,6 +257,24 @@ public class CaseStatementDiscriminatorMappingImpl extends AbstractDiscriminator this.entityTableGroup = entityTableGroup; } + public List getUsedTableReferences() { + final ArrayList usedTableReferences = new ArrayList<>( tableDiscriminatorDetailsMap.size() ); + tableDiscriminatorDetailsMap.forEach( + (tableName, tableDiscriminatorDetails) -> { + final TableReference tableReference = entityTableGroup.getTableReference( + entityTableGroup.getNavigablePath(), + tableName, + false + ); + + if ( tableReference != null ) { + usedTableReferences.add( tableReference ); + } + } + ); + return usedTableReferences; + } + @Override public void renderToSql( SqlAppender sqlAppender, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java index 3c40af99c0..c539daeaab 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java @@ -52,6 +52,8 @@ import org.hibernate.type.AnyType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Singular, any-valued attribute * @@ -487,9 +489,9 @@ public class DiscriminatedAssociationAttributeMapping public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -512,11 +514,11 @@ public class DiscriminatedAssociationAttributeMapping public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java index 280c782c9c..81d86e6f5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java @@ -45,6 +45,10 @@ import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.type.AnyType; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -343,19 +347,13 @@ public class DiscriminatedCollectionPart implements DiscriminatedAssociationMode public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -374,11 +372,11 @@ public class DiscriminatedCollectionPart implements DiscriminatedAssociationMode public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java index 9308988f03..a47872909d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java @@ -53,6 +53,10 @@ import org.hibernate.sql.results.graph.embeddable.internal.AggregateEmbeddableRe import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -318,13 +322,13 @@ public class EmbeddedAttributeMapping public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -343,11 +347,11 @@ public class EmbeddedAttributeMapping public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java index 190b5fc138..aa49eb298c 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java @@ -13,7 +13,6 @@ import java.util.function.Consumer; import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -21,7 +20,6 @@ import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.property.access.internal.PropertyAccessStrategyBasicImpl; @@ -41,7 +39,6 @@ import org.hibernate.sql.ast.tree.from.PluralTableGroup; 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; @@ -49,12 +46,15 @@ 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.Fetchable; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -231,19 +231,13 @@ public class EmbeddedCollectionPart implements CollectionPart, EmbeddableValuedF public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -262,11 +256,11 @@ public class EmbeddedCollectionPart implements CollectionPart, EmbeddableValuedF public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { assert lhs.getModelPart() instanceof PluralAttributeMapping; return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java index 4e262fc3fc..f0818f97df 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java @@ -56,6 +56,8 @@ import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.type.EntityType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyOrder; @@ -258,14 +260,13 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -302,11 +303,11 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); @@ -393,7 +394,8 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple fkTargetModelPart = resolveNamedTargetPart( mapKeyPropertyName, entityMappingType, collectionDescriptor ); } else { - fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); + fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMappingForJoin(); +// fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); } } else if ( StringHelper.isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { @@ -449,7 +451,8 @@ public class ManyToManyCollectionPart extends AbstractEntityCollectionPart imple } else { // non-inverse @ManyToMany - fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); + fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMappingForJoin(); +// fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); } if ( getNature() == Nature.ELEMENT ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index 53e824e8d4..b91ab23374 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -116,6 +116,8 @@ import org.hibernate.type.descriptor.java.MutabilityPlan; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.metamodel.mapping.MappingModelCreationLogging.MAPPING_MODEL_CREATION_MESSAGE_LOGGER; import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; @@ -775,7 +777,8 @@ public class MappingModelCreationHelper { } if ( isReferenceToPrimaryKey ) { - fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMapping(); + fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMappingForJoin(); +// fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMapping(); } else { fkTargetPart = declaringType.findContainingEntityMapping().findSubPart( lhsPropertyName ); @@ -928,7 +931,8 @@ public class MappingModelCreationHelper { final ModelPart fkTarget; if ( bootValueMapping.isReferenceToPrimaryKey() ) { - fkTarget = referencedEntityDescriptor.getIdentifierMapping(); + fkTarget = referencedEntityDescriptor.getIdentifierMappingForJoin(); +// fkTarget = referencedEntityDescriptor.getIdentifierMapping(); } else { fkTarget = referencedEntityDescriptor.findByPath( bootValueMapping.getReferencedPropertyName() ); @@ -1677,9 +1681,9 @@ public class MappingModelCreationHelper { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -1690,11 +1694,11 @@ public class MappingModelCreationHelper { public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java index d6a082027d..495641d6c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java @@ -31,6 +31,8 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; /** @@ -154,9 +156,9 @@ public class OneToManyCollectionPart extends AbstractEntityCollectionPart implem public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -193,11 +195,11 @@ public class OneToManyCollectionPart extends AbstractEntityCollectionPart implem public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return createTableGroupInternal( true, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java index a4e0ded48e..62d3b3313a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java @@ -75,6 +75,8 @@ import org.hibernate.sql.results.graph.collection.internal.SelectEagerCollection import org.jboss.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; @@ -683,9 +685,9 @@ public class PluralAttributeMappingImpl public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -802,7 +804,7 @@ public class PluralAttributeMappingImpl } } - public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( hasSoftDelete() ) { return SqlAstJoinType.LEFT; } @@ -824,11 +826,11 @@ public class PluralAttributeMappingImpl public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return createRootTableGroupJoin( navigablePath, @@ -861,8 +863,10 @@ public class PluralAttributeMappingImpl if ( collectionDescriptor.isOneToMany() ) { tableGroup = createOneToManyTableGroup( lhs.canUseInnerJoins() && joinType == SqlAstJoinType.INNER, + joinType, navigablePath, fetched, + addsPredicate, explicitSourceAlias, sqlAliasBase, creationState @@ -896,8 +900,10 @@ public class PluralAttributeMappingImpl private TableGroup createOneToManyTableGroup( boolean canUseInnerJoins, + SqlAstJoinType joinType, NavigablePath navigablePath, boolean fetched, + boolean addsPredicate, String sourceAlias, SqlAliasBase explicitSqlAliasBase, SqlAstCreationState creationState) { @@ -920,6 +926,12 @@ public class PluralAttributeMappingImpl elementTableGroup, creationState.getCreationContext().getSessionFactory() ); + // For inner joins we never need join nesting + final boolean nestedJoin = joinType != SqlAstJoinType.INNER + // For outer joins we need nesting if there might be an on-condition that refers to the element table + && ( addsPredicate + || isAffectedByEnabledFilters( creationState.getLoadQueryInfluencers(), creationState.applyOnlyLoadByKeyFilters() ) + || collectionDescriptor.hasWhereRestrictions() ); if ( indexDescriptor instanceof TableGroupJoinProducer ) { final TableGroupJoin tableGroupJoin = ( (TableGroupJoinProducer) indexDescriptor ).createTableGroupJoin( @@ -932,7 +944,7 @@ public class PluralAttributeMappingImpl false, creationState ); - tableGroup.registerIndexTableGroup( tableGroupJoin ); + tableGroup.registerIndexTableGroup( tableGroupJoin, nestedJoin ); } return tableGroup; @@ -1022,8 +1034,10 @@ public class PluralAttributeMappingImpl if ( getCollectionDescriptor().isOneToMany() ) { return createOneToManyTableGroup( canUseInnerJoins, + SqlAstJoinType.INNER, navigablePath, false, + false, explicitSourceAlias, explicitSqlAliasBase, creationState diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index 7e4846cccb..ff0a35ec53 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -66,6 +66,7 @@ import org.hibernate.persister.entity.JoinedSubclassEntityPersister; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; import org.hibernate.spi.TreatedNavigablePath; @@ -106,13 +107,14 @@ import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl; import org.hibernate.sql.results.internal.NullValueAssembler; import org.hibernate.sql.results.internal.domain.CircularBiDirectionalFetchImpl; import org.hibernate.sql.results.internal.domain.CircularFetchImpl; -import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EmbeddedComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; /** @@ -855,6 +857,7 @@ public class ToOneAttributeMapping // * the association does not force a join (`@NotFound`, nullable 1-1, ...) // Otherwise we need to join to the associated entity table(s) final boolean forceJoin = hasNotFoundAction() + || entityMappingType.getSoftDeleteMapping() != null || ( cardinality == Cardinality.ONE_TO_ONE && isNullable() ); this.canUseParentTableGroup = ! forceJoin && sideNature == ForeignKeyDescriptor.Nature.KEY @@ -1978,9 +1981,9 @@ public class ToOneAttributeMapping public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -2072,7 +2075,11 @@ public class ToOneAttributeMapping final TableGroupJoin join = new TableGroupJoin( navigablePath, - joinType, + // Avoid checking for nested joins in here again, since this is already done in createRootTableGroupJoin + // and simply rely on the canUseInnerJoins flag instead for override the join type to LEFT + requestedJoinType == null && !lazyTableGroup.canUseInnerJoins() + ? SqlAstJoinType.LEFT + : joinType, lazyTableGroup, null ); @@ -2144,7 +2151,7 @@ public class ToOneAttributeMapping } @Override - public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( requestedJoinType != null ) { return requestedJoinType; } @@ -2160,11 +2167,11 @@ public class ToOneAttributeMapping public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAliasBase sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, @@ -2174,18 +2181,16 @@ public class ToOneAttributeMapping ); final SoftDeleteMapping softDeleteMapping = getAssociatedEntityMappingType().getSoftDeleteMapping(); - final boolean canUseInnerJoin; - if ( ! lhs.canUseInnerJoins() ) { - canUseInnerJoin = false; - } - else if ( isNullable - || hasNotFoundAction() - || softDeleteMapping != null ) { + final SqlAstJoinType currentlyProcessingJoinType = creationState instanceof SqmToSqlAstConverter + ? ( (SqmToSqlAstConverter) creationState ).getCurrentlyProcessingJoinType() + : null; + if ( currentlyProcessingJoinType != null && currentlyProcessingJoinType != SqlAstJoinType.INNER ) { + // Don't change the join type though, as that has implications for eager initialization of a LazyTableGroup canUseInnerJoin = false; } else { - canUseInnerJoin = requestedJoinType == SqlAstJoinType.INNER; + canUseInnerJoin = determineSqlJoinType( lhs, requestedJoinType, fetched ) == SqlAstJoinType.INNER; } TableGroup realParentTableGroup = lhs; diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index e1fbb2dde7..5e4d62aee5 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -5814,6 +5814,7 @@ public abstract class AbstractEntityPersister } private ModelPart getIdentifierModelPart(String name, EntityMappingType treatTargetType) { + final EntityIdentifierMapping identifierMapping = getIdentifierMappingForJoin(); if ( identifierMapping instanceof NonAggregatedIdentifierMapping ) { NonAggregatedIdentifierMapping mapping = (NonAggregatedIdentifierMapping) identifierMapping; final ModelPart subPart = mapping.findSubPart( name, treatTargetType ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index 5063df66c9..61fda37053 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -1331,6 +1331,15 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { } } + @Override + public EntityIdentifierMapping getIdentifierMappingForJoin() { + // If the joined subclass has a physical discriminator and has subtypes + // we must use the root table identifier mapping for joining to allow table group elimination to work + return isPhysicalDiscriminator() && !getSubMappingTypes().isEmpty() + ? getRootEntityDescriptor().getIdentifierMapping() + : super.getIdentifierMappingForJoin(); + } + private boolean applyDiscriminatorPredicate( TableReferenceJoin join, NamedTableReference tableReference, @@ -1341,9 +1350,11 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { final String discriminatorPredicate = getPrunedDiscriminatorPredicate( entityNameUses, metamodel, - tableReference.getIdentificationVariable() + "t" +// tableReference.getIdentificationVariable() ); - join.applyPredicate( new SqlFragmentPredicate( discriminatorPredicate ) ); + tableReference.setPrunedTableExpression( "(select * from " + getRootTableName() + " t where " + discriminatorPredicate + ")" ); +// join.applyPredicate( new SqlFragmentPredicate( discriminatorPredicate ) ); return true; } return false; diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java index 484f8ee4db..7061d915b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java @@ -63,6 +63,9 @@ import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; import org.hibernate.type.descriptor.java.JavaType; import jakarta.persistence.metamodel.Attribute; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; /** * @author Christian Beikov @@ -341,13 +344,13 @@ public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValued public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -366,18 +369,13 @@ public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValued public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { - return new StandardVirtualTableGroup( - navigablePath, - this, - lhs, - fetched - ); + return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java index 60ff519058..82bda97f79 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java @@ -70,6 +70,8 @@ import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; @@ -245,14 +247,13 @@ public class AnonymousTupleEntityValuedModelPart public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -440,11 +441,11 @@ public class AnonymousTupleEntityValuedModelPart public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAliasBase sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index 8ef0242935..fbbc0e7103 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -41,8 +41,11 @@ import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.Order; @@ -63,6 +66,7 @@ import org.hibernate.query.sqm.tree.SqmDmlStatement; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -101,6 +105,7 @@ import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.Tuple; +import jakarta.persistence.metamodel.Type; import org.checkerframework.checker.nullness.qual.Nullable; import static java.util.stream.Collectors.toList; @@ -188,7 +193,7 @@ public class SqmUtil { // we need to render the target side if in group/order by if ( association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() ) && ( clause == Clause.GROUP || clause == Clause.ORDER - || !isFkOptimizationAllowed( sqmPath.getLhs() ) + || !isFkOptimizationAllowed( sqmPath.getLhs(), association ) || queryPart.getFirstQuerySpec().groupByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) || queryPart.getFirstQuerySpec().orderByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) ) ) { return association.getAssociatedEntityMappingType(); @@ -217,17 +222,106 @@ public class SqmUtil { * a join that cannot be dereferenced through the foreign key on the associated table, * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} * or one that has an explicit on clause predicate. + * + * @deprecated Use {@link #isFkOptimizationAllowed(SqmPath, EntityAssociationMapping)} instead */ + @Deprecated(forRemoval = true, since = "6.6.1") public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { - if ( sqmPath instanceof SqmJoin sqmJoin ) { - return switch ( sqmJoin.getSqmJoinType() ) { - case INNER, LEFT -> sqmJoin.getJoinPredicate() == null; - default -> false; - }; + if ( sqmPath instanceof SqmJoin ) { + final SqmJoin sqmJoin = (SqmJoin) sqmPath; + switch ( sqmJoin.getSqmJoinType() ) { + case LEFT: + final EntityAssociationMapping associationMapping = resolveAssociationMapping( sqmJoin ); + if ( associationMapping != null && isFiltered( associationMapping ) ) { + return false; + } + // FallThrough intended + case INNER: + return sqmJoin.getJoinPredicate() == null; + default: + return false; + } } return false; } + /** + * Utility that returns {@code false} when the provided {@link SqmPath sqmPath} is + * a join that cannot be dereferenced through the foreign key on the associated table, + * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} + * or one that has an explicit on clause predicate. + */ + public static boolean isFkOptimizationAllowed(SqmPath sqmPath, EntityAssociationMapping associationMapping) { + if ( sqmPath instanceof SqmJoin ) { + final SqmJoin sqmJoin = (SqmJoin) sqmPath; + switch ( sqmJoin.getSqmJoinType() ) { + case LEFT: + if ( isFiltered( associationMapping ) ) { + return false; + } + // FallThrough intended + case INNER: + return sqmJoin.getJoinPredicate() == null; + default: + return false; + } + } + return false; + } + + private static boolean isFiltered(EntityAssociationMapping associationMapping) { + final EntityMappingType entityMappingType = associationMapping.getAssociatedEntityMappingType(); + return !associationMapping.isFkOptimizationAllowed() + // When the identifier mappings are different we have a joined subclass entity + // which will filter rows based on a discriminator predicate + || entityMappingType.getIdentifierMappingForJoin() != entityMappingType.getIdentifierMapping(); + } + + private static @Nullable EntityAssociationMapping resolveAssociationMapping(SqmJoin sqmJoin) { + if ( sqmJoin instanceof SqmSingularJoin ) { + final SqmSingularJoin singularJoin = (SqmSingularJoin) sqmJoin; + if ( singularJoin.getAttribute().getSqmPathType() instanceof EntityDomainType ) { + return resolveAssociationMapping( singularJoin ); + } + } + return null; + } + + private static @Nullable EntityAssociationMapping resolveAssociationMapping(SqmSingularJoin sqmJoin) { + SingularPersistentAttribute attribute = sqmJoin.getAttribute(); + ManagedDomainType declaringType = attribute.getDeclaringType(); + if ( declaringType.getPersistenceType() != Type.PersistenceType.ENTITY ) { + final StringBuilder pathBuilder = new StringBuilder(); + do { + if ( pathBuilder.length() > 0 ) { + pathBuilder.insert(0, '.'); + } + pathBuilder.insert( 0, attribute.getName() ); + final SqmFrom lhs = sqmJoin.getLhs(); + if ( !(lhs instanceof SqmSingularJoin ) ) { + return null; + } + sqmJoin = (SqmSingularJoin) lhs; + attribute = sqmJoin.getAttribute(); + declaringType = attribute.getDeclaringType(); + } while (declaringType.getPersistenceType() != Type.PersistenceType.ENTITY ); + pathBuilder.insert(0, '.'); + pathBuilder.insert( 0, attribute.getName() ); + final EntityPersister entityDescriptor = sqmJoin.nodeBuilder() + .getSessionFactory() + .getMappingMetamodel() + .getEntityDescriptor( ( (EntityDomainType) declaringType ).getHibernateEntityName() ); + return (EntityAssociationMapping) entityDescriptor.findByPath( pathBuilder.toString() ); + } + else { + final EntityPersister entityDescriptor = sqmJoin.nodeBuilder() + .getSessionFactory() + .getMappingMetamodel() + .getEntityDescriptor( ( (EntityDomainType) declaringType ).getHibernateEntityName() ); + return (EntityAssociationMapping) entityDescriptor.findAttributeMapping( attribute.getName() ); + } + } + public static List getWhereClauseNavigablePaths(SqmQuerySpec querySpec) { final SqmWhereClause where = querySpec.getWhereClause(); if ( where == null || where.getPredicate() == null ) { 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 9a7f7b0092..4baec07486 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 @@ -6,6 +6,7 @@ */ package org.hibernate.query.sqm.sql; +import jakarta.annotation.Nullable; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.PreparedStatement; @@ -28,7 +29,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; - import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.LockMode; @@ -3922,7 +3922,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base this ); // Implicit joins in the ON clause need to be added as nested table group joins - final boolean nested = currentClauseStack.getCurrent() == Clause.FROM; + final boolean nested = currentlyProcessingJoin != null; if ( nested ) { parentTableGroup.addNestedTableGroupJoin( tableGroupJoin ); } @@ -3996,6 +3996,13 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } + @Override + public @Nullable SqlAstJoinType getCurrentlyProcessingJoinType() { + return currentlyProcessingJoin == null + ? null + : currentlyProcessingJoin.getSqmJoinType().getCorrespondingSqlJoinType(); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SqmPath handling // - Note that SqmFrom references defined in the FROM-clause are already @@ -4125,8 +4132,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base // When the inferred mapping is null, we try to resolve to the FK by default, which is fine because // expansion to all target columns for select and group by clauses is handled in EntityValuedPathInterpretation if ( entityValuedModelPart instanceof EntityAssociationMapping - && ( (EntityAssociationMapping) entityValuedModelPart ).isFkOptimizationAllowed() - && isFkOptimizationAllowed( path ) ) { + && isFkOptimizationAllowed( path, (EntityAssociationMapping) entityValuedModelPart ) ) { // If the table group uses an association mapping that is not a one-to-many, // we make use of the FK model part - unless the path is a non-optimizable join, // for which we should always use the target's identifier to preserve semantics @@ -8401,7 +8407,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base final TableGroupJoinProducer joinProducer = (TableGroupJoinProducer) fetchable; final TableGroup compatibleTableGroup = lhs.findCompatibleJoinedGroup( joinProducer, - joinProducer.determineSqlJoinType( lhs, null, true ) + joinProducer.getDefaultSqlAstJoinType( lhs ) ); final SqmQueryPart queryPart = getCurrentSqmQueryPart(); if ( compatibleTableGroup == null diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java index 20065e26dc..02ffca6028 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java @@ -21,6 +21,7 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmQueryPart; 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; @@ -31,6 +32,8 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.QueryTransformer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import jakarta.annotation.Nullable; + /** * */ @@ -101,6 +104,11 @@ public class FakeSqmToSqlAstConverter extends BaseSemanticQueryWalker implements public void registerQueryTransformer(QueryTransformer transformer) { } + @Override + public @Nullable SqlAstJoinType getCurrentlyProcessingJoinType() { + return null; + } + @Override public boolean isInTypeInference() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java index f5fc0caea3..e932d9a4a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java @@ -18,6 +18,7 @@ import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmQueryPart; +import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.tree.expression.Expression; @@ -38,6 +39,12 @@ public interface SqmToSqlAstConverter extends SemanticQueryWalker, SqlAs void registerQueryTransformer(QueryTransformer transformer); + /** + * Returns the {@link SqlAstJoinType} of the currently processing join if there is one, or {@code null}. + * This is used to determine the join type for implicit joins happening in the {@code ON} clause. + */ + @Nullable SqlAstJoinType getCurrentlyProcessingJoinType(); + /** * Returns whether the state of the translation is currently in type inference mode. * This is useful to avoid type inference based on other incomplete inference information. diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java new file mode 100644 index 0000000000..06567a2c02 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java @@ -0,0 +1,282 @@ +/* + * 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.sql.ast.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.metamodel.mapping.internal.CaseStatementDiscriminatorMappingImpl; +import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.FilterPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.model.ast.ColumnWriteFragment; + +/** + * A simple walker that checks if a predicate contains qualifiers. + * + * @author Christian Beikov + */ +public class TableGroupHelper extends AbstractSqlAstWalker { + + public static final int REAL_TABLE_GROUP_REQUIRED = Integer.MAX_VALUE; + public static final int NO_TABLE_GROUP_REQUIRED = -1; + + private final String primaryQualifier; + private final Map qualifiers; + private final String[] qualifierFragments; + private Integer usedTableReferenceJoinIndex; + + private TableGroupHelper(String primaryQualifier, Map qualifiers) { + this.primaryQualifier = primaryQualifier; + this.qualifiers = qualifiers; + final String[] qualifierFragments = new String[qualifiers.size()]; + for ( Map.Entry entry : qualifiers.entrySet() ) { + qualifierFragments[entry.getValue()] = entry.getKey() + "."; + } + this.qualifierFragments = qualifierFragments; + } + + /** + * Returns the index of a table reference join which can be swapped with the primary table reference + * to avoid rendering a real nested table group. + * {@link #REAL_TABLE_GROUP_REQUIRED} is returned if swapping is not possible. + * {@code #NO_TABLE_GROUP_REQUIRED} is returned if no swapping is necessary. + */ + public static int findReferenceJoinForPredicateSwap(TableGroup tableGroup, Predicate predicate) { + if ( predicate != null && !tableGroup.getTableReferenceJoins().isEmpty() ) { + final TableReference primaryTableReference = tableGroup.getPrimaryTableReference(); + final HashMap qualifiers = CollectionHelper.mapOfSize( tableGroup.getTableReferenceJoins().size() ); + final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); + for ( int i = 0; i < tableReferenceJoins.size(); i++ ) { + final TableReferenceJoin tableReferenceJoin = tableReferenceJoins.get( i ); + if ( !tableGroup.canUseInnerJoins() ) { + if ( !isSimplePredicate( tableGroup, i ) ){//|| isSimpleOrOuterJoin( tableReferenceJoin ) ) { + // Can't do avoid the real table group rendering in this case if it's not inner joined, + // because doing so might change the meaning of the SQL. Consider this example: + // `from tbl1 t1 left join (tbl2 t2 join tbl3 t3 on t2.id=t3.id and ...) on t1.fk=t2.id` + // + // To avoid the nested table group rendering, the join on `tbl3` has to switch to left join + // `from tbl1 t1 left join tbl2 t2 on t1.fk=t2.id left join tbl3 t3 on t2.id=t3.id and ...` + // The additional predicate in the `tbl3` join can make `t3` null even though `t2` is non-null + return REAL_TABLE_GROUP_REQUIRED; + } + } + qualifiers.put( tableReferenceJoin.getJoinedTableReference().getIdentificationVariable(), i ); + } + final TableGroupHelper qualifierCollector = new TableGroupHelper( + primaryTableReference.getIdentificationVariable(), + qualifiers + ); + try { + predicate.accept( qualifierCollector ); + if ( qualifierCollector.usedTableReferenceJoinIndex == null ) { + return NO_TABLE_GROUP_REQUIRED; + } + if ( qualifierCollector.usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED + && !tableGroup.canUseInnerJoins() && !isSimpleTableReference( primaryTableReference ) ) { + // Can't reorder table reference join with primary table reference if the primary table reference + // might filter out elements, since that affects result count with outer joins + return REAL_TABLE_GROUP_REQUIRED; + } + return qualifierCollector.usedTableReferenceJoinIndex; + } + catch (MultipleUsesFoundException ex) { + return REAL_TABLE_GROUP_REQUIRED; + } + } + return NO_TABLE_GROUP_REQUIRED; + } + + private static boolean isSimpleTableReference(TableReference tableReference) { + return tableReference instanceof NamedTableReference && !tableReference.getTableId().startsWith( "(select" ); + } + + /** + * Checks if the table reference join at the given index uses a simple equality join predicate. + * Predicates that contain anything but comparisons of the primary table reference with table reference join columns + * are non-simple. + */ + private static boolean isSimplePredicate(TableGroup tableGroup, int index) { + final TableReference primaryTableReference = tableGroup.getPrimaryTableReference(); + final TableReferenceJoin tableReferenceJoin = tableGroup.getTableReferenceJoins().get( index ); + final NamedTableReference joinedTableReference = tableReferenceJoin.getJoinedTableReference(); + final Predicate predicate = tableReferenceJoin.getPredicate(); + if ( predicate instanceof Junction ) { + final Junction junction = (Junction) predicate; + if ( junction.getNature() == Junction.Nature.CONJUNCTION ) { + for ( Predicate subPredicate : junction.getPredicates() ) { + if ( !isComparison( subPredicate, primaryTableReference, joinedTableReference ) ) { + return false; + } + } + return true; + } + } + else { + return isComparison( predicate, primaryTableReference, joinedTableReference ); + } + return false; + } + + private static boolean isComparison(Predicate predicate, TableReference table1, TableReference table2) { + if ( predicate instanceof ComparisonPredicate ) { + final ComparisonPredicate comparisonPredicate = (ComparisonPredicate) predicate; + final Expression lhs = comparisonPredicate.getLeftHandExpression(); + final Expression rhs = comparisonPredicate.getRightHandExpression(); + final SqlTuple lhsTuple; + if ( lhs instanceof SqlTupleContainer && ( lhsTuple = ( (SqlTupleContainer) lhs ).getSqlTuple() ) != null ) { + final SqlTuple rhsTuple = ( (SqlTupleContainer) rhs ).getSqlTuple(); + final List lhsExpressions = lhsTuple.getExpressions(); + final List rhsExpressions = rhsTuple.getExpressions(); + for ( int i = 0; i < lhsExpressions.size(); i++ ) { + final ColumnReference lhsColumn = lhsExpressions.get( i ).getColumnReference(); + final ColumnReference rhsColumn = rhsExpressions.get( i ).getColumnReference(); + if ( !isComparison( table1, table2, lhsColumn, rhsColumn ) ) { + return false; + } + } + return true; + } + else { + return isComparison( table1, table2, lhs.getColumnReference(), rhs.getColumnReference() ); + } + } + return false; + } + + private static boolean isComparison( + TableReference table1, + TableReference table2, + ColumnReference column1, + ColumnReference column2) { + if ( column1 != null && column2 != null ) { + final String column1Qualifier = column1.getQualifier(); + final String column2Qualifier = column2.getQualifier(); + final String table1Qualifier = table1.getIdentificationVariable(); + final String table2Qualifier = table2.getIdentificationVariable(); + return column1Qualifier.equals( table1Qualifier ) && column2Qualifier.equals( table2Qualifier ) + || column1Qualifier.equals( table2Qualifier ) && column2Qualifier.equals( table1Qualifier ); + } + return false; + } + + private static class MultipleUsesFoundException extends RuntimeException { + public MultipleUsesFoundException() { + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + } + + private void checkQualifier(String qualifier) { + if ( primaryQualifier.equals( qualifier ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = NO_TABLE_GROUP_REQUIRED; + } + else { + final Integer index = qualifiers.get( qualifier ); + if ( index != null ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex.intValue() != index ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = index; + } + } + } + + private void checkSql(String sql) { + if ( sql.contains( primaryQualifier + "." ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = NO_TABLE_GROUP_REQUIRED; + } + else { + for ( int i = 0; i < qualifierFragments.length; i++ ) { + if ( sql.contains( qualifierFragments[i] ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != i ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = i; + } + } + } + } + + @Override + public void visitSelfRenderingExpression(SelfRenderingExpression expression) { + if ( expression instanceof SelfRenderingSqlFragmentExpression ) { + checkSql( ( (SelfRenderingSqlFragmentExpression) expression ).getExpression() ); + } + else if ( expression instanceof CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression ) { + for ( TableReference usedTableReference : ( (CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression) expression ).getUsedTableReferences() ) { + usedTableReference.accept( this ); + } + } + else { + super.visitSelfRenderingExpression( expression ); + } + } + + @Override + public void visitNamedTableReference(NamedTableReference tableReference) { + checkQualifier( tableReference.getIdentificationVariable() ); + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + checkQualifier( columnReference.getQualifier() ); + } + + @Override + public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) { + checkQualifier( aggregateColumnWriteExpression.getAggregateColumnReference().getQualifier() ); + } + + @Override + public void visitFilterPredicate(FilterPredicate filterPredicate) { + for ( FilterPredicate.FilterFragmentPredicate fragment : filterPredicate.getFragments() ) { + visitFilterFragmentPredicate( fragment ); + } + } + + @Override + public void visitFilterFragmentPredicate(FilterPredicate.FilterFragmentPredicate fragmentPredicate) { + checkSql( fragmentPredicate.getSqlFragment() ); + } + + @Override + public void visitSqlFragmentPredicate(SqlFragmentPredicate predicate) { + checkSql( predicate.getSqlFragment() ); + } + + @Override + public void visitColumnWriteFragment(ColumnWriteFragment columnWriteFragment) { + checkSql( columnWriteFragment.getFragment() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index d5577ec1c9..01deee81bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -88,6 +88,7 @@ import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlTreeCreationException; +import org.hibernate.sql.ast.internal.TableGroupHelper; import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; import org.hibernate.sql.ast.tree.MutationStatement; @@ -6034,10 +6035,60 @@ public abstract class AbstractSqlAstTranslator implemen } protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List tableGroupJoinCollector) { - // Without reference joins or nested join groups, even a real table group does not need parenthesis - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( CollectionHelper.isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); + final boolean realTableGroup; + int swappedJoinIndex = -1; + boolean forceLeftJoin = false; + if ( tableGroup.isRealTableGroup() ) { + if ( hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ) { + // If there are nested table groups, we need to render a real table group + realTableGroup = true; + } + else { + // Determine the reference join indexes of the table reference used in the predicate + final int referenceJoinIndexForPredicateSwap = TableGroupHelper.findReferenceJoinForPredicateSwap( + tableGroup, + predicate + ); + if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.REAL_TABLE_GROUP_REQUIRED ) { + // Means that real table group rendering is necessary + realTableGroup = true; + } + else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_REQUIRED ) { + // Means that no swap is necessary to avoid the table group rendering + realTableGroup = false; + forceLeftJoin = !tableGroup.canUseInnerJoins(); + } + else { + // Means that real table group rendering can be avoided if the primary table reference is swapped + // with the table reference join at the given index + realTableGroup = false; + forceLeftJoin = !tableGroup.canUseInnerJoins(); + swappedJoinIndex = referenceJoinIndexForPredicateSwap; + + // Render the table reference of the table reference join first + final TableReferenceJoin tableReferenceJoin = tableGroup.getTableReferenceJoins().get( swappedJoinIndex ); + renderNamedTableReference( tableReferenceJoin.getJoinedTableReference(), LockMode.NONE ); + // along with the predicate for the table group + if ( predicate != null ) { + appendSql( " on " ); + predicate.accept( this ); + } + + // Then render the join syntax and fall through to rendering the primary table reference + appendSql( WHITESPACE ); + if ( tableGroup.canUseInnerJoins() ) { + appendSql( tableReferenceJoin.getJoinType().getText() ); + } + else { + append( "left " ); + } + appendSql( "join " ); + } + } + } + else { + realTableGroup = false; + } if ( realTableGroup ) { appendSql( OPEN_PARENTHESIS ); } @@ -6065,7 +6116,8 @@ public abstract class AbstractSqlAstTranslator implemen tableGroupJoins = null; } - if ( predicate != null ) { + // Predicate was already rendered when swappedJoinIndex is not equal to -1 + if ( predicate != null && swappedJoinIndex == -1 ) { appendSql( " on " ); predicate.accept( this ); } @@ -6083,7 +6135,7 @@ public abstract class AbstractSqlAstTranslator implemen } if ( !realTableGroup ) { - renderTableReferenceJoins( tableGroup ); + renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); processNestedTableGroupJoins( tableGroup, tableGroupJoinCollector ); } if ( tableGroupJoinCollector != null ) { @@ -6362,21 +6414,43 @@ public abstract class AbstractSqlAstTranslator implemen } protected void renderTableReferenceJoins(TableGroup tableGroup) { + renderTableReferenceJoins( tableGroup, -1, false ); + } + + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { final List joins = tableGroup.getTableReferenceJoins(); if ( joins == null || joins.isEmpty() ) { return; } - for ( TableReferenceJoin tableJoin : joins ) { - appendSql( WHITESPACE ); - appendSql( tableJoin.getJoinType().getText() ); - appendSql( "join " ); - - renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); - - if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { + if ( swappedJoinIndex != -1 ) { + // Finish the join against the primary table reference after the swap + final TableReferenceJoin swappedJoin = joins.get( swappedJoinIndex ); + if ( swappedJoin.getPredicate() != null && !swappedJoin.getPredicate().isEmpty() ) { appendSql( " on " ); - tableJoin.getPredicate().accept( this ); + swappedJoin.getPredicate().accept( this ); + } + } + + for ( int i = 0; i < joins.size(); i++ ) { + // Skip the swapped join since it was already rendered + if ( swappedJoinIndex != i ) { + final TableReferenceJoin tableJoin = joins.get( i ); + appendSql( WHITESPACE ); + if ( forceLeftJoin ) { + append( "left " ); + } + else { + appendSql( tableJoin.getJoinType().getText() ); + } + appendSql( "join " ); + + renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + + if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { + appendSql( " on " ); + tableJoin.getPredicate().accept( this ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java index 45b250520c..5d5b06ef84 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java @@ -63,9 +63,18 @@ public class OneToManyTableGroup extends AbstractColumnReferenceQualifier implem } public void registerIndexTableGroup(TableGroupJoin indexTableGroupJoin) { + registerIndexTableGroup( indexTableGroupJoin, true ); + } + + public void registerIndexTableGroup(TableGroupJoin indexTableGroupJoin, boolean nested) { assert this.indexTableGroup == null; this.indexTableGroup = indexTableGroupJoin.getJoinedGroup(); - addNestedTableGroupJoin( indexTableGroupJoin ); + if ( nested ) { + addNestedTableGroupJoin( indexTableGroupJoin ); + } + else { + addTableGroupJoin( indexTableGroupJoin ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java index 80c5a392f0..d2c795232d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java @@ -12,10 +12,11 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAliasBase; 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.tree.predicate.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole */ @@ -41,9 +42,9 @@ public interface TableGroupJoinProducer extends TableGroupProducer { TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState); @@ -70,14 +71,14 @@ public interface TableGroupJoinProducer extends TableGroupProducer { TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState); - default SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + default SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( requestedJoinType != null ) { return requestedJoinType; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java index 369ac57233..e9df32992e 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java @@ -458,7 +458,10 @@ public final class DateTimeUtils { /** * Do the same conversion that databases do when they encounter a timestamp with a higher precision * than what is supported by a column, which is to round the excess fractions. + * + * @deprecated Use {@link #adjustToDefaultPrecision(Temporal, Dialect)} instead */ + @Deprecated(forRemoval = true, since = "6.6.1") public static T roundToDefaultPrecision(T temporal, Dialect d) { final int defaultTimestampPrecision = d.getDefaultTimestampPrecision(); if ( defaultTimestampPrecision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { @@ -472,6 +475,9 @@ public final class DateTimeUtils { } public static T roundToSecondPrecision(T temporal, int precision) { + if ( precision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { + return temporal; + } if ( precision == 0 ) { //noinspection unchecked return temporal.get( ChronoField.NANO_OF_SECOND ) >= 500_000_000L diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java index 2ae3ccc0bc..405b3606b9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java @@ -5,10 +5,8 @@ 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.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -//@Disabled("test produces broken SQL and issue needs to be fixed") @TestForIssue(jiraKey = "HHH-15933") @SessionFactory @DomainModel(annotatedClasses = { Split.class, Reference.class }) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java index c5896e3423..c5ff0c9d37 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java @@ -57,6 +57,33 @@ public class AttributeJoinWithJoinedInheritanceTest { } ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) + public void testLeftJoinSelectFk(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select ce.id " + + "from RootOne r left join r.child ce ", + Integer.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertEquals( 11, resultList.get( 0 ) ); + assertNull( resultList.get( 1 ) ); + } ); + } + @Test public void testLeftJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java new file mode 100644 index 0000000000..08ef6698e0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java @@ -0,0 +1,225 @@ +/* + * 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.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +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.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithNaturalJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithNaturalJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithNaturalJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r left join treat(r.child as SubChildEntityA1) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null, null ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass, + Integer uk) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + if ( uk != null ) { + assertEquals( uk, result.get( 2 ) ); + } + else { + assertNull( result.get( 2 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + @Column(unique = true) + private Integer uk; + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + this.uk = id; + } + + public Integer getUk() { + return uk; + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id", referencedColumnName = "uk") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java new file mode 100644 index 0000000000..4e64027d59 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java @@ -0,0 +1,215 @@ +/* + * 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.inheritance.join; + +import java.util.List; + +import org.hibernate.annotations.SQLRestriction; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +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.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithRestrictedJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithRestrictedJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + @SQLRestriction( "ident < 20" ) + public static class BaseClass { + @Id + @Column(name = "ident") + private Integer id; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java index b2eabc30a6..6b2aaf126d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java @@ -17,7 +17,6 @@ import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.Jpa; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -411,7 +410,6 @@ public class OuterJoinTest { } @Test - @Disabled("Hibernate doesn't support implicit joins") public void testJoinOrderWithRightNormalJoinWithInnerImplicitJoins(EntityManagerFactoryScope scope) { scope.inTransaction( em -> { List resultList = em.createQuery( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java index 985f9b0e9e..18fe576ca7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java @@ -35,7 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; ) public class LocalTimeTest { - private static final LocalTime LOCAL_TIME = DateTimeUtils.roundToDefaultPrecision( + private static final LocalTime LOCAL_TIME = DateTimeUtils.adjustToDefaultPrecision( LocalTime.now(), DialectContext.getDialect() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java index 9f79f5066e..eb6b87b3ab 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java @@ -8,7 +8,6 @@ package org.hibernate.orm.test.jpa.ql; import java.util.Set; -import org.hibernate.testing.TestForIssue; import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; @@ -21,19 +20,28 @@ import jakarta.persistence.CollectionTable; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; -@TestForIssue(jiraKey = "HHH-16691") @DomainModel( annotatedClasses = { - JoinTableOptimizationTest.Document.class, JoinTableOptimizationTest.Person.class + JoinTableOptimizationTest.Document.class, + JoinTableOptimizationTest.Person.class, + JoinTableOptimizationTest.File.class, + JoinTableOptimizationTest.Picture.class }) @SessionFactory(useCollectingStatementInspector = true) public class JoinTableOptimizationTest { @Test + @JiraKey("HHH-16691") public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -48,6 +56,7 @@ public class JoinTableOptimizationTest { } @Test + @JiraKey("HHH-16691") public void testInnerJoin(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -68,6 +77,7 @@ public class JoinTableOptimizationTest { } @Test + @JiraKey("HHH-16691") public void testLeftJoin(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -88,6 +98,7 @@ public class JoinTableOptimizationTest { } @Test + @JiraKey("HHH-16691") public void testInnerJoinCustomOnClause(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -108,6 +119,7 @@ public class JoinTableOptimizationTest { } @Test + @JiraKey("HHH-16691") public void testLeftJoinCustomOnClause(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -147,6 +159,199 @@ public class JoinTableOptimizationTest { ); } + @Test + @JiraKey("HHH-17646") + public void testLeftJoinFetch(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select d from Document d left join fetch d.people" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select d1_0.id,d1_0.name,p1_0.Document_id,p1_1.id,p1_1.name " + + "from Document d1_0 " + + "left join people p1_0 on d1_0.id=p1_0.Document_id " + + "left join Person p1_1 on p1_1.id=p1_0.people_id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.manyFiles f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mf1_1.id,case when mf1_2.id is not null then 1 when mf1_1.id is not null then 0 end,mf1_1.document_id,mf1_1.name,mf1_2.extension " + + "from Document d1_0 " + + "join many_files mf1_0 on d1_0.id=mf1_0.Document_id " + + "join file_tbl mf1_1 on mf1_1.id=mf1_0.manyFiles_id " + + "left join Picture mf1_2 on mf1_1.id=mf1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.manyFiles f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mf1_1.id,case when mf1_2.id is not null then 1 when mf1_1.id is not null then 0 end,mf1_1.document_id,mf1_1.name,mf1_2.extension " + + "from Document d1_0 " + + "left join many_files mf1_0 on d1_0.id=mf1_0.Document_id " + + "left join file_tbl mf1_1 on mf1_1.id=mf1_0.manyFiles_id " + + "left join Picture mf1_2 on mf1_1.id=mf1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.files f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select f1_0.id,case when f1_1.id is not null then 1 when f1_0.id is not null then 0 end,d1_0.id,d1_0.name,f1_0.name,f1_1.extension " + + "from Document d1_0 " + + "join file_tbl f1_0 on d1_0.id=f1_0.document_id " + + "left join Picture f1_1 on f1_0.id=f1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.files f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select f1_0.id,case when f1_1.id is not null then 1 when f1_0.id is not null then 0 end,d1_0.id,d1_0.name,f1_0.name,f1_1.extension " + + "from Document d1_0 " + + "left join file_tbl f1_0 on d1_0.id=f1_0.document_id " + + "left join Picture f1_1 on f1_0.id=f1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicSubtypeJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.manyPictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mp1_1.id,mp1_2.document_id,mp1_2.name,mp1_1.extension " + + "from Document d1_0 " + + "join many_pictures mp1_0 on d1_0.id=mp1_0.Document_id " + + "join Picture mp1_1 on mp1_1.id=mp1_0.manyPictures_id " + + "join file_tbl mp1_2 on mp1_1.id=mp1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicSubtypeLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.manyPictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mp1_1.id,mp1_2.document_id,mp1_2.name,mp1_1.extension " + + "from Document d1_0 " + + "left join many_pictures mp1_0 on d1_0.id=mp1_0.Document_id " + + "left join Picture mp1_1 on mp1_1.id=mp1_0.manyPictures_id " + + "left join file_tbl mp1_2 on mp1_1.id=mp1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicSubtypeJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.pictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select p1_0.id,p1_1.document_id,p1_1.name,p1_0.extension " + + "from Document d1_0 " + + "join file_tbl p1_1 on d1_0.id=p1_1.document_id " + + "join Picture p1_0 on p1_0.id=p1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicSubtypeLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.pictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select p1_0.id,p1_1.document_id,p1_1.name,p1_0.extension " + + "from Document d1_0 " + + "left join file_tbl p1_1 on d1_0.id=p1_1.document_id " + + "left join Picture p1_0 on p1_0.id=p1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + @Entity(name = "Document") public static class Document { @Id @@ -158,6 +363,16 @@ public class JoinTableOptimizationTest { @ElementCollection @CollectionTable(name = "document_pages") Set pages; + @OneToMany(mappedBy = "document") + Set files; + @ManyToMany + @JoinTable(name = "many_files") + Set manyFiles; + @OneToMany(mappedBy = "document") + Set pictures; + @ManyToMany + @JoinTable(name = "many_pictures") + Set manyPictures; } @Entity(name = "Person") public static class Person { @@ -165,6 +380,20 @@ public class JoinTableOptimizationTest { Long id; String name; } + @Entity(name = "File") + @Table(name = "file_tbl") + @Inheritance(strategy = InheritanceType.JOINED) + public static class File { + @Id + Long id; + String name; + @ManyToOne(fetch = FetchType.LAZY) + Document document; + } + @Entity(name = "Picture") + public static class Picture extends File { + String extension; + } @Embeddable public static class Page { String text; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java index a607a69656..983c7df49e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java @@ -44,7 +44,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; /** * Tests for "direct" JDBC handling of {@linkplain java.time Java Time} types. @@ -84,7 +84,7 @@ public class GlobalJavaTimeJdbcTypeTests { @Test void testInstant(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final Instant start = roundToDefaultPrecision( Instant.EPOCH, dialect ); + final Instant start = adjustToDefaultPrecision( Instant.EPOCH, dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -114,7 +114,7 @@ public class GlobalJavaTimeJdbcTypeTests { @Test void testLocalDateTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDateTime start = roundToDefaultPrecision( LocalDateTime.now(), dialect ); + final LocalDateTime start = adjustToDefaultPrecision( LocalDateTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -144,7 +144,7 @@ public class GlobalJavaTimeJdbcTypeTests { @Test void testLocalDate(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDate startTime = roundToDefaultPrecision( LocalDate.now(), dialect ); + final LocalDate startTime = adjustToDefaultPrecision( LocalDate.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -177,7 +177,7 @@ public class GlobalJavaTimeJdbcTypeTests { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase drivers truncate fractional seconds from the LocalTime") void testLocalTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalTime startTime = roundToDefaultPrecision( LocalTime.now(), dialect ); + final LocalTime startTime = adjustToDefaultPrecision( LocalTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java index 20ec4b3139..8bc7c408fa 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java @@ -45,7 +45,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; /** * Tests for "direct" JDBC handling of {@linkplain java.time Java Time} types. @@ -85,7 +85,7 @@ public class JavaTimeJdbcTypeTests { @Test void testInstant(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final Instant start = roundToDefaultPrecision( Instant.EPOCH, dialect ); + final Instant start = adjustToDefaultPrecision( Instant.EPOCH, dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -115,7 +115,7 @@ public class JavaTimeJdbcTypeTests { @Test void testLocalDateTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDateTime start = roundToDefaultPrecision( LocalDateTime.now(), dialect ); + final LocalDateTime start = adjustToDefaultPrecision( LocalDateTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -145,7 +145,7 @@ public class JavaTimeJdbcTypeTests { @Test void testLocalDate(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDate startTime = roundToDefaultPrecision( LocalDate.now(), dialect ); + final LocalDate startTime = adjustToDefaultPrecision( LocalDate.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -178,7 +178,7 @@ public class JavaTimeJdbcTypeTests { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase drivers truncate fractional seconds from the LocalTime") void testLocalTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalTime startTime = roundToDefaultPrecision( LocalTime.now(), dialect ); + final LocalTime startTime = adjustToDefaultPrecision( LocalTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java index a2367e71ff..280c81a6cb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java @@ -111,6 +111,23 @@ public class ToOneTests { } ); } + @Test + void fkAccessTest(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final Integer issue2Reporter = session.createQuery( "select i.reporter.id from Issue i where i.id = 2", Integer.class ).getSingleResultOrNull(); + assertThat( issue2Reporter ).isNull(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".reporter_fk" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsAnyOf( ".active='Y'", ".active=N'Y'" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsOnlyOnce( "active" ); + } ); + } + @Entity(name="Issue") @Table(name="issues") public static class Issue { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java index a37c96fb13..73dbf9f97d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java @@ -86,10 +86,10 @@ public class UTCNormalizedInstantTest { }); scope.inSession( s-> { final Zoned z = s.find(Zoned.class, id); - Instant expected = DateTimeUtils.adjustToDefaultPrecision( z.utcInstant, dialect ); + Instant expected = DateTimeUtils.adjustToDefaultPrecision( z.utcInstant, dialect ); Instant actual = DateTimeUtils.adjustToDefaultPrecision( instant, dialect ); assertEquals( - expected, + expected, actual ); expected = DateTimeUtils.adjustToDefaultPrecision( z.localInstant, dialect );