From 054c34601397f652fb1eca69c7ca14424d82e5aa Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 14 Aug 2025 16:57:58 +0200 Subject: [PATCH 1/2] HHH-19712 Reproduce faulty native query alias expansion with column selection deduplication --- ...yResultBuilderColumnDeduplicationTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java new file mode 100644 index 000000000000..0e327fbc9f22 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +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.Test; + +@DomainModel(annotatedClasses = { + NativeQueryResultBuilderColumnDeduplicationTest.MyEntity.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19712") +public class NativeQueryResultBuilderColumnDeduplicationTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createNativeQuery( "select {t.*} from MyEntity t", Object.class ) + .addEntity( "t", MyEntity.class ) + .getResultList(); + } + ); + } + + @Entity(name = "MyEntity") + public static class MyEntity { + @EmbeddedId + private MyEntityPk id; + @Column(insertable = false, updatable = false) + private String name; + private String description; + } + + @Embeddable + public static class MyEntityPk { + private String id; + private String name; + } +} From 485a4285de8e73dee9b9c6c8ac0978b42a09186c Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 14 Aug 2025 16:58:53 +0200 Subject: [PATCH 2/2] HHH-19712 Rework select fragment generation to work with column selection deduplication --- .../entity/AbstractEntityPersister.java | 162 ++++++++++-------- 1 file changed, 87 insertions(+), 75 deletions(-) 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 abef1ac61a24..d23fec16e188 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 @@ -1999,61 +1999,62 @@ public String selectFragment(String alias, String suffix) { // Wrap expressions with aliases final SelectClause selectClause = rootQuerySpec.getSelectClause(); final List sqlSelections = selectClause.getSqlSelections(); + final Set processedExpressions = new HashSet<>( sqlSelections.size() ); int i = 0; - int columnIndex = 0; - final String[] columnAliases = getSubclassColumnAliasClosure(); - final int columnAliasesSize = columnAliases.length; - for ( String identifierAlias : identifierAliases ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), identifierAlias + suffix ) - ) - ); - if ( i < columnAliasesSize && columnAliases[i].equals( identifierAlias ) ) { - columnIndex++; + final int identifierSelectionSize = identifierMapping.getJdbcTypeCount(); + for ( int j = 0; j < identifierSelectionSize; j++ ) { + final SelectableMapping selectableMapping = identifierMapping.getSelectable( j ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, identifierAliases[j] + suffix ); + i++; } - i++; } - if ( entityMetamodel.hasSubclasses() ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), getDiscriminatorAlias() + suffix ) - ) - ); - i++; + if ( hasSubclasses() ) { + assert discriminatorMapping.getJdbcTypeCount() == 1; + final SelectableMapping selectableMapping = discriminatorMapping.getSelectable( 0 ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, getDiscriminatorAlias() + suffix ); + i++; + } } if ( hasRowId() ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), ROWID_ALIAS + suffix ) - ) - ); - i++; + final SelectableMapping selectableMapping = rowIdMapping; + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, ROWID_ALIAS + suffix ); + i++; + } } + final String[] columnAliases = getSubclassColumnAliasClosure(); final String[] formulaAliases = getSubclassFormulaAliasClosure(); + int columnIndex = 0; int formulaIndex = 0; - for ( ; i < sqlSelections.size(); i++ ) { - final SqlSelection sqlSelection = sqlSelections.get( i ); - final ColumnReference columnReference = (ColumnReference) sqlSelection.getExpression(); - final String selectAlias = !columnReference.isColumnExpressionFormula() - ? columnAliases[columnIndex++] + suffix - : formulaAliases[formulaIndex++] + suffix; - sqlSelections.set( - i, - new SqlSelectionImpl( - sqlSelection.getValuesArrayPosition(), - new AliasedExpression( sqlSelection.getExpression(), selectAlias ) - ) - ); + final int size = getNumberOfFetchables(); + // getSubclassColumnAliasClosure contains the _identifierMapper columns when it has an id class, + // which need to be skipped + if ( identifierMapping instanceof NonAggregatedIdentifierMapping + && ( (NonAggregatedIdentifierMapping) identifierMapping ).getIdClassEmbeddable() != null ) { + columnIndex = identifierSelectionSize; + } + for ( int j = 0; j < size; j++ ) { + final AttributeMapping fetchable = getFetchable( j ); + if ( !(fetchable instanceof PluralAttributeMapping) + && !skipFetchable( fetchable, fetchable.getMappedFetchOptions().getTiming() ) + && fetchable.isSelectable() ) { + final int jdbcTypeCount = fetchable.getJdbcTypeCount(); + for ( int k = 0; k < jdbcTypeCount; k++ ) { + final SelectableMapping selectableMapping = fetchable.getSelectable( k ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + final String baseAlias = selectableMapping.isFormula() + ? formulaAliases[formulaIndex++] + : columnAliases[columnIndex++]; + aliasSelection( sqlSelections, i, baseAlias + suffix ); + i++; + } + } + } } final String sql = getFactory().getJdbcServices() @@ -2073,6 +2074,17 @@ public String selectFragment(String alias, String suffix) { return expression; } + private static void aliasSelection( + List sqlSelections, + int selectionIndex, + String alias) { + final Expression expression = sqlSelections.get( selectionIndex ).getExpression(); + sqlSelections.set( + selectionIndex, + new SqlSelectionImpl( selectionIndex, new AliasedExpression( expression, alias ) ) + ); + } + private ImmutableFetchList fetchProcessor(FetchParent fetchParent, LoaderSqlAstCreationState creationState) { final FetchableContainer fetchableContainer = fetchParent.getReferencedMappingContainer(); final int size = fetchableContainer.getNumberOfFetchables(); @@ -2083,45 +2095,45 @@ private ImmutableFetchList fetchProcessor(FetchParent fetchParent, LoaderSqlAstC // Ignore plural attributes if ( !( fetchable instanceof PluralAttributeMapping ) ) { final FetchTiming fetchTiming = fetchable.getMappedFetchOptions().getTiming(); - if ( fetchable.asBasicValuedModelPart() != null ) { - // Ignore lazy basic columns - if ( fetchTiming == FetchTiming.DELAYED ) { - continue; + if ( !skipFetchable( fetchable, fetchTiming ) ) { + if ( fetchTiming == null ) { + throw new AssertionFailure( "fetchTiming was null" ); } - } - else if ( fetchable instanceof Association ) { - final Association association = (Association) fetchable; - // Ignore the fetchable if the FK is on the other side - if ( association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) { - continue; - } - // Ensure the FK comes from the root table - if ( !getRootTableName().equals( association.getForeignKeyDescriptor().getKeyTable() ) ) { - continue; + if ( fetchable.isSelectable() ) { + final Fetch fetch = fetchParent.generateFetchableFetch( + fetchable, + fetchParent.resolveNavigablePath( fetchable ), + fetchTiming, + false, + null, + creationState + ); + fetches.add( fetch ); } } - - if ( fetchTiming == null ) { - throw new AssertionFailure("fetchTiming was null"); - } - - if ( fetchable.isSelectable() ) { - final Fetch fetch = fetchParent.generateFetchableFetch( - fetchable, - fetchParent.resolveNavigablePath( fetchable ), - fetchTiming, - false, - null, - creationState - ); - fetches.add( fetch ); - } } } return fetches.build(); } + private boolean skipFetchable(Fetchable fetchable, FetchTiming fetchTiming) { + if ( fetchable.asBasicValuedModelPart() != null ) { + // Ignore lazy basic columns + return fetchTiming == FetchTiming.DELAYED; + } + else if ( fetchable instanceof Association ) { + final Association association = (Association) fetchable; + // Ignore the fetchable if the FK is on the other side + return association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET + // Ensure the FK comes from the root table + || !getRootTableName().equals( association.getForeignKeyDescriptor().getKeyTable() ); + } + else { + return false; + } + } + /** * @deprecated use {@link Fetchable#isSelectable()} instead. */ @@ -6354,7 +6366,7 @@ public Fetchable getKeyFetchable(int position) { } @Override - public Fetchable getFetchable(int position) { + public AttributeMapping getFetchable(int position) { return getStaticFetchableList().get( position ); }