From 692f19c83fef4339112e4fbabcc09437999a1bdf Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Wed, 4 Mar 2020 12:06:59 -0600 Subject: [PATCH] HHH-13619 - Support for JPA's `size` function as a select expression - initial support --- hibernate-core/src/main/antlr/hql-sql.g | 25 +- hibernate-core/src/main/antlr/hql.g | 23 ++ hibernate-core/src/main/antlr/sql-gen.g | 7 + .../hibernate/hql/internal/ast/HqlParser.java | 1 + .../hql/internal/ast/HqlSqlWalker.java | 15 ++ .../hql/internal/ast/SqlGenerator.java | 18 ++ .../hql/internal/ast/exec/DeleteExecutor.java | 1 + .../internal/ast/tree/CollectionPathNode.java | 248 ++++++++++++++++++ .../internal/ast/tree/CollectionSizeNode.java | 207 +++++++++++++++ .../hql/internal/ast/tree/ComponentJoin.java | 3 + .../hql/internal/ast/tree/FromElement.java | 4 + .../internal/ast/tree/ImpliedFromElement.java | 15 ++ .../hql/internal/ast/tree/MethodNode.java | 4 +- .../ComponentInWhereClauseTest.java | 13 + .../java/org/hibernate/test/hql/HQLTest.java | 13 +- .../test/hql/size/ManyToManySizeTest.java | 44 +++- 16 files changed, 630 insertions(+), 11 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java create mode 100644 hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java diff --git a/hibernate-core/src/main/antlr/hql-sql.g b/hibernate-core/src/main/antlr/hql-sql.g index 96c40bbbea..17a9d8ece3 100644 --- a/hibernate-core/src/main/antlr/hql-sql.g +++ b/hibernate-core/src/main/antlr/hql-sql.g @@ -244,6 +244,14 @@ tokens protected void handleResultVariableRef(AST resultVariableRef) throws SemanticException { } + protected AST createCollectionSizeFunction(AST collectionPath, boolean inSelect) throws SemanticException { + throw new UnsupportedOperationException( "Walker should implement" ); + } + + protected AST createCollectionPath(AST qualifier, AST reference) throws SemanticException { + throw new UnsupportedOperationException( "Walker should implement" ); + } + protected AST lookupProperty(AST dot,boolean root,boolean inSelect) throws SemanticException { return dot; } @@ -683,7 +691,10 @@ collectionFunction ; functionCall - : #(METHOD_CALL {inFunctionCall=true;} pathAsIdent ( #(EXPR_LIST (exprOrSubquery [ null ])* ) )? ) { + : #( COLL_SIZE path:collectionPath ) { + #functionCall = createCollectionSizeFunction( #path, inSelect ); + } + | #(METHOD_CALL {inFunctionCall=true;} pathAsIdent ( #(EXPR_LIST (exprOrSubquery [ null ])* ) )? ) { processFunction( #functionCall, inSelect ); inFunctionCall=false; } @@ -694,6 +705,18 @@ functionCall | #(AGGREGATE aggregateExpr ) ; +collectionPath! +// for now we do not support nested path refs. + : #( COLL_PATH ref:identifier (qualifier:collectionPathQualifier)? ) { + resolve( #qualifier ); + #collectionPath = createCollectionPath( #qualifier, #ref ); + } + ; + +collectionPathQualifier + : addrExpr [true] + ; + constant : literal | NULL diff --git a/hibernate-core/src/main/antlr/hql.g b/hibernate-core/src/main/antlr/hql.g index ac9e2a4ac3..31d75fe683 100644 --- a/hibernate-core/src/main/antlr/hql.g +++ b/hibernate-core/src/main/antlr/hql.g @@ -109,6 +109,7 @@ tokens CONSTRUCTOR; CASE2; // a "simple case statement", whereas CASE represents a "searched case statement" CAST; + COLL_PATH; EXPR_LIST; FILTER_ENTITY; // FROM element injected because of a filter expression (happens during compilation phase 2) IN_LIST; @@ -124,6 +125,7 @@ tokens RANGE; ROW_STAR; SELECT_FROM; + COLL_SIZE; UNARY_MINUS; UNARY_PLUS; VECTOR_EXPR; // ( x, y, z ) @@ -723,6 +725,7 @@ atom primaryExpression : { validateSoftKeyword("function") && LA(2) == OPEN && LA(3) == QUOTED_STRING }? jpaFunctionSyntax | { validateSoftKeyword("cast") && LA(2) == OPEN }? castFunction + | { validateSoftKeyword("size") && LA(2) == OPEN }? collectionSizeFunction | identPrimary ( options {greedy=true;} : DOT^ "class" )? | constant | parameter @@ -762,6 +765,26 @@ castTargetType : identifier { handleDotIdent(); } ( options { greedy=true; } : DOT^ identifier )* ; +collectionSizeFunction! + : s:IDENT OPEN p:collectionPath CLOSE { + assert #s.getText().equalsIgnoreCase( "size" ); + #collectionSizeFunction = #( [COLL_SIZE], #p ); + } + ; + +collectionPath! +// for now we do not support nested path refs (for embeddables) + : simpleRef:identifier { + #collectionPath = #( [COLL_PATH], #simpleRef ); + } + | qualifier:collectionPathQualifier DOT propertyName:identifier { + #collectionPath = #( [COLL_PATH], #propertyName, #qualifier ); + }; + +collectionPathQualifier + : identifier ( DOT^ { weakKeywords(); } identifier )* + ; + parameter : COLON^ { expectNamedParameterName(); } IDENT | PARAM^ (NUM_INT)? diff --git a/hibernate-core/src/main/antlr/sql-gen.g b/hibernate-core/src/main/antlr/sql-gen.g index 40d48f0343..c8eeda17f6 100644 --- a/hibernate-core/src/main/antlr/sql-gen.g +++ b/hibernate-core/src/main/antlr/sql-gen.g @@ -127,6 +127,10 @@ options { protected String renderOrderByElement(String expression, String order, String nulls) { throw new UnsupportedOperationException("Concrete SQL generator should override this method."); } + + protected void renderCollectionSize(AST collectionSizeNode) { + throw new UnsupportedOperationException( "Concrete SQL generator should override this method." ); + } } statement @@ -478,6 +482,9 @@ methodCall ( #(EXPR_LIST (arguments)? ) )? { endFunctionTemplate(m); } ) | #( c:CAST { beginFunctionTemplate(c,c); } castExpression {betweenFunctionArguments();} castTargetType { endFunctionTemplate(c); } ) + | cs:COLL_SIZE { + renderCollectionSize( #cs ); + } ; arguments diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java index 80130d8781..7c3fd925f7 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlParser.java @@ -109,6 +109,7 @@ public final class HqlParser extends HqlBaseParser { return parseErrorHandler; } + /** * Overrides the base behavior to retry keywords as identifiers. * diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java index d2e2c56dff..f1a0001e54 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/HqlSqlWalker.java @@ -34,6 +34,8 @@ import org.hibernate.hql.internal.ast.tree.AggregateNode; import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; import org.hibernate.hql.internal.ast.tree.CastFunctionNode; import org.hibernate.hql.internal.ast.tree.CollectionFunction; +import org.hibernate.hql.internal.ast.tree.CollectionPathNode; +import org.hibernate.hql.internal.ast.tree.CollectionSizeNode; import org.hibernate.hql.internal.ast.tree.ConstructorNode; import org.hibernate.hql.internal.ast.tree.DeleteStatement; import org.hibernate.hql.internal.ast.tree.DotNode; @@ -82,6 +84,7 @@ import org.hibernate.param.VersionTypeSeedParameterSpecification; import org.hibernate.persister.collection.CollectionPropertyNames; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.PropertyMapping; import org.hibernate.persister.entity.Queryable; import org.hibernate.sql.JoinType; import org.hibernate.type.AssociationType; @@ -639,6 +642,13 @@ public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, Par return impliedJoinType; } + @Override + protected AST createCollectionSizeFunction(AST collectionPath, boolean inSelect) throws SemanticException { + assert collectionPath instanceof CollectionPathNode; + + return new CollectionSizeNode( (CollectionPathNode) collectionPath, this ); + } + @Override protected AST lookupProperty(AST dot, boolean root, boolean inSelect) throws SemanticException { DotNode dotNode = (DotNode) dot; @@ -1221,6 +1231,11 @@ public class HqlSqlWalker extends HqlSqlBaseWalker implements ErrorReporter, Par indexNode.resolve( true, true ); } + @Override + protected AST createCollectionPath(AST qualifier, AST reference) throws SemanticException { + return CollectionPathNode.from( qualifier, reference, this ); + } + @Override protected void processFunction(AST functionCall, boolean inSelect) throws SemanticException { MethodNode methodNode = (MethodNode) functionCall; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java index 4db85674f0..1582651bcd 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/SqlGenerator.java @@ -17,6 +17,8 @@ import org.hibernate.dialect.function.SQLFunction; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.hql.internal.antlr.SqlGeneratorBase; import org.hibernate.hql.internal.antlr.SqlTokenTypes; +import org.hibernate.hql.internal.ast.tree.CollectionPathNode; +import org.hibernate.hql.internal.ast.tree.CollectionSizeNode; import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.hql.internal.ast.tree.FunctionNode; import org.hibernate.hql.internal.ast.tree.Node; @@ -32,6 +34,7 @@ import org.hibernate.param.ParameterSpecification; import org.hibernate.type.Type; import antlr.RecognitionException; +import antlr.SemanticException; import antlr.collections.AST; /** @@ -426,4 +429,19 @@ public class SqlGenerator extends SqlGeneratorBase implements ErrorReporter { ); return sessionFactory.getDialect().renderOrderByElement( expression, null, order, nullPrecedence ); } + + @Override + protected void renderCollectionSize(AST ast) { + assert ast instanceof CollectionSizeNode; + + final CollectionSizeNode collectionSizeNode = (CollectionSizeNode) ast; + + // todo : or `#getStringBuilder()` directly? + try { + writer.clause( collectionSizeNode.toSqlExpression() ); + } + catch (SemanticException e) { + throw new QueryException( "Unable to render collection-size node" ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java index ef16c1e41f..9226f6ba65 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/DeleteExecutor.java @@ -19,6 +19,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.hql.internal.ast.HqlSqlWalker; import org.hibernate.hql.internal.ast.SqlGenerator; import org.hibernate.hql.internal.ast.tree.DeleteStatement; +import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.metamodel.spi.MetamodelImplementor; import org.hibernate.param.ParameterSpecification; import org.hibernate.persister.collection.AbstractCollectionPersister; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java new file mode 100644 index 0000000000..9d8e2c1ffa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionPathNode.java @@ -0,0 +1,248 @@ +/* + * 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.hql.internal.ast.tree; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.SqlASTFactory; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.collection.QueryableCollection; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.persister.entity.PropertyMapping; +import org.hibernate.type.CollectionType; +import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; +import org.hibernate.type.Type; + +import antlr.SemanticException; +import antlr.collections.AST; + +/** + * @author Steve Ebersole + */ +public class CollectionPathNode extends SqlNode { + /** + * Used to resolve the collection "owner key" columns + */ + private final FromElement ownerFromElement; + + private final CollectionPersister collectionDescriptor; + + private final String collectionPropertyName; + private final String collectionPropertyPath; + private final String collectionQueryPath; + + + + /** + * Instantiate a `CollectionPathNode` + * + * @see #from(AST, AST, HqlSqlWalker) + */ + public CollectionPathNode( + FromElement ownerFromElement, + CollectionPersister collectionDescriptor, + String collectionPropertyName, + String collectionQueryPath, + String collectionPropertyPath) { + this.ownerFromElement = ownerFromElement; + this.collectionDescriptor = collectionDescriptor; + this.collectionPropertyName = collectionPropertyName; + this.collectionQueryPath = collectionQueryPath; + this.collectionPropertyPath = collectionPropertyPath; + + super.setType( SqlASTFactory.COLL_PATH ); + super.setDataType( collectionDescriptor.getCollectionType() ); + super.setText( collectionDescriptor.getRole() ); + } + + /** + * Factory for `CollectionPathNode` instances + * + * @param qualifier The left-hand-side of a dot-ident node - may be null to indicate an ident arg + * @param reference The right-hand-side of the dot-ident or the ident that is an unqualified reference + */ + public static CollectionPathNode from( + AST qualifier, + AST reference, + HqlSqlWalker walker) { + + final String referenceName = reference.getText(); + final String qualifierQueryPath = qualifier == null + ? "" + : ( (FromReferenceNode) qualifier ).getPath(); + final String referencePath = qualifier == null + ? referenceName + : qualifierQueryPath + "." + reference; + + if ( qualifier == null ) { + // If there is no qualifier it means the argument to `size()` was a simple IDENT node as opposed to a DOT-IDENT + // node. In this case, `reference` could technically be a join alias. This is not JPA + // compliant, but is a Hibernate-specific extension + + // size( cu ) + + final FromElement byAlias = walker.getCurrentFromClause().getFromElement( referenceName ); + + if ( byAlias != null ) { + final FromElement ownerRef = byAlias.getOrigin(); + final QueryableCollection collectionDescriptor = byAlias.getQueryableCollection(); + + return new CollectionPathNode( + ownerRef, + collectionDescriptor, + referenceName, + referencePath, + referenceName + ); + } + else { + // we (should) have an unqualified plural-attribute name - look through all of the defined from-elements + // and look for one that exposes that property + + //noinspection unchecked + final List fromElements = walker.getCurrentFromClause().getExplicitFromElements(); + + if ( fromElements.size() == 1 ) { + final FromElement ownerRef = fromElements.get( 0 ); + + final PropertyMapping collectionPropertyMapping = ownerRef.getPropertyMapping( referenceName ); + + //noinspection RedundantClassCall + if ( ! CollectionType.class.isInstance( collectionPropertyMapping.getType() ) ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + final CollectionType collectionType = (CollectionType) collectionPropertyMapping.getType(); + + return new CollectionPathNode( + ownerRef, + walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), + referenceName, + referencePath, + referenceName + ); + } + else { + FromElement discoveredQualifier = null; + + //noinspection ForLoopReplaceableByForEach + for ( int i = 0; i < fromElements.size(); i++ ) { + final FromElement fromElement = fromElements.get( i ); + try { + final PropertyMapping propertyMapping = fromElement.getPropertyMapping( referenceName ); + //noinspection RedundantClassCall + if ( ! CollectionType.class.isInstance( propertyMapping.getType() ) ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + discoveredQualifier = fromElement; + + break; + } + catch (Exception e) { + // try the next + } + } + + if ( discoveredQualifier == null ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + final FromElement ownerRef = discoveredQualifier; + + final PropertyMapping collectionPropertyMapping = ownerRef.getPropertyMapping( referenceName ); + + //noinspection RedundantClassCall + if ( ! CollectionType.class.isInstance( collectionPropertyMapping.getType() ) ) { + throw new QueryException( "Could not resolve identifier `" + referenceName + "` as plural-attribute" ); + } + + final CollectionType collectionType = (CollectionType) collectionPropertyMapping.getType(); + + return new CollectionPathNode( + ownerRef, + walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), + referenceName, + referencePath, + referenceName + ); + } + } + } + else { + // we have a dot-ident structure + final FromReferenceNode qualifierFromReferenceNode = (FromReferenceNode) qualifier; + try { + qualifierFromReferenceNode.resolve( false, false ); + } + catch (SemanticException e) { + throw new QueryException( "Unable to resolve collection-path qualifier : " + qualifier.getText(), e ); + } + + final Type qualifierType = qualifierFromReferenceNode.getDataType(); + final FromElement ownerRef = ( (FromReferenceNode) qualifier ).getFromElement(); + + final CollectionType collectionType; + final String mappedPath; + + if ( qualifierType instanceof CompositeType ) { + final CompositeType qualifierCompositeType = (CompositeType) qualifierType; + final int collectionPropertyIndex = (qualifierCompositeType).getPropertyIndex( referenceName ); + collectionType = (CollectionType) qualifierCompositeType.getSubtypes()[collectionPropertyIndex]; + + if ( ownerRef instanceof ComponentJoin ) { + mappedPath = ( (ComponentJoin) ownerRef ).getComponentPath() + "." + referenceName; + } + else { + mappedPath = qualifierQueryPath.substring( qualifierQueryPath.indexOf( "." ) + 1 ); + } + } + else if ( qualifierType instanceof EntityType ) { + final EntityType qualifierEntityType = (EntityType) qualifierType; + final String entityName = qualifierEntityType.getAssociatedEntityName(); + final EntityPersister entityPersister = walker.getSessionFactoryHelper().findEntityPersisterByName( entityName ); + final int propertyIndex = entityPersister.getEntityMetamodel().getPropertyIndex( referenceName ); + collectionType = (CollectionType) entityPersister.getPropertyTypes()[ propertyIndex ]; + mappedPath = referenceName; + } + else { + throw new QueryException( "Unexpected collection-path reference qualifier type : " + qualifier ); + } + + return new CollectionPathNode( + ( (FromReferenceNode) qualifier ).getFromElement(), + walker.getSessionFactoryHelper().requireQueryableCollection( collectionType.getRole() ), + referenceName, + referencePath, + mappedPath + ); + } + } + + public FromElement getCollectionOwnerRef() { + return ownerFromElement; + } + + public CollectionPersister getCollectionDescriptor() { + return collectionDescriptor; + } + + public String getCollectionPropertyName() { + return collectionPropertyName; + } + + public String getCollectionPropertyPath() { + return collectionPropertyPath; + } + + public String getCollectionQueryPath() { + return collectionQueryPath; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java new file mode 100644 index 0000000000..a90a4d7687 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/CollectionSizeNode.java @@ -0,0 +1,207 @@ +/* + * 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.hql.internal.ast.tree; + +import org.hibernate.AssertionFailure; +import org.hibernate.hql.internal.NameGenerator; +import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.internal.util.StringHelper; +import org.hibernate.persister.collection.CollectionPropertyMapping; +import org.hibernate.persister.collection.CollectionPropertyNames; +import org.hibernate.persister.collection.QueryableCollection; +import org.hibernate.persister.entity.Joinable; +import org.hibernate.type.StandardBasicTypes; + +import org.jboss.logging.Logger; + +import antlr.SemanticException; +import antlr.collections.AST; + +/** + * @author Steve Ebersole + */ +public class CollectionSizeNode extends SqlNode implements SelectExpression { + private static final Logger log = Logger.getLogger( CollectionSizeNode.class ); + + private final CollectionPathNode collectionPathNode; + private final CollectionPropertyMapping collectionPropertyMapping; + + private final HqlSqlWalker walker; + private String alias; + + public CollectionSizeNode(CollectionPathNode collectionPathNode, HqlSqlWalker walker) { + this.collectionPathNode = collectionPathNode; + this.walker = walker; + + this.collectionPropertyMapping = new CollectionPropertyMapping( (QueryableCollection) collectionPathNode.getCollectionDescriptor() ); + + setType( HqlSqlTokenTypes.COLL_SIZE ); + setDataType( StandardBasicTypes.INTEGER ); + setText( "collection-size" ); + } + + public CollectionPathNode getCollectionPathNode() { + return collectionPathNode; + } + + public HqlSqlWalker getWalker() { + return walker; + } + + public String toSqlExpression() throws SemanticException { + // generate subquery in the form: + // + // select count( alias_. ) + // from as alias_ + // where = alias_. + + // need: + // => QueryableCollection#getKeyColumnNames + // => QueryableCollection#getKeyColumnNames + // => QueryableCollection#getTableName + // => ??? + + + final FromElement collectionOwnerFromElement = collectionPathNode.getCollectionOwnerRef(); + final QueryableCollection collectionDescriptor = (QueryableCollection) collectionPathNode.getCollectionDescriptor(); + final String collectionPropertyName = collectionPathNode.getCollectionPropertyName(); + + getWalker().addQuerySpaces( collectionDescriptor.getCollectionSpaces() ); + + // silly : need to prime `SessionFactoryHelper#collectionPropertyMappingByRole` + walker.getSessionFactoryHelper().requireQueryableCollection( collectionDescriptor.getRole() ); + + // owner-key + final String[] ownerKeyColumns; + final AST ast = walker.getAST(); + final String ownerTableAlias; + if ( ast instanceof DeleteStatement || ast instanceof UpdateStatement ) { + ownerTableAlias = collectionOwnerFromElement.getTableName(); + } + else { + ownerTableAlias = collectionOwnerFromElement.getTableAlias(); + } + + final String lhsPropertyName = collectionDescriptor.getCollectionType().getLHSPropertyName(); + if ( lhsPropertyName == null ) { + ownerKeyColumns = StringHelper.qualify( + ownerTableAlias, + ( (Joinable) collectionDescriptor.getOwnerEntityPersister() ).getKeyColumnNames() + ); + } + else { + ownerKeyColumns = collectionOwnerFromElement.toColumns( ownerTableAlias, lhsPropertyName, true ); + } + + // collection-key + final String collectionTableAlias = collectionOwnerFromElement.getFromClause() + .getAliasGenerator() + .createName( collectionPathNode.getCollectionPropertyName() ); + final String[] collectionKeyColumns = StringHelper.qualify( collectionTableAlias, collectionDescriptor.getKeyColumnNames() ); + + + if ( collectionKeyColumns.length != ownerKeyColumns.length ) { + throw new AssertionFailure( "Mismatch between collection key columns" ); + } + + // PropertyMapping(c).toColumns(customers) + // PropertyMapping(c.customers).toColumns(SIZE) + + // size expression (the count function) + final String[] sizeColumns = this.collectionPropertyMapping.toColumns( + collectionTableAlias, + CollectionPropertyNames.COLLECTION_SIZE + ); + assert sizeColumns.length == 1; + final String sizeColumn = sizeColumns[0]; + + final StringBuilder buffer = new StringBuilder( "(select " ).append( sizeColumn ); + buffer.append( " from " ).append( collectionDescriptor.getTableName() ).append( " as " ).append( collectionTableAlias ); + buffer.append( " where " ); + + boolean firstPass = true; + for ( int i = 0; i < ownerKeyColumns.length; i++ ) { + if ( firstPass ) { + firstPass = false; + } + else { + buffer.append( " and " ); + } + + buffer.append( ownerKeyColumns[i] ).append( " = " ).append( collectionKeyColumns[i] ); + } + + buffer.append( ")" ); + + if ( scalarName != null ) { + buffer.append( " as " ).append( scalarName ); + } + + final String subQuery = buffer.toString(); + + log.debugf( + "toSqlExpression( size(%s) ) -> %s", + collectionPathNode.getCollectionQueryPath(), + subQuery + ); + + return subQuery; + } + + private String scalarName; + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // SelectExpression + + @Override + public void setScalarColumnText(int i) throws SemanticException { + log.debugf( "setScalarColumnText(%s)", i ); + scalarName = NameGenerator.scalarName( i, 0 ); + } + + @Override + public void setScalarColumn(int i) throws SemanticException { + log.debugf( "setScalarColumn(%s)", i ); + setScalarColumnText( i ); + } + + @Override + public int getScalarColumnIndex() { + return -1; + } + + @Override + public FromElement getFromElement() { + return null; + } + + @Override + public boolean isConstructor() { + return false; + } + + @Override + public boolean isReturnableEntity() throws SemanticException { + return false; + } + + @Override + public boolean isScalar() throws SemanticException { + return false; + } + + @Override + public void setAlias(String alias) { + this.alias = alias; + } + + @Override + public String getAlias() { + return alias; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java index 2ea9e55d42..a44391c5f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ComponentJoin.java @@ -152,6 +152,9 @@ public class ComponentJoin extends FromElement { return getComponentPath() + '.' + propertyName; } + // `size( c.component.customers )` + // PropertyMapping(c).toColumns( component.customers ) + @Override public String[] toColumns(String alias, String propertyName) throws QueryException { return getBasePropertyMapping().toColumns( alias, getPropertyPath( propertyName ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java index 7c0db88a37..b4cf19dd7b 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/FromElement.java @@ -97,6 +97,10 @@ public class FromElement extends HqlSqlWalkerNode implements DisplayableNode, Pa } + public FromElementType getElementType() { + return elementType; + } + protected void initializeComponentJoin(FromElementType elementType) { fromClause.registerFromElement( this ); elementType.applyTreatAsDeclarations( getWalker().getTreatAsDeclarationsByPath( classAlias ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java index d5e36f32d0..5f6141e5bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/ImpliedFromElement.java @@ -23,6 +23,21 @@ public class ImpliedFromElement extends FromElement { */ private boolean inProjectionList; + /** + * Here to add debug breakpoints + */ + @SuppressWarnings("unused") + public ImpliedFromElement() { + super(); + } + + /** + * Here to add debug breakpoints + */ + public ImpliedFromElement(FromClause fromClause, FromElement origin, String alias) { + super( fromClause, origin, alias ); + } + public boolean isImplied() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java index 219a8411c9..31584a153d 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/tree/MethodNode.java @@ -154,11 +154,13 @@ public class MethodNode extends AbstractSelectExpression implements FunctionNode selectColumns = cpr.toColumns( fromElement.getTableAlias() ); // setDataType( fromElement.getPropertyType( propertyName, propertyName ) ); - selectColumns = fromElement.toColumns( fromElement.getTableAlias(), propertyName, inSelect ); +// selectColumns = fromElement.toColumns( fromElement.getTableAlias(), propertyName, inSelect ); } + if ( collectionNode instanceof DotNode ) { prepareAnyImplicitJoins( (DotNode) collectionNode ); } + if ( !inSelect ) { fromElement.setText( "" ); fromElement.setUseWhereFragment( false ); diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java index 0750751544..755d49e55c 100644 --- a/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/criteria/components/ComponentInWhereClauseTest.java @@ -34,6 +34,7 @@ import org.junit.Test; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.transaction.TransactionUtil; +import org.hibernate.testing.transaction.TransactionUtil2; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -96,6 +97,18 @@ public class ComponentInWhereClauseTest extends BaseEntityManagerFunctionalTestC ); } + @Test + public void testSizeExpressionForTheOneToManyPropertyOfAComponentHql() { + TransactionUtil2.inTransaction( + entityManagerFactory(), + session -> { + final String hql = "from Employee e where size( e.projects.previousProjects ) = 2"; + final List resultsList = session.createQuery( hql ).list(); + assertThat( resultsList.size(), is( 1 ) ); + } + ); + } + @Test public void testSizeExpressionForTheElementCollectionPropertyOfAComponent() { TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> { diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java index 4d256a378f..3131958c32 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/HQLTest.java @@ -60,6 +60,7 @@ import org.hibernate.testing.FailureExpected; import org.hibernate.testing.RequiresDialectFeature; import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.TestForIssue; +import org.junit.Ignore; import org.junit.Test; import antlr.RecognitionException; @@ -571,6 +572,7 @@ public class HQLTest extends QueryTranslatorTestCase { } @Test + @Ignore( "Old parser generated incorrect SQL for `size()`") public void testSizeFunctionAndProperty() { assertTranslation("from Animal a where a.offspring.size > 0"); assertTranslation("from Animal a join a.offspring where a.offspring.size > 1"); @@ -624,16 +626,9 @@ public class HQLTest extends QueryTranslatorTestCase { assertTranslation( "from Simple s where s = some( select sim from Simple sim where sim.other.count=s.other.count )" ); } - @Test - public void testCollectionOfValuesSize() throws Exception { - //SQL *was* missing a comma - assertTranslation( "select size(baz.stringDateMap) from org.hibernate.test.legacy.Baz baz" ); - } - @Test public void testCollectionFunctions() throws Exception { //these are both broken, a join that belongs in the subselect finds its way into the main query - assertTranslation( "from Zoo zoo where size(zoo.animals) > 100" ); assertTranslation( "from Zoo zoo where maxindex(zoo.mammals) = 'dog'" ); } @@ -651,8 +646,10 @@ public class HQLTest extends QueryTranslatorTestCase { } @Test - public void testCollectionSize() throws Exception { + @Ignore( "The old parser generated incorrect SQL for selection of size functions" ) + public void testCollectionSizeSelection() throws Exception { assertTranslation( "select size(zoo.animals) from Zoo zoo" ); + assertTranslation( "select size(baz.stringDateMap) from org.hibernate.test.legacy.Baz baz" ); } @Test diff --git a/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java b/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java index 7414e53635..371c570b59 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/hql/size/ManyToManySizeTest.java @@ -31,7 +31,49 @@ import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; public class ManyToManySizeTest extends BaseNonConfigCoreFunctionalTestCase { @Test - public void testSizeAsSelectExpression() { + public void testSizeAsRestriction() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select c.id from Company c where size( c.customers ) = 0" + ).list(); + assertThat( results.size(), is( 1 ) ); + assertThat( results.get( 0 ), is( 0 ) ); + } + ); + } + + @Test + public void testSizeAsCompoundSelectExpression() { + doInHibernate( + this::sessionFactory, + session -> { + final List results = session.createQuery( + "select c.id, c.name, size( c.customers )" + + " from Company c" + + " group by c.id, c.name" + + " order by c.id" + ).list(); + assertThat( results.size(), is( 3 ) ); + + final Object[] first = (Object[]) results.get( 0 ); + assertThat( first[ 0 ], is( 0 ) ); + assertThat( first[ 2 ], is( 0 ) ); + + final Object[] second = (Object[]) results.get( 1 ); + assertThat( second[ 0 ], is( 1 ) ); + assertThat( second[ 2 ], is( 1 ) ); + + final Object[] third = (Object[]) results.get( 2 ); + assertThat( third[ 0 ], is( 2 ) ); + assertThat( third[ 2 ], is( 2 ) ); + } + ); + } + + @Test + public void testSizeAsCtorSelectExpression() { doInHibernate( this::sessionFactory, session -> {