From 74b8e6dcf8dd74b41ac53257efd11373782eb6ba Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 18 May 2017 11:56:39 +0200 Subject: [PATCH] [#418, #569, #194] Optimize away null precedence handling when order by item is not nullable. Fixes #418. Fixes #569. Fixes #194 --- .../persistence/FullQueryBuilder.java | 118 +++++-- .../persistence/spi/ExtendedManagedType.java | 9 + .../persistence/spi/OrderByElement.java | 8 + .../impl/AbstractCTECriteriaBuilder.java | 2 +- .../impl/AbstractCommonQueryBuilder.java | 95 +++--- .../impl/AbstractFullQueryBuilder.java | 176 ++++++++-- .../AbstractModificationCriteriaBuilder.java | 8 +- .../BaseFinalSetOperationBuilderImpl.java | 76 ++++- .../impl/CommonQueryBuilderAdapter.java | 4 +- .../impl/DefaultOrderByElement.java | 35 +- .../persistence/impl/EntityMetamodelImpl.java | 37 ++- .../persistence/impl/ExpressionUtils.java | 153 ++------- .../persistence/impl/GroupByManager.java | 116 ++++++- .../persistence/impl/HavingManager.java | 8 +- .../persistence/impl/JoinManager.java | 18 +- .../blazebit/persistence/impl/JpaUtils.java | 7 +- .../persistence/impl/OrderByExpression.java | 8 +- .../persistence/impl/OrderByManager.java | 130 +++++--- .../impl/PaginatedCriteriaBuilderImpl.java | 284 ++++++++-------- .../impl/PaginatedTypedQueryImpl.java | 39 ++- .../impl/ResolvingQueryGenerator.java | 6 +- .../persistence/impl/SelectManager.java | 8 +- .../persistence/impl/SimplePathReference.java | 5 + ...ssociationParameterTransformerFactory.java | 2 +- .../impl/UniquenessDetectionVisitor.java | 303 ++++++++++++++++++ .../impl/dialect/DB2DbmsDialect.java | 2 +- .../impl/dialect/DefaultDbmsDialect.java | 10 +- .../impl/dialect/MSSQLDbmsDialect.java | 2 +- .../impl/dialect/MySQLDbmsDialect.java | 6 +- .../transform/ExpressionTransformerGroup.java | 7 +- .../transform/SimpleTransformerGroup.java | 13 +- .../transform/SizeTransformationVisitor.java | 103 +++--- .../impl/transform/SizeTransformerGroup.java | 36 +-- .../SubqueryRecursiveExpressionVisitor.java | 2 +- .../PathTargetResolvingExpressionVisitor.java | 10 +- .../parser/expression/PathExpression.java | 4 +- .../parser/util/ExpressionUtils.java | 10 + .../parser/util/JpaMetamodelUtils.java | 56 +++- .../testsuite/entity/Document.java | 2 + .../persistence/testsuite/entity/TestCTE.java | 2 +- .../persistence/testsuite/CTETest.java | 16 +- .../persistence/testsuite/DeleteTest.java | 4 +- .../persistence/testsuite/EntityJoinTest.java | 4 +- .../persistence/testsuite/InsertTest.java | 22 +- .../persistence/testsuite/JoinTest.java | 2 +- .../testsuite/JpqlFunctionTest.java | 4 +- .../testsuite/KeysetPaginationNullsTest.java | 6 +- .../testsuite/KeysetPaginationTest.java | 32 +- .../persistence/testsuite/LimitTest.java | 4 +- .../OptimizedKeysetPaginationNullsTest.java | 12 +- ...ysetPaginationRowValueConstructorTest.java | 32 +- .../OptimizedKeysetPaginationTest.java | 32 +- .../persistence/testsuite/OrderByTest.java | 89 ++++- .../testsuite/PaginationEmbeddedIdTest.java | 6 +- .../persistence/testsuite/PaginationTest.java | 76 +++-- .../persistence/testsuite/SelectNewTest.java | 4 +- .../persistence/testsuite/SelectTest.java | 18 +- .../testsuite/SetOperationTest.java | 54 ++-- .../testsuite/SizeTransformationTest.java | 8 +- .../persistence/testsuite/SubqueryTest.java | 26 +- .../persistence/testsuite/UpdateTest.java | 2 +- .../testsuite/ValuesClauseTest.java | 2 +- .../core/manual/en_US/11_expressions.adoc | 2 +- .../view/impl/EntityViewManagerImpl.java | 2 +- .../impl/entity/AbstractEntityLoader.java | 2 +- .../DefaultEntityLoaderFetchGraphNode.java | 4 +- .../view/impl/entity/FullEntityLoader.java | 2 +- .../entity/InverseEntityToEntityMapper.java | 2 +- .../entity/InverseViewToEntityMapper.java | 2 +- .../mapper/TupleElementMapperBuilder.java | 2 +- ...bstractCorrelatedTupleListTransformer.java | 2 +- .../view/impl/proxy/ProxyFactory.java | 33 +- .../impl/update/EntityViewUpdaterImpl.java | 4 +- .../handler/EntityViewRepositoryHandler.java | 2 +- .../handler/EntityViewRepositoryHandler.java | 2 +- .../base/HibernateJpa21Provider.java | 32 +- .../persistence/criteria/OrderByTest.java | 8 +- 77 files changed, 1656 insertions(+), 820 deletions(-) create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/UniquenessDetectionVisitor.java diff --git a/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java b/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java index c73102434a..e61384fb2c 100644 --- a/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java +++ b/core/api/src/main/java/com/blazebit/persistence/FullQueryBuilder.java @@ -49,61 +49,134 @@ public interface FullQueryBuilder> extends Q public TypedQuery getCountQuery(); /** - * Paginates the results of this query. + * Invokes {@link FullQueryBuilder#page(int, int, String, String...)} with the identifiers of the query root entity. * - *

- * Please note: The pagination only works on entity level and NOT on row level. This means that for queries which yield multiple - * result set rows per entity (i.e. rows with the same entity id), the multiple rows are treated as 1 page entry during the - * pagination process. Hence, the result set size of such paginated queries might be greater than the specified page size. - *

+ * @param firstResult The position of the first result to retrieve, numbered from 0 + * @param maxResults The maximum number of results to retrieve + * @return This query builder as paginated query builder + */ + public PaginatedCriteriaBuilder page(int firstResult, int maxResults); + + /** + * Invokes {@link FullQueryBuilder#page(Object, int, String, String...)} with the identifiers of the query root entity. + * + * @param entityId The id of the entity which should be located on the page + * @param maxResults The maximum number of results to retrieve + * @return This query builder as paginated query builder + */ + public PaginatedCriteriaBuilder page(Object entityId, int maxResults); + + /** + * Invokes {@link FullQueryBuilder#page(KeysetPage, int, int, String, String...)} with the identifiers of the query root entity. + * + * @param keysetPage The key set from a previous result, may be null + * @param firstResult The position of the first result to retrieve, numbered from 0 + * @param maxResults The maximum number of results to retrieve + * @return This query builder as paginated query builder + * @see PagedList#getKeysetPage() + */ + public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstResult, int maxResults); + + /** + * Like {@link FullQueryBuilder#page(int, int, String, String...)} but lacks the varargs parameter to avoid heap pollution. + * + * @param firstResult The position of the first result to retrieve, numbered from 0 + * @param maxResults The maximum number of results to retrieve + * @param identifierExpression The first identifier expression + * @return This query builder as paginated query builder + * @since 1.3.0 + */ + public PaginatedCriteriaBuilder page(int firstResult, int maxResults, String identifierExpression); + + /** + * Like {@link FullQueryBuilder#page(Object, int, String, String...)} but lacks the varargs parameter to avoid heap pollution. + * + * @param entityId The id of the entity which should be located on the page + * @param maxResults The maximum number of results to retrieve + * @param identifierExpression The first identifier expression + * @return This query builder as paginated query builder + * @since 1.3.0 + */ + public PaginatedCriteriaBuilder page(Object entityId, int maxResults, String identifierExpression); + + /** + * Like {@link FullQueryBuilder#page(KeysetPage, int, int, String, String...)} but lacks the varargs parameter to avoid heap pollution. + * + * @param keysetPage The key set from a previous result, may be null + * @param firstResult The position of the first result to retrieve, numbered from 0 + * @param maxResults The maximum number of results to retrieve + * @param identifierExpression The first identifier expression + * @return This query builder as paginated query builder + * @since 1.3.0 + * @see PagedList#getKeysetPage() + */ + public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstResult, int maxResults, String identifierExpression); + + /** + * Paginates the results of this query based on the given identifier expressions. + * + * In JPA, the use of setFirstResult and setMaxResults is not defined when involving fetch joins for collections. + * When no collection joins are involved, this is fine as rows essentially represent objects, but when collections are joined, this is no longer true. + * JPA providers usually fall back to querying all data and doing pagination in-memory based on objects or simply don't support that kind of query. + * + * This API allows to specify the identifier expressions to use for pagination and transparently handles collection join support. + * The big advantage of this API over plain setFirstResult and setMaxResults can also be seen when doing scalar queries. * *

- * An example for such queries would be a query that joins a collection: SELECT d.id, contacts.name FROM Document d LEFT JOIN - * d.contacts contacts If one Document has associated multiple contacts, the above query will produce multiple result set rows for - * this document. + * An example for such queries would be a query that joins a collection: + * + * SELECT d.id, contacts.name FROM Document d LEFT JOIN d.contacts contacts + * + * If one Document has associated multiple contacts, the above query will produce multiple result set rows for that document. + * Paginating via setFirstResult and setMaxResults would produce unexpected results whereas using this API, will produce the expected results. *

* *

- * Since the pagination works on entity id level, the results are implicitly grouped by id and distinct. Therefore calling - * distinct() or groupBy() on a PaginatedCriteriaBuilder is not allowed. + * When paginating on the identifier i.e. d.id, the results are implicitly grouped by the document id and distinct. Therefore calling + * distinct() on a PaginatedCriteriaBuilder is not allowed. *

* * @param firstResult The position of the first result to retrieve, numbered from 0 * @param maxResults The maximum number of results to retrieve + * @param identifierExpression The first identifier expression + * @param identifierExpressions The other identifier expressions * @return This query builder as paginated query builder + * @since 1.3.0 */ - public PaginatedCriteriaBuilder page(int firstResult, int maxResults); + public PaginatedCriteriaBuilder page(int firstResult, int maxResults, String identifierExpression, String... identifierExpressions); /** * Paginates the results of this query and navigates to the page on which - * the entity with the given entity id is located. + * the object with the given identifier is located. * * Beware that the same limitations like for {@link FullQueryBuilder#page(int, int)} apply. - * If the entity with the given entity id does not exist in the result list: + * If the object with the given identifier does not exist in the result list: *
    *
  • The result of {@link PaginatedCriteriaBuilder#getResultList()} will contain the first page
  • *
  • {@link PagedList#getFirstResult()} will return -1
  • *
* - * @param entityId The id of the entity which should be located on the page + * @param entityId The id of the object which should be located on the page * @param maxResults The maximum number of results to retrieve + * @param identifierExpression The first identifier expression + * @param identifierExpressions The other identifier expressions * @return This query builder as paginated query builder + * @since 1.3.0 */ - public PaginatedCriteriaBuilder page(Object entityId, int maxResults); + public PaginatedCriteriaBuilder page(Object entityId, int maxResults, String identifierExpression, String... identifierExpressions); /** * Like {@link FullQueryBuilder#page(int, int)} but additionally uses key set pagination when possible. * * Beware that keyset pagination should not be used as a direct replacement for offset pagination. * Since entries that have a lower rank than some keyset might be added or removed, the calculations - * for the firstResult might be wrong. If strict pagination is required, then a keyset should - * be thrown away when the count of lower ranked items changes to make use of offset pagination again. + * for the firstResult might be wrong. If strict pagination is required, then the {@link KeysetPage} should + * not be used when the count of lower ranked items changes which will result in the use of offset pagination for that request. * *

* Key set pagination is possible if and only if the following conditions are met: *

    - *
  • This keyset reference values fit the order by expressions of this query builder AND
  • - * + *
  • The keyset reference values fit the order by expressions of this query builder AND
  • *
  • {@link KeysetPage#getMaxResults()} and maxResults evaluate to the same value AND
  • *
  • One of the following conditions is met: *
      @@ -118,10 +191,13 @@ public interface FullQueryBuilder> extends Q * @param keysetPage The key set from a previous result, may be null * @param firstResult The position of the first result to retrieve, numbered from 0 * @param maxResults The maximum number of results to retrieve + * @param identifierExpression The first identifier expression + * @param identifierExpressions The other identifier expressions * @return This query builder as paginated query builder + * @since 1.3.0 * @see PagedList#getKeysetPage() */ - public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstResult, int maxResults); + public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstResult, int maxResults, String identifierExpression, String... identifierExpressions); /* * Join methods diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java index ff43225cf5..d0269acd45 100644 --- a/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java +++ b/core/api/src/main/java/com/blazebit/persistence/spi/ExtendedManagedType.java @@ -19,6 +19,7 @@ import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.SingularAttribute; import java.util.Map; +import java.util.Set; /** * This is a wrapper around the JPA {@link javax.persistence.metamodel.ManagedType} that allows additionally efficient access to properties of the metamodel. @@ -50,6 +51,14 @@ public interface ExtendedManagedType { */ public SingularAttribute getIdAttribute(); + /** + * Returns the id attributes or an empty set if it doesn't have an id. + * + * @return The id attributes + * @since 1.3.0 + */ + public Set> getIdAttributes(); + /** * Returns the extended attributes of the managed type. * diff --git a/core/api/src/main/java/com/blazebit/persistence/spi/OrderByElement.java b/core/api/src/main/java/com/blazebit/persistence/spi/OrderByElement.java index a79f6a9d23..77ea0fb85a 100644 --- a/core/api/src/main/java/com/blazebit/persistence/spi/OrderByElement.java +++ b/core/api/src/main/java/com/blazebit/persistence/spi/OrderByElement.java @@ -39,6 +39,14 @@ public interface OrderByElement { */ public boolean isAscending(); + /** + * Whether the expression may produce null values. + * + * @return True if nullable, false otherwise + * @since 1.3.0 + */ + public boolean isNullable(); + /** * Whether nulls have precedence or non-nulls. * diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java index 0c020b3455..4fd4003d83 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCTECriteriaBuilder.java @@ -199,7 +199,7 @@ protected List prepareAndGetAttributes() { if (JpaMetamodelUtils.isJoinable(attributePath.get(attributePath.size() - 1))) { // We have to map *-to-one relationships to their ids EntityType type = mainQuery.metamodel.entity(attributeEntry.getElementClass()); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(type); + Attribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute(type); // NOTE: Since we are talking about *-to-ones, the expression can only be a path to an object // so it is safe to just append the id to the path Expression selectExpression = selectManager.getSelectInfos().get(bindingEntry.getValue()).getExpression(); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java index 7600754f82..57e156ee60 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractCommonQueryBuilder.java @@ -39,6 +39,7 @@ import com.blazebit.persistence.SubqueryInitiator; import com.blazebit.persistence.WhereOrBuilder; import com.blazebit.persistence.impl.util.SqlUtils; +import com.blazebit.persistence.parser.EntityMetamodel; import com.blazebit.persistence.parser.expression.Expression; import com.blazebit.persistence.parser.expression.ExpressionFactory; import com.blazebit.persistence.parser.expression.PathExpression; @@ -52,6 +53,7 @@ import com.blazebit.persistence.impl.keyset.KeysetManager; import com.blazebit.persistence.impl.keyset.KeysetMode; import com.blazebit.persistence.impl.keyset.SimpleKeysetLink; +import com.blazebit.persistence.parser.expression.modifier.ExpressionModifier; import com.blazebit.persistence.parser.predicate.Predicate; import com.blazebit.persistence.impl.query.AbstractCustomQuery; import com.blazebit.persistence.impl.query.CTENode; @@ -92,7 +94,6 @@ import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ManagedType; -import javax.persistence.metamodel.Metamodel; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; @@ -258,18 +259,18 @@ protected AbstractCommonQueryBuilder(MainQuery mainQuery, boolean isMainQuery, D this.groupByExpressionGatheringVisitor = new GroupByExpressionGatheringVisitor(false, dbmsDialect); this.whereManager = new WhereManager(queryGenerator, parameterManager, subqueryInitFactory, expressionFactory); - this.havingManager = new HavingManager(queryGenerator, parameterManager, subqueryInitFactory, expressionFactory, groupByExpressionGatheringVisitor); this.groupByManager = new GroupByManager(queryGenerator, parameterManager, subqueryInitFactory); + this.havingManager = new HavingManager(queryGenerator, parameterManager, subqueryInitFactory, expressionFactory, groupByExpressionGatheringVisitor); this.selectManager = new SelectManager(queryGenerator, parameterManager, this.joinManager, this.aliasManager, subqueryInitFactory, expressionFactory, jpaProvider, mainQuery, groupByExpressionGatheringVisitor, resultClazz); - this.orderByManager = new OrderByManager(queryGenerator, parameterManager, subqueryInitFactory, this.aliasManager, jpaProvider, groupByExpressionGatheringVisitor); + this.orderByManager = new OrderByManager(queryGenerator, parameterManager, subqueryInitFactory, this.joinManager, this.aliasManager, mainQuery.metamodel, jpaProvider, groupByExpressionGatheringVisitor); this.keysetManager = new KeysetManager(queryGenerator, parameterManager, jpaProvider, dbmsDialect); final SizeTransformationVisitor sizeTransformationVisitor = new SizeTransformationVisitor(mainQuery, subqueryInitFactory, joinManager, jpaProvider); this.transformerGroups = Arrays.>asList( new SimpleTransformerGroup(new OuterFunctionVisitor(joinManager)), new SimpleTransformerGroup(new SubqueryRecursiveExpressionVisitor()), - new SizeTransformerGroup(sizeTransformationVisitor, orderByManager, selectManager, joinManager)); + new SizeTransformerGroup(sizeTransformationVisitor, orderByManager, selectManager, joinManager, groupByManager)); this.resultType = resultClazz; this.finalSetOperationBuilder = finalSetOperationBuilder; @@ -766,7 +767,7 @@ public EntityManager getEntityManager() { return em; } - public Metamodel getMetamodel() { + public EntityMetamodel getMetamodel() { return mainQuery.metamodel; } @@ -1559,10 +1560,13 @@ protected void applyVisitor(VisitorAdapter expressionVisitor) { orderByManager.acceptVisitor(expressionVisitor); } - public void applyExpressionTransformers() { + public void applyExpressionTransformersAndBuildGroupByClauses(boolean addsGroupBy) { + groupByManager.resetCollected(); + groupByManager.collectGroupByClauses(); int size = transformerGroups.size(); for (int i = 0; i < size; i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); + @SuppressWarnings("unchecked") + ExpressionTransformerGroup transformerGroup = (ExpressionTransformerGroup) transformerGroups.get(i); transformerGroup.applyExpressionTransformer(joinManager); transformerGroup.applyExpressionTransformer(selectManager); transformerGroup.applyExpressionTransformer(whereManager); @@ -1570,16 +1574,35 @@ public void applyExpressionTransformers() { transformerGroup.applyExpressionTransformer(havingManager); transformerGroup.applyExpressionTransformer(orderByManager); - transformerGroup.afterGlobalTransformation(); + transformerGroup.afterTransformationGroup(); } // After all transformations are done, we can finally check if aggregations are used - hasGroupBy = groupByManager.hasGroupBys(); + hasGroupBy = !groupByManager.getCollectedGroupByClauses().isEmpty(); hasGroupBy = hasGroupBy || Boolean.TRUE.equals(selectManager.acceptVisitor(AggregateDetectionVisitor.INSTANCE, true)); hasGroupBy = hasGroupBy || Boolean.TRUE.equals(joinManager.acceptVisitor(AggregateDetectionVisitor.INSTANCE, true)); hasGroupBy = hasGroupBy || Boolean.TRUE.equals(whereManager.acceptVisitor(AggregateDetectionVisitor.INSTANCE)); hasGroupBy = hasGroupBy || Boolean.TRUE.equals(orderByManager.acceptVisitor(AggregateDetectionVisitor.INSTANCE, true)); hasGroupBy = hasGroupBy || Boolean.TRUE.equals(havingManager.acceptVisitor(AggregateDetectionVisitor.INSTANCE)); + + if (hasGroupBy || addsGroupBy) { + if (mainQuery.getQueryConfiguration().isImplicitGroupByFromSelectEnabled()) { + selectManager.buildGroupByClauses(cbf.getMetamodel(), groupByManager); + } + if (mainQuery.getQueryConfiguration().isImplicitGroupByFromHavingEnabled()) { + havingManager.buildGroupByClauses(groupByManager); + } + if (mainQuery.getQueryConfiguration().isImplicitGroupByFromOrderByEnabled()) { + orderByManager.buildGroupByClauses(groupByManager); + } + } + + if (!groupByManager.getCollectedGroupByClauses().isEmpty()) { + for (int i = 0; i < size; i++) { + ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); + transformerGroup.afterAllTransformations(); + } + } } public Class getResultType() { @@ -1810,21 +1833,25 @@ private Query getValuesExampleQuery(Class clazz, int valueCount, boolean iden Map mapping = mainQuery.metamodel.getManagedType(ExtendedManagedType.class, clazz).getAttributes(); StringBuilder paramBuilder = new StringBuilder(); for (int i = 0; i < attributes.length; i++) { - sb.append("e."); ExtendedAttribute entry = mapping.get(attributes[i]); Attribute attribute = entry.getAttribute(); String[] columnTypes = entry.getColumnTypes(); attributeParameter[i] = getCastedParameters(paramBuilder, mainQuery.dbmsDialect, columnTypes); - sb.append(attributes[i]); // When the class for which we want a VALUES clause has *ToOne relations, we need to put their ids into the select // otherwise we would fetch all of the types attributes, but the VALUES clause can only ever contain the id if (attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.BASIC && attribute.getPersistentAttributeType() != Attribute.PersistentAttributeType.EMBEDDED) { ManagedType managedAttributeType = mainQuery.metamodel.managedType(entry.getElementClass()); - Attribute attributeTypeIdAttribute = JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedAttributeType); - sb.append('.'); - sb.append(attributeTypeIdAttribute.getName()); + for (Attribute attributeTypeIdAttribute : JpaMetamodelUtils.getIdAttributes((IdentifiableType) managedAttributeType)) { + sb.append("e."); + sb.append(attributes[i]); + sb.append('.'); + sb.append(attributeTypeIdAttribute.getName()); + } + } else { + sb.append("e."); + sb.append(attributes[i]); } sb.append(','); @@ -2128,12 +2155,12 @@ public void visit(JoinNode node) { // join("a.b", "b").where("b.c") // in the first case applyImplicitJoins(null); - applyExpressionTransformers(); + applyExpressionTransformersAndBuildGroupByClauses(false); if (keysetManager.hasKeyset()) { // The last order by expression must be unique, otherwise keyset scrolling wouldn't work - List orderByExpressions = orderByManager.getOrderByExpressions(cbf.getMetamodel()); - if (!orderByExpressions.get(orderByExpressions.size() - 1).isUnique()) { + List orderByExpressions = orderByManager.getOrderByExpressions(true, groupByManager.getCollectedGroupByClauses()); + if (!orderByExpressions.get(orderByExpressions.size() - 1).isResultUnique()) { throw new IllegalStateException("The last order by item must be unique!"); } keysetManager.initialize(orderByExpressions); @@ -2225,42 +2252,14 @@ protected void appendWhereClause(StringBuilder sbSelectFrom, List whereC } protected void appendGroupByClause(StringBuilder sbSelectFrom) { - Set clauses = new LinkedHashSet(); - groupByManager.buildGroupByClauses(clauses); - - int size = transformerGroups.size(); - for (int i = 0; i < transformerGroups.size(); i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); - clauses.addAll(transformerGroup.getRequiredGroupByClauses()); - } - if (hasGroupBy) { - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromSelectEnabled()) { - selectManager.buildGroupByClauses(cbf.getMetamodel(), clauses); - } - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromHavingEnabled()) { - havingManager.buildGroupByClauses(clauses); - } - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromOrderByEnabled()) { - orderByManager.buildGroupByClauses(clauses); - } - } - - if (!clauses.isEmpty()) { - for (int i = 0; i < size; i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); - clauses.addAll(transformerGroup.getOptionalGroupByClauses()); - } + groupByManager.buildGroupBy(sbSelectFrom); + havingManager.buildClause(sbSelectFrom); } - - groupByManager.buildGroupBy(sbSelectFrom, clauses); - havingManager.buildClause(sbSelectFrom); } protected void appendOrderByClause(StringBuilder sbSelectFrom) { - queryGenerator.setResolveSelectAliases(false); - orderByManager.buildOrderBy(sbSelectFrom, false, false); - queryGenerator.setResolveSelectAliases(true); + orderByManager.buildOrderBy(sbSelectFrom, false, false, false); } protected Map getModificationStates(Map, Map> explicitVersionEntities) { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java index 15019b4ad8..4c3294e77a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractFullQueryBuilder.java @@ -18,14 +18,16 @@ import java.lang.reflect.Constructor; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.persistence.TypedQuery; import javax.persistence.metamodel.Attribute; -import javax.persistence.metamodel.EntityType; +import javax.persistence.metamodel.SingularAttribute; import com.blazebit.persistence.FullQueryBuilder; import com.blazebit.persistence.JoinType; @@ -39,6 +41,7 @@ import com.blazebit.persistence.impl.query.CustomSQLTypedQuery; import com.blazebit.persistence.impl.query.EntityFunctionNode; import com.blazebit.persistence.impl.query.QuerySpecification; +import com.blazebit.persistence.parser.expression.Expression; import com.blazebit.persistence.parser.util.JpaMetamodelUtils; /** @@ -125,15 +128,27 @@ protected String buildPageCountQueryString(boolean externalRepresentation) { return sbSelectFrom.toString(); } - private String buildPageCountQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { - JoinNode rootNode = joinManager.getRootNodeOrFail("Count queries do not support multiple from clause elements!"); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(mainQuery.metamodel.entity(rootNode.getJavaType())); - String idName = idAttribute.getName(); + protected final String buildPageCountQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { + if (!havingManager.isEmpty()) { + throw new IllegalStateException("Count queries when HAVING predicates are present are not yet supported!"); + } + + String[] identifierExpressions = getIdentifierExpressions(); StringBuilder idClause = new StringBuilder(100); - rootNode.appendDeReference(idClause, idName); - // Spaces are important to be able to reuse the string builder without copying + // So when there is an explicit group by, we consider that as being our "identifier" on which we do a distinct count + // In any case, we also include implicit group bys into our distinct count + int lastIndex = -1; + for (String identifierExpression : identifierExpressions) { + idClause.append(identifierExpression); + lastIndex = idClause.length(); + idClause.append(", "); + } + idClause.setLength(lastIndex); + boolean hasGroupByClauses = groupByManager.buildClause(idClause, ", ", EnumSet.of(ClauseType.ORDER_BY, ClauseType.SELECT), Arrays.asList(identifierExpressions)); + String countString = jpaProvider.getCustomFunctionInvocation(AbstractCountFunction.FUNCTION_NAME, 1) + "'DISTINCT'," + idClause + ")"; sbSelectFrom.append("SELECT ").append(countString); + appendPageCountQueryStringExtensions(sbSelectFrom); List whereClauseConjuncts = new ArrayList<>(); // The count query does not have any fetch owners @@ -146,7 +161,7 @@ private String buildPageCountQueryString(StringBuilder sbSelectFrom, boolean ext whereManager.buildClause(sbSelectFrom, whereClauseConjuncts, null); // Count distinct is obviously unnecessary if we have no collection joins - if (!hasCollectionJoinUsages) { + if (!hasCollectionJoinUsages && !hasGroupByClauses) { int idx = sbSelectFrom.indexOf(countString); int endIdx = idx + countString.length() - 1; String countStar; @@ -167,6 +182,13 @@ private String buildPageCountQueryString(StringBuilder sbSelectFrom, boolean ext return sbSelectFrom.toString(); } + protected void appendPageCountQueryStringExtensions(StringBuilder sbSelectFrom) { + } + + protected String[] getIdentifierExpressions() { + return getQueryRootEntityIdentifierExpressions(); + } + @Override public TypedQuery getCountQuery() { prepareAndCheck(); @@ -208,56 +230,152 @@ public TypedQuery getCountQuery() { @Override public PaginatedCriteriaBuilder page(int firstRow, int pageSize) { + return page(firstRow, pageSize, getQueryRootEntityIdentifierExpressions()); + } + + @Override + public PaginatedCriteriaBuilder page(Object entityId, int pageSize) { + return page(entityId, pageSize, getQueryRootEntityIdentifierExpressions()); + } + + @Override + public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstRow, int pageSize) { + return page(keysetPage, firstRow, pageSize, getQueryRootEntityIdentifierExpressions()); + } + + @Override + public PaginatedCriteriaBuilder page(int firstRow, int pageSize, String identifierExpression) { + return page(firstRow, pageSize, new String[]{ identifierExpression }); + } + + @Override + public PaginatedCriteriaBuilder page(Object entityId, int pageSize, String identifierExpression) { + return page(entityId, pageSize, new String[]{ identifierExpression }); + } + + @Override + public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstRow, int pageSize, String identifierExpression) { + return page(keysetPage, firstRow, pageSize, new String[]{ identifierExpression }); + } + + @Override + public PaginatedCriteriaBuilder page(int firstRow, int pageSize, String identifierExpression, String... identifierExpressions) { + return page(firstRow, pageSize, getIdentifierExpressions(identifierExpression, identifierExpressions)); + } + + @Override + public PaginatedCriteriaBuilder page(Object entityId, int pageSize, String identifierExpression, String... identifierExpressions) { + return page(entityId, pageSize, getIdentifierExpressions(identifierExpression, identifierExpressions)); + } + + @Override + public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstRow, int pageSize, String identifierExpression, String... identifierExpressions) { + return page(keysetPage, firstRow, pageSize, getIdentifierExpressions(identifierExpression, identifierExpressions)); + } + + private String[] getQueryRootEntityIdentifierExpressions() { + JoinNode rootNode = joinManager.getRootNodeOrFail("Paginated criteria builders do not support multiple from clause elements!"); + Set> idAttributes = JpaMetamodelUtils.getIdAttributes(mainQuery.metamodel.entity(rootNode.getJavaType())); + String[] identifierExpressions = new String[idAttributes.size()]; + StringBuilder sb = new StringBuilder(); + int i = 0; + for (Attribute idAttribute : idAttributes) { + String idName = idAttribute.getName(); + sb.setLength(0); + rootNode.appendDeReference(sb, idName); + identifierExpressions[i++] = sb.toString(); + } + return identifierExpressions; + } + + private String[] getIdentifierExpressions(String identifierExpression, String[] identifierExpressions) { + Set set = new LinkedHashSet<>(); + if (identifierExpression != null) { + set.add(identifierExpression); + } + if (identifierExpressions != null) { + for (String expression : identifierExpressions) { + set.add(expression); + } + } + return set.toArray(new String[set.size()]); + } + + private PaginatedCriteriaBuilder page(int firstRow, int pageSize, String[] identifierExpressions) { prepareForModification(); if (selectManager.isDistinct()) { throw new IllegalStateException("Cannot paginate a DISTINCT query"); } - if (!groupByManager.isEmpty()) { - throw new IllegalStateException("Cannot paginate a GROUP BY query"); + if (!havingManager.isEmpty()) { + throw new IllegalStateException("Cannot paginate a HAVING query"); } createdPaginatedBuilder = true; - return new PaginatedCriteriaBuilderImpl(this, false, null, firstRow, pageSize); + return new PaginatedCriteriaBuilderImpl(this, false, null, firstRow, pageSize, identifierExpressions); } - @Override - public PaginatedCriteriaBuilder page(Object entityId, int pageSize) { + private PaginatedCriteriaBuilder page(Object entityId, int pageSize, String[] identifierExpressions) { prepareForModification(); if (selectManager.isDistinct()) { throw new IllegalStateException("Cannot paginate a DISTINCT query"); } - if (!groupByManager.isEmpty()) { - throw new IllegalStateException("Cannot paginate a GROUP BY query"); + if (!havingManager.isEmpty()) { + throw new IllegalStateException("Cannot paginate a HAVING query"); } - checkEntityId(entityId); + checkEntityId(entityId, identifierExpressions); createdPaginatedBuilder = true; - return new PaginatedCriteriaBuilderImpl(this, false, entityId, pageSize); + return new PaginatedCriteriaBuilderImpl(this, false, entityId, pageSize, identifierExpressions); } - @Override - public PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstRow, int pageSize) { + private PaginatedCriteriaBuilder page(KeysetPage keysetPage, int firstRow, int pageSize, String[] identifierExpressions) { prepareForModification(); if (selectManager.isDistinct()) { throw new IllegalStateException("Cannot paginate a DISTINCT query"); } - if (!groupByManager.isEmpty()) { - throw new IllegalStateException("Cannot paginate a GROUP BY query"); + if (!havingManager.isEmpty()) { + throw new IllegalStateException("Cannot paginate a HAVING query"); } createdPaginatedBuilder = true; - return new PaginatedCriteriaBuilderImpl(this, true, keysetPage, firstRow, pageSize); + return new PaginatedCriteriaBuilderImpl(this, true, keysetPage, firstRow, pageSize, identifierExpressions); } - private void checkEntityId(Object entityId) { + private void checkEntityId(Object entityId, String[] identifierExpressions) { if (entityId == null) { throw new IllegalArgumentException("Invalid null entity id given"); } + if (identifierExpressions.length == 0) { + throw new IllegalArgumentException("Empty identifier expressions given"); + } + if (identifierExpressions.length > 1) { + if (!entityId.getClass().isArray()) { + throw new IllegalArgumentException("The type of the given entity id '" + entityId.getClass().getName() + + "' is not an array of the identifier components " + Arrays.toString(identifierExpressions) + " !"); + } + + Object[] entityIdComponents = (Object[]) entityId; + if (entityIdComponents.length != identifierExpressions.length) { + throw new IllegalArgumentException("The number of entity id components is '" + entityIdComponents.length + + "' which does not match the number of identifier component expressions " + identifierExpressions.length + " !"); + } + + for (int i = 0; i < identifierExpressions.length; i++) { + Expression expression = expressionFactory.createSimpleExpression(identifierExpressions[i], false); + joinManager.implicitJoin(expression, false, null, null, null, false, false, false, false); + checkEntityIdComponent(entityIdComponents[i], expression, identifierExpressions[i]); + } + } else { + Expression expression = expressionFactory.createSimpleExpression(identifierExpressions[0], false); + joinManager.implicitJoin(expression, false, null, null, null, false, false, false, false); + checkEntityIdComponent(entityId, expression, "identifier"); + } + } - EntityType entityType = mainQuery.metamodel.entity(joinManager.getRootNodeOrFail("Paginated queries do not support multiple from clause elements!").getJavaType()); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(entityType); - Class idType = JpaMetamodelUtils.resolveFieldClass(entityType.getJavaType(), idAttribute); + private void checkEntityIdComponent(Object component, Expression expression, String componentName) { + AttributeHolder attribute = JpaUtils.getAttributeForJoining(getMetamodel(), expression); + Class type = attribute.getAttributeType().getJavaType(); - if (!idType.isInstance(entityId)) { - throw new IllegalArgumentException("The type of the given entity id '" + entityId.getClass().getName() - + "' is not an instance of the expected id type '" + idType.getName() + "' of the entity class '" + entityType.getJavaType().getName() + "'"); + if (type == null || !type.isInstance(component)) { + throw new IllegalArgumentException("The type of the given " + componentName + " '" + component.getClass().getName() + + "' is not an instance of the expected type '" + JpaMetamodelUtils.getTypeName(attribute.getAttributeType()) + "'"); } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java index f94f40d320..040489d80b 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/AbstractModificationCriteriaBuilder.java @@ -406,7 +406,7 @@ public SimpleReturningBuilder returning(String modificationQueryAttribute) { if (isReturningEntityAliasAllowed && modificationQueryAttribute.equals(entityAlias)) { // Our little special case, since there would be no other way to refer to the id as the object type - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(entityType); + Attribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute(entityType); modificationQueryAttribute = idAttribute.getName(); } @@ -446,7 +446,7 @@ public X returning(String cteAttribute, String modificationQueryAttribute) { if (isReturningEntityAliasAllowed && modificationQueryAttribute.equals(entityAlias)) { // Our little special case, since there would be no other way to refer to the id as the object type queryAttrType = entityType.getJavaType(); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(entityType); + Attribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute(entityType); modificationQueryAttribute = idAttribute.getName(); } else { AttributePath queryAttributePath = JpaMetamodelUtils.getBasicAttributePath(getMetamodel(), entityType, modificationQueryAttribute); @@ -579,7 +579,7 @@ private TypedQuery getExampleQuery(List>> attribu } // TODO: actually we should also check if the attribute is a @GeneratedValue - if (!dbmsDialect.supportsReturningColumns() && !JpaMetamodelUtils.getIdAttribute(entityType).equals(lastPathElem)) { + if (!dbmsDialect.supportsReturningColumns() && !JpaMetamodelUtils.getSingleIdAttribute(entityType).equals(lastPathElem)) { throw new IllegalArgumentException("Returning the query attribute [" + lastPathElem.getName() + "] is not supported by the dbms, only generated keys can be returned!"); } @@ -587,7 +587,7 @@ private TypedQuery getExampleQuery(List>> attribu sb.append(entityAlias).append('.'); // We have to map *-to-one relationships to their ids EntityType type = mainQuery.metamodel.entity(JpaMetamodelUtils.resolveFieldClass(entityType.getJavaType(), lastPathElem)); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(type); + Attribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute(type); // NOTE: Since we are talking about *-to-ones, the expression can only be a path to an object // so it is safe to just append the id to the path sb.append(lastPathElem.getName()).append('.').append(idAttribute.getName()); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/BaseFinalSetOperationBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/BaseFinalSetOperationBuilderImpl.java index a38aa8cd48..633e412dfa 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/BaseFinalSetOperationBuilderImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/BaseFinalSetOperationBuilderImpl.java @@ -24,6 +24,10 @@ import com.blazebit.persistence.impl.query.EntityFunctionNode; import com.blazebit.persistence.impl.query.QuerySpecification; import com.blazebit.persistence.impl.query.SetOperationQuerySpecification; +import com.blazebit.persistence.parser.expression.PathElementExpression; +import com.blazebit.persistence.parser.expression.PathExpression; +import com.blazebit.persistence.parser.expression.PropertyExpression; +import com.blazebit.persistence.parser.util.JpaMetamodelUtils; import com.blazebit.persistence.spi.DbmsStatementType; import com.blazebit.persistence.spi.OrderByElement; import com.blazebit.persistence.spi.SetOperationType; @@ -31,6 +35,7 @@ import javax.persistence.Query; import javax.persistence.TypedQuery; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -74,20 +79,63 @@ private static boolean isNestedAndComplex(AbstractCommonQueryBuilder leftMostQuery = getLeftMost(setOperationManager.getStartQueryBuilder()); - + + int position; AliasInfo aliasInfo = leftMostQuery.aliasManager.getAliasInfo(expression); - if (aliasInfo != null) { + if (aliasInfo == null) { + position = cbf.getExtendedQuerySupport().getSqlSelectAttributePosition(em, leftMostQuery.getTypedQueryForFinalOperationBuilder(), expression); + } else { // find out the position by JPQL alias - int position = cbf.getExtendedQuerySupport().getSqlSelectAliasPosition(em, leftMostQuery.getTypedQueryForFinalOperationBuilder(), expression); - orderByElements.add(new DefaultOrderByElement(expression, position, ascending, nullFirst)); - return (X) this; + position = cbf.getExtendedQuerySupport().getSqlSelectAliasPosition(em, leftMostQuery.getTypedQueryForFinalOperationBuilder(), expression); } - int position = cbf.getExtendedQuerySupport().getSqlSelectAttributePosition(em, leftMostQuery.getTypedQueryForFinalOperationBuilder(), expression); - orderByElements.add(new DefaultOrderByElement(expression, position, ascending, nullFirst)); - + orderByElements.add(new DefaultOrderByElement(expression, position, ascending, isNullable(this, expression), nullFirst)); return (X) this; } + + private boolean isNullable(AbstractCommonQueryBuilder queryBuilder, String expression) { + if (queryBuilder instanceof BaseFinalSetOperationBuilderImpl) { + SetOperationManager setOpManager = ((BaseFinalSetOperationBuilderImpl) queryBuilder).setOperationManager; + if (isNullable(setOpManager.getStartQueryBuilder(), expression)) { + return true; + } + for (AbstractCommonQueryBuilder setOp : setOpManager.getSetOperations()) { + if (isNullable(setOp, expression)) { + return true; + } + } + + return false; + } + + AliasInfo aliasInfo = queryBuilder.aliasManager.getAliasInfo(expression); + if (aliasInfo == null) { + List selectInfos = queryBuilder.selectManager.getSelectInfos(); + if (selectInfos.size() > 1) { + throw new IllegalArgumentException("Can't order by an attribute when having multiple select items! Use a select alias!"); + } + JoinNode rootNode; + if (selectInfos.isEmpty()) { + rootNode = queryBuilder.joinManager.getRootNodeOrFail("Can't order by an attribute when having multiple query roots! Use a select alias!"); + } else { + if (!(selectInfos.get(0).get() instanceof PathExpression)) { + throw new IllegalArgumentException("Can't order by an attribute when the select item is a complex expression! Use a select alias!"); + } + rootNode = (JoinNode) ((PathExpression) selectInfos.get(0).get()).getBaseNode(); + } + if (JpaMetamodelUtils.getAttribute(rootNode.getManagedType(), expression) == null) { + throw new IllegalArgumentException("The attribute '" + expression + "' does not exist on the type '" + rootNode.getJavaType().getName() + "'! Did you maybe forget to use a select alias?"); + } + return ExpressionUtils.isNullable(getMetamodel(), new PathExpression( + Arrays.asList(new PropertyExpression(rootNode.getAlias()), new PropertyExpression(expression)), + new SimplePathReference(rootNode, expression, null), + false, + false + )); + } else { + return ExpressionUtils.isNullable(getMetamodel(), ((SelectInfo) aliasInfo).getExpression()); + } + } private AbstractCommonQueryBuilder getLeftMost(AbstractCommonQueryBuilder queryBuilder) { if (queryBuilder instanceof BaseFinalSetOperationBuilderImpl) { @@ -205,11 +253,13 @@ protected void applySetOrderBy(StringBuilder sbSelectFrom) { } else { sbSelectFrom.append(" DESC"); } - - if (elem.isNullsFirst()) { - sbSelectFrom.append(" NULLS FIRST"); - } else { - sbSelectFrom.append(" NULLS LAST"); + + if (elem.isNullable()) { + if (elem.isNullsFirst()) { + sbSelectFrom.append(" NULLS FIRST"); + } else { + sbSelectFrom.append(" NULLS LAST"); + } } } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/CommonQueryBuilderAdapter.java b/core/impl/src/main/java/com/blazebit/persistence/impl/CommonQueryBuilderAdapter.java index 511daf1d7a..1e6b78076a 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/CommonQueryBuilderAdapter.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/CommonQueryBuilderAdapter.java @@ -23,10 +23,10 @@ import javax.persistence.Parameter; import javax.persistence.TemporalType; -import javax.persistence.metamodel.Metamodel; import com.blazebit.persistence.CommonQueryBuilder; import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.parser.EntityMetamodel; import com.blazebit.persistence.spi.JpqlMacro; /** @@ -43,7 +43,7 @@ public CommonQueryBuilderAdapter(AbstractCommonQueryBuilder build } @Override - public Metamodel getMetamodel() { + public EntityMetamodel getMetamodel() { return builder.getMetamodel(); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/DefaultOrderByElement.java b/core/impl/src/main/java/com/blazebit/persistence/impl/DefaultOrderByElement.java index acda0f747e..956643067b 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/DefaultOrderByElement.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/DefaultOrderByElement.java @@ -27,12 +27,14 @@ public class DefaultOrderByElement implements OrderByElement { private final String name; private final int position; private final boolean ascending; + private final boolean nullable; private final boolean nullsFirst; - public DefaultOrderByElement(String name, int position, boolean ascending, boolean nullsFirst) { + public DefaultOrderByElement(String name, int position, boolean ascending, boolean nullable, boolean nullsFirst) { this.name = name; this.position = position; this.ascending = ascending; + this.nullable = nullable; this.nullsFirst = nullsFirst; } @@ -50,6 +52,11 @@ public boolean isAscending() { return ascending; } + @Override + public boolean isNullable() { + return nullable; + } + @Override public boolean isNullsFirst() { return nullsFirst; @@ -65,26 +72,28 @@ public String toString() { } else { sb.append(" DESC"); } - - if (nullsFirst) { - sb.append(" NULLS FIRST"); - } else { - sb.append(" NULLS LAST"); + + if (nullable) { + if (nullsFirst) { + sb.append(" NULLS FIRST"); + } else { + sb.append(" NULLS LAST"); + } } return sb.toString(); } - public static OrderByElement fromString(String string) { - return fromString(string, 0, string.length() - 1); - } - public static OrderByElement fromString(String string, int startIndex, int endIndex) { int spaceIndex = string.indexOf(' ', startIndex); int pos = Integer.parseInt(string.substring(startIndex, spaceIndex)); boolean asc = string.charAt(spaceIndex + 1) == 'A'; - spaceIndex = string.lastIndexOf(' ', endIndex); - boolean nullFirst = string.charAt(spaceIndex + 1) == 'F'; - return new DefaultOrderByElement(null, pos, asc, nullFirst); + int lastSpaceIndex = string.lastIndexOf(' ', endIndex); + if (spaceIndex == lastSpaceIndex) { + return new DefaultOrderByElement(null, pos, asc, false, false); + } else { + boolean nullFirst = string.charAt(lastSpaceIndex + 1) == 'F'; + return new DefaultOrderByElement(null, pos, asc, true, nullFirst); + } } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java index 539b182ab6..0ebc3b43bb 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/EntityMetamodelImpl.java @@ -45,6 +45,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -141,10 +142,17 @@ public EntityMetamodelImpl(EntityManagerFactory emf, JpaProviderFactory jpaProvi for (ManagedType t : managedTypes) { // we already checked all entity types, so skip these if (!(t instanceof EntityType)) { - Set> attributes = (Set>) (Set) t.getAttributes(); - Map> attributeMap = new HashMap<>(attributes.size()); - TemporaryExtendedManagedType extendedManagedType = new TemporaryExtendedManagedType(t, attributeMap); - temporaryExtendedManagedTypes.put(t.getJavaType().getName(), extendedManagedType); + TemporaryExtendedManagedType extendedManagedType = temporaryExtendedManagedTypes.get(t.getJavaType().getName()); + if (extendedManagedType == null) { + Set> attributes = (Set>) (Set) t.getAttributes(); + Map> attributeMap = new HashMap<>(attributes.size()); + for (Attribute attribute : attributes) { + Class attributeType = JpaMetamodelUtils.resolveFieldClass(t.getJavaType(), attribute); + attributeMap.put(attribute.getName(), new AttributeEntry(jpaProvider, t, attribute, attribute.getName(), attributeType, Collections.singletonList(attribute))); + } + extendedManagedType = new TemporaryExtendedManagedType(t, attributeMap); + temporaryExtendedManagedTypes.put(t.getJavaType().getName(), extendedManagedType); + } if (t.getJavaType() != null) { classToType.put(t.getJavaType(), t); } @@ -487,16 +495,16 @@ private TemporaryExtendedManagedType(ManagedType managedType, Map implements ExtendedManagedType { private final ManagedType managedType; private final boolean hasCascadingDeleteCycle; - private final SingularAttribute idAttribute; + private final Set> idAttributes; private final Map> attributes; @SuppressWarnings("unchecked") private ExtendedManagedTypeImpl(ManagedType managedType, boolean hasCascadingDeleteCycle, Map> attributes) { this.managedType = managedType; if (managedType instanceof EntityType) { - this.idAttribute = (SingularAttribute) JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedType); + this.idAttributes = (Set>) (Set) JpaMetamodelUtils.getIdAttributes((IdentifiableType) managedType); } else { - this.idAttribute = null; + this.idAttributes = Collections.emptySet(); } this.hasCascadingDeleteCycle = hasCascadingDeleteCycle; this.attributes = attributes; @@ -514,7 +522,20 @@ public boolean hasCascadingDeleteCycle() { @Override public SingularAttribute getIdAttribute() { - return idAttribute; + Iterator> iterator = idAttributes.iterator(); + if (iterator.hasNext()) { + SingularAttribute idAttribute = iterator.next(); + if (iterator.hasNext()) { + throw new IllegalStateException("Can't access a single id attribute as the entity has multiple id attributes i.e. uses @IdClass!"); + } + return idAttribute; + } + return null; + } + + @Override + public Set> getIdAttributes() { + return idAttributes; } @Override diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/ExpressionUtils.java b/core/impl/src/main/java/com/blazebit/persistence/impl/ExpressionUtils.java index c164ba17ab..ff7cc5f0b5 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/ExpressionUtils.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/ExpressionUtils.java @@ -91,110 +91,8 @@ public Boolean visit(FunctionExpression expression) { private ExpressionUtils() { } - public static boolean isUnique(EntityMetamodel metamodel, Expression expr) { - if (expr instanceof FunctionExpression) { - return isUnique(metamodel, (FunctionExpression) expr); - } else if (expr instanceof PathExpression) { - return isUnique(metamodel, (PathExpression) expr); - } else if (expr instanceof SubqueryExpression) { - // Never assume uniqueness propagates - return false; - } else if (expr instanceof ParameterExpression) { - return false; - } else if (expr instanceof GeneralCaseExpression) { - return isUnique(metamodel, (GeneralCaseExpression) expr); - } else if (expr instanceof ListIndexExpression) { - return false; - } else if (expr instanceof MapKeyExpression) { - return false; - } else if (expr instanceof MapEntryExpression) { - return false; - } else if (expr instanceof MapValueExpression) { - return false; - } else if (expr instanceof EntityLiteral) { - return false; - } else if (expr instanceof EnumLiteral) { - return false; - } else if (expr instanceof NumericLiteral) { - return false; - } else if (expr instanceof BooleanLiteral) { - return false; - } else if (expr instanceof StringLiteral) { - return false; - } else if (expr instanceof TemporalLiteral) { - return false; - } else if (expr instanceof ArithmeticFactor) { - return isUnique(metamodel, ((ArithmeticFactor) expr).getExpression()); - } else if (expr instanceof ArithmeticExpression) { - return false; - } else if (expr instanceof NullExpression) { - // The actual semantics of NULL are, that NULL != NULL - return true; - } else { - throw new IllegalArgumentException("The expression of type '" + expr.getClass().getName() + "' can not be analyzed for uniqueness!"); - } - } - - private static boolean isUnique(EntityMetamodel metamodel, FunctionExpression expr) { - // The existing JPA functions don't return unique results regardless of their arguments - return false; - } - - private static boolean isUnique(EntityMetamodel metamodel, GeneralCaseExpression expr) { - if (expr.getDefaultExpr() != null && !isUnique(metamodel, expr.getDefaultExpr())) { - return false; - } - - List expressions = expr.getWhenClauses(); - int size = expressions.size(); - for (int i = 0; i < size; i++) { - if (!isUnique(metamodel, expressions.get(i).getResult())) { - return false; - } - } - - return true; - } - - private static boolean isUnique(EntityMetamodel metamodel, PathExpression expr) { - JoinNode baseNode = ((JoinNode) expr.getBaseNode()); - Attribute attr; - - if (expr.getField() != null) { - attr = JpaUtils.getAttributeForJoining(metamodel, expr).getAttribute(); - - if (!isUnique(attr)) { - return false; - } - } - - while (baseNode.getParent() != null) { - if (baseNode.getParentTreeNode() == null) { - // Don't assume uniqueness when encountering a cross or entity join - return false; - } else { - attr = baseNode.getParentTreeNode().getAttribute(); - if (!isUnique(attr)) { - return false; - } - baseNode = baseNode.getParent(); - } - } - - return true; - } - - private static boolean isUnique(Attribute attr) { - if (attr.isCollection()) { - return false; - } - - // Right now we only support ids, but we actually should check for unique constraints - return ((SingularAttribute) attr).isId(); - } - /** - * + * * @param stringLiteral A possibly quoted string literal * @return The stringLiteral without quotes */ @@ -216,8 +114,15 @@ public static boolean isNullable(EntityMetamodel metamodel, Expression expr) { } else if (expr instanceof PathExpression) { return isNullable(metamodel, (PathExpression) expr); } else if (expr instanceof SubqueryExpression) { - // Subqueries are always nullable - return true; + // Subqueries are always nullable, unless they use a count query + AbstractCommonQueryBuilder subquery = (AbstractCommonQueryBuilder) ((SubqueryExpression) expr).getSubquery(); + // TODO: Ideally, we would query nullability of aggregate functions instead of relying on this + for (SelectInfo selectInfo : subquery.selectManager.getSelectInfos()) { + if (!com.blazebit.persistence.parser.util.ExpressionUtils.isCountFunction(selectInfo.get())) { + return true; + } + } + return false; } else if (expr instanceof ParameterExpression) { return true; } else if (expr instanceof GeneralCaseExpression) { @@ -276,6 +181,8 @@ private static boolean isNullable(EntityMetamodel metamodel, GeneralCaseExpressi private static boolean isNullable(EntityMetamodel metamodel, FunctionExpression expr) { if ("NULLIF".equalsIgnoreCase(expr.getFunctionName())) { return true; + } else if (com.blazebit.persistence.parser.util.ExpressionUtils.isCountFunction(expr)) { + return false; } else if ("COALESCE".equalsIgnoreCase(expr.getFunctionName())) { boolean nullable; List expressions = expr.getExpressions(); @@ -290,6 +197,7 @@ private static boolean isNullable(EntityMetamodel metamodel, FunctionExpression return true; } else { + // TODO: Ideally, we would query nullability of functions instead of relying on this boolean nullable; List expressions = expr.getExpressions(); int size = expressions.size(); @@ -306,36 +214,23 @@ private static boolean isNullable(EntityMetamodel metamodel, FunctionExpression } private static boolean isNullable(EntityMetamodel metamodel, PathExpression expr) { - JoinNode baseNode = ((JoinNode) expr.getBaseNode()); - Attribute attr; - + // First we check if the target attribute is optional/nullable, because then we don't need to check the join structure if (expr.getField() != null) { - attr = JpaUtils.getAttributeForJoining(metamodel, expr).getAttribute(); + Attribute attr = JpaUtils.getAttributeForJoining(metamodel, expr).getAttribute(); if (isNullable(attr)) { return true; } } - while (baseNode.getParent() != null) { - if (baseNode.getParentTreeNode() == null) { - // This is a cross or entity join - if (baseNode.getParent().getJoinType() != JoinType.LEFT) { - attr = JpaUtils.getAttributeForJoining(metamodel, expr).getAttribute(); - return isNullable(attr); - } - // Any attribute of a left joined relation could be null - return false; - } else { - attr = baseNode.getParentTreeNode().getAttribute(); - if (isNullable(attr)) { - return true; - } - baseNode = baseNode.getParent(); - } - } - - return false; + JoinNode baseNode = ((JoinNode) expr.getBaseNode()); + // If the parent join is an INNER or RIGHT join, this can never produce null + // We also consider CROSS joins or simple root references, which have a joinType of null, to be non-optional + // For simplicity, we simply say that a LEFT join will always produce null + // Since implicit joining would produce inner joins, using LEFT can only be a deliberate decision of the user + // If the user wants to avoid implications of this path being considered nullable, the join should be changed + // Note that a VALUES clause does not adhere to the nullability guarantees + return baseNode.getValueCount() > 0 && baseNode.getValuesCastedParameter() == null || baseNode.getJoinType() == JoinType.LEFT; } private static boolean isNullable(Attribute attr) { @@ -353,7 +248,7 @@ public static FetchType getFetchType(Attribute attr) { if (m instanceof Method) { annotations = AnnotationUtils.getAllAnnotations((Method) m); } else if (m instanceof Field) { - annotations = new HashSet(); + annotations = new HashSet<>(); Collections.addAll(annotations, ((Field) m).getAnnotations()); } else { throw new IllegalStateException("Attribute member [" + attr.getName() + "] is neither field nor method"); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/GroupByManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/GroupByManager.java index 7f9b07ddf8..0b2afc2326 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/GroupByManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/GroupByManager.java @@ -23,7 +23,12 @@ import com.blazebit.persistence.impl.transform.ExpressionModifierVisitor; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; /** @@ -34,15 +39,20 @@ */ public class GroupByManager extends AbstractManager { + private static final String[] EMPTY = new String[0]; + /** * We use an ArrayList since a HashSet causes problems when the path reference in the expression is changed * after it was inserted into the set (e.g. when implicit joining is performed). */ private final List groupByInfos; + // These are the collected group by clauses + private final Map> groupByClauses; GroupByManager(ResolvingQueryGenerator queryGenerator, ParameterManager parameterManager, SubqueryInitiatorFactory subqueryInitFactory) { super(queryGenerator, parameterManager, subqueryInitFactory); - groupByInfos = new ArrayList(); + groupByInfos = new ArrayList<>(); + groupByClauses = new LinkedHashMap<>(); } void applyFrom(GroupByManager groupByManager) { @@ -65,7 +75,7 @@ boolean existsGroupBy(Expression expr) { return groupByInfos.contains(new NodeInfo(expr)); } - void buildGroupByClauses(Set clauses) { + void collectGroupByClauses() { if (groupByInfos.isEmpty()) { return; } @@ -78,16 +88,71 @@ void buildGroupByClauses(Set clauses) { for (NodeInfo info : groupByInfos) { sb.setLength(0); queryGenerator.generate(info.getExpression()); - clauses.add(sb.toString()); + collect(sb.toString(), ClauseType.GROUP_BY); } queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext); queryGenerator.setClauseType(null); } - void buildGroupBy(StringBuilder sb, Set clauses) { - if (!clauses.isEmpty()) { - sb.append(" GROUP BY "); - build(sb, clauses); + void buildGroupBy(StringBuilder sb) { + buildClause(sb, " GROUP BY ", "", EnumSet.noneOf(ClauseType.class), EMPTY, Collections.EMPTY_SET); + } + + void buildGroupBy(StringBuilder sb, Set excludedClauses, String[] additionalGroupBys, String clausePrefix) { + buildClause(sb, " GROUP BY ", clausePrefix, excludedClauses, additionalGroupBys, Collections.EMPTY_SET); + } + + boolean buildClause(StringBuilder sb, String clauseNamePrefix, Set excludedClauses, Collection excludedGroupBys) { + return buildClause(sb, clauseNamePrefix, "", excludedClauses, EMPTY, excludedGroupBys); + } + + boolean buildClause(StringBuilder sb, String clauseNamePrefix, String clausePrefix, Set excludedClauses, String[] additionalGroupBys, Collection excludedGroupBys) { + if (groupByClauses.isEmpty()) { + if (additionalGroupBys.length != 0) { + sb.append(clauseNamePrefix); + for (String additionalGroupBy : additionalGroupBys) { + if (!excludedClauses.contains(additionalGroupBy)) { + sb.append(clausePrefix); + sb.append(additionalGroupBy); + sb.append(", "); + } + } + sb.setLength(sb.length() - 2); + return true; + } + return false; + } else { + int initialIndex = sb.length(); + sb.append(clauseNamePrefix); + int emptyIndex = sb.length(); + for (Map.Entry> entry : groupByClauses.entrySet()) { + if (!excludedGroupBys.contains(entry.getKey()) && !excludedClauses.containsAll(entry.getValue())) { + sb.append(clausePrefix); + sb.append(entry.getKey()); + sb.append(", "); + } + } + + if (additionalGroupBys.length == 0) { + if (sb.length() == emptyIndex) { + sb.setLength(initialIndex); + return false; + } else { + sb.setLength(sb.length() - 2); + return true; + } + } else { + for (String additionalGroupBy : additionalGroupBys) { + Set clauseTypes = groupByClauses.get(additionalGroupBy); + if (!excludedGroupBys.contains(additionalGroupBy) && (clauseTypes == null || excludedClauses.containsAll(clauseTypes))) { + sb.append(clausePrefix); + sb.append(additionalGroupBy); + sb.append(", "); + } + } + sb.setLength(sb.length() - 2); + return true; + } } } @@ -115,6 +180,43 @@ boolean isEmpty() { return groupByInfos.isEmpty(); } + public void resetCollected() { + groupByClauses.clear(); + } + + public void collect(String expression, ClauseType clauseType) { + Set clauseTypes = groupByClauses.get(expression); + if (clauseTypes == null) { + clauseTypes = EnumSet.of(clauseType); + groupByClauses.put(expression, clauseTypes); + } else { + clauseTypes.add(clauseType); + } + } + + public void collect(String expression, Set newClauseTypes) { + Set clauseTypes = groupByClauses.get(expression); + if (clauseTypes == null) { + clauseTypes = EnumSet.copyOf(newClauseTypes); + groupByClauses.put(expression, clauseTypes); + } else { + clauseTypes.addAll(newClauseTypes); + } + } + + public Set getCollectedGroupByClauses() { + return groupByClauses.keySet(); + } + + public boolean hasCollectedGroupByClauses(Set excludedClauses) { + for (Map.Entry> entry : groupByClauses.entrySet()) { + if (!excludedClauses.containsAll(entry.getValue())) { + return true; + } + } + return false; + } + @Override public int hashCode() { int hash = 5; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/HavingManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/HavingManager.java index 05182c0edb..42c708f96e 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/HavingManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/HavingManager.java @@ -52,7 +52,7 @@ HavingOrBuilderImpl havingOr(AbstractCommonQueryBuilder builde return rootPredicate.startBuilder(new HavingOrBuilderImpl((T) builder, rootPredicate, subqueryInitFactory, expressionFactory, parameterManager)); } - void buildGroupByClauses(Set clauses) { + void buildGroupByClauses(GroupByManager groupByManager) { if (rootPredicate.getPredicate().getChildren().isEmpty()) { return; } @@ -66,7 +66,7 @@ void buildGroupByClauses(Set clauses) { if (!extractedGroupByExpressions.isEmpty()) { for (Expression expr : extractedGroupByExpressions) { queryGenerator.generate(expr); - clauses.add(sb.toString()); + groupByManager.collect(sb.toString(), ClauseType.HAVING); sb.setLength(0); } } @@ -75,4 +75,8 @@ void buildGroupByClauses(Set clauses) { queryGenerator.setClauseType(null); groupByExpressionGatheringVisitor.clear(); } + + public boolean isEmpty() { + return rootPredicate.getPredicate().getChildren().isEmpty(); + } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java index 884c6ab19a..3a56bae3a1 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/JoinManager.java @@ -282,7 +282,7 @@ String addRootValues(Class clazz, Class valueClazz, String rootAlias, int Set> attributeSet; if (identifiableReference) { - SingularAttribute idAttribute = JpaMetamodelUtils.getIdAttribute((EntityType) managedType); + SingularAttribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute((EntityType) managedType); idAttributeName = idAttribute.getName(); attributeSet = (Set>) (Set) Collections.singleton(idAttribute); } else { @@ -691,7 +691,7 @@ Set buildClause(StringBuilder sb, Set clauseExclusions, St } String exampleAttributeName = "value"; if (rootNode.getType() instanceof IdentifiableType) { - exampleAttributeName = JpaMetamodelUtils.getIdAttribute(rootNode.getEntityType()).getName(); + exampleAttributeName = JpaMetamodelUtils.getSingleIdAttribute(rootNode.getEntityType()).getName(); } syntheticSubqueryValuesWhereClauseConjuncts.add(rootNode.getAlias() + "." + exampleAttributeName + " IS NULL"); } @@ -1789,6 +1789,11 @@ public Class getJavaType() { return type.getJavaType(); } + @Override + public String toString() { + return getPath(); + } + @Override public int hashCode() { final int prime = 31; @@ -1833,7 +1838,7 @@ private Type getPathType(Type baseType, String expression, PathExpression return JpaUtils.getAttributeForJoining(metamodel, baseType, expressionFactory.createPathExpression(expression), null).getAttributeType(); } catch (IllegalArgumentException ex) { throw new IllegalArgumentException("The join path [" + pathExpression + "] has a non joinable part [" - + expression + "]"); + + expression + "]", ex); } } @@ -2339,7 +2344,7 @@ private JoinResult createOrUpdateNode(JoinNode baseNode, List joinRelati if (implicit) { String aliasToUse = alias == null ? attr.getName() : alias; - alias = aliasManager.generateJoinAlias(aliasToUse); + alias = baseNode.getAliasInfo().getAliasOwner().generateJoinAlias(aliasToUse); } if (joinType == null) { @@ -2352,7 +2357,7 @@ private JoinResult createOrUpdateNode(JoinNode baseNode, List joinRelati return new JoinResult(newNode, null, newNode.getNodeType()); } - private void checkAliasIsAvailable(String alias, String currentJoinPath, String errorMessage) { + private void checkAliasIsAvailable(AliasManager aliasManager, String alias, String currentJoinPath, String errorMessage) { AliasInfo oldAliasInfo = aliasManager.getAliasInfoForBottomLevel(alias); if (oldAliasInfo instanceof SelectInfo) { throw new IllegalStateException("Alias [" + oldAliasInfo.getAlias() + "] already used as select alias"); @@ -2397,7 +2402,8 @@ private JoinNode getOrCreate(JoinNode baseNode, String joinRelationName, Type if (node == null) { // a join node for the join relation does not yet exist - checkAliasIsAvailable(alias, currentJoinPath, errorMessage); + AliasManager aliasManager = baseNode.getAliasInfo().getAliasOwner(); + checkAliasIsAvailable(aliasManager, alias, currentJoinPath, errorMessage); // the alias might have to be postfixed since it might already exist in parent queries if (implicit && aliasManager.getAliasInfo(alias) != null) { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/JpaUtils.java b/core/impl/src/main/java/com/blazebit/persistence/impl/JpaUtils.java index 7ed565972f..a4a47fe372 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/JpaUtils.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/JpaUtils.java @@ -24,7 +24,6 @@ import javax.persistence.metamodel.Attribute; import javax.persistence.metamodel.Type; import java.util.Map; -import java.util.logging.Logger; /** * @@ -33,8 +32,6 @@ */ public final class JpaUtils { - private static final Logger LOG = Logger.getLogger(JpaUtils.class.getName()); - private JpaUtils() { } @@ -60,6 +57,10 @@ public static AttributeHolder getAttributeForJoining(EntityMetamodel metamodel, return getAttributeForJoining(metamodel, baseNode.getNodeType(), expression, baseNodeAlias); } + public static AttributeHolder getAttributeForJoining(EntityMetamodel metamodel, Expression resolvedExpression) { + return getAttributeForJoining(metamodel, null, resolvedExpression, null); + } + public static AttributeHolder getAttributeForJoining(EntityMetamodel metamodel, Type baseNodeType, Expression joinExpression, String baseNodeAlias) { PathTargetResolvingExpressionVisitor visitor = new PathTargetResolvingExpressionVisitor(metamodel, baseNodeType, baseNodeAlias); joinExpression.accept(visitor); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByExpression.java b/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByExpression.java index 16ec50b7a9..f128614c23 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByExpression.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByExpression.java @@ -30,13 +30,15 @@ public final class OrderByExpression { private final Expression expression; private final boolean nullable; private final boolean unique; + private final boolean resultUnique; - public OrderByExpression(boolean ascending, boolean nullFirst, Expression expression, boolean nullable, boolean unique) { + public OrderByExpression(boolean ascending, boolean nullFirst, Expression expression, boolean nullable, boolean unique, boolean resultUnique) { this.ascending = ascending; this.nullFirst = nullFirst; this.expression = expression; this.nullable = nullable; this.unique = unique; + this.resultUnique = resultUnique; } public boolean isAscending() { @@ -63,6 +65,10 @@ public boolean isUnique() { return unique; } + public boolean isResultUnique() { + return resultUnique; + } + @Override public int hashCode() { int hash = 7; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByManager.java index 0782714d1b..abbd5f43bc 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/OrderByManager.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -37,13 +38,19 @@ public class OrderByManager extends AbstractManager { private final GroupByExpressionGatheringVisitor groupByExpressionGatheringVisitor; + private final UniquenessDetectionVisitor uniquenessDetectionVisitor; private final List orderByInfos = new ArrayList(); + private final JoinManager joinManager; private final AliasManager aliasManager; + private final EntityMetamodel metamodel; private final JpaProvider jpaProvider; - OrderByManager(ResolvingQueryGenerator queryGenerator, ParameterManager parameterManager, SubqueryInitiatorFactory subqueryInitFactory, AliasManager aliasManager, JpaProvider jpaProvider, GroupByExpressionGatheringVisitor groupByExpressionGatheringVisitor) { + OrderByManager(ResolvingQueryGenerator queryGenerator, ParameterManager parameterManager, SubqueryInitiatorFactory subqueryInitFactory, JoinManager joinManager, AliasManager aliasManager, EntityMetamodel metamodel, JpaProvider jpaProvider, GroupByExpressionGatheringVisitor groupByExpressionGatheringVisitor) { super(queryGenerator, parameterManager, subqueryInitFactory); + this.joinManager = joinManager; + this.metamodel = metamodel; this.groupByExpressionGatheringVisitor = groupByExpressionGatheringVisitor; + this.uniquenessDetectionVisitor = new UniquenessDetectionVisitor(metamodel); this.aliasManager = aliasManager; this.jpaProvider = jpaProvider; } @@ -76,32 +83,59 @@ public boolean containsOrderBySelectAlias(String alias) { return false; } - List getOrderByExpressions(EntityMetamodel metamodel) { + List getOrderByExpressions(boolean requiresNoCollections, Set groupByClauses) { if (orderByInfos.isEmpty()) { return Collections.emptyList(); } - List realExpressions = new ArrayList(orderByInfos.size()); + Set clausesRequiredForResultUniqueness = groupByClauses.isEmpty() ? null : new HashSet<>(groupByClauses); + List realExpressions = new ArrayList<>(orderByInfos.size()); List infos = orderByInfos; int size = infos.size(); + boolean resultUnique = false; + + uniquenessDetectionVisitor.clear(); for (int i = 0; i < size; i++) { final OrderByInfo orderByInfo = infos.get(i); - AliasInfo aliasInfo = aliasManager.getAliasInfo(orderByInfo.getExpression().toString()); + String expressionString = orderByInfo.getExpression().toString(); + AliasInfo aliasInfo = aliasManager.getAliasInfo(expressionString); Expression expr; - if (aliasInfo != null && aliasInfo instanceof SelectInfo) { + if (aliasInfo instanceof SelectInfo) { SelectInfo selectInfo = (SelectInfo) aliasInfo; expr = selectInfo.getExpression(); + if (clausesRequiredForResultUniqueness != null) { + clausesRequiredForResultUniqueness.remove(expr.toString()); + } } else { expr = orderByInfo.getExpression(); + if (clausesRequiredForResultUniqueness != null) { + clausesRequiredForResultUniqueness.remove(expressionString); + } } - // TODO: This analysis is seriously broken - // In order to give correct results, we actually have to analyze the whole query + // Note that we could actually determine a lot more non-nullable cases, but this requires analyzing the query for top level predicates + // Detecting top-level predicates is out of scope right now and will be done as part of #610 boolean nullable = ExpressionUtils.isNullable(metamodel, expr); - boolean unique = ExpressionUtils.isUnique(metamodel, expr); - realExpressions.add(new OrderByExpression(orderByInfo.ascending, orderByInfo.nullFirst, expr, nullable, unique)); + + // Note that there are actually two notions of uniqueness that we have to check for + // There is a result uniqueness which is relevant for the safety checks we do + // and there is a also the general uniqueness which is what is relevant for keyset pagination + // + // The general uniqueness can be inferred, when a path expression refers to a unique attribute and parent joins are "uniqueness preserving" + // A join node is uniqueness preserving when it is a join of a one-to-one or more generally, when there is a top-level equality predicate between unique keys + // Detecting top-level equality predicates is out of scope right now and will be done as part of #610 + + // Normally, when there are multiple query roots, we can only determine uniqueness when query roots are somehow joined by a unique attributes + // Since that is out of scope now, we require that there must be a single root in order for us to detect uniqueness properly + boolean unique = joinManager.getRoots().size() == 1 + // Determining general uniqueness requires that no collection joins are involved in a query which is kind of guaranteed by design by the PaginatedCriteriaBuilder + && (!requiresNoCollections || !joinManager.hasCollections()) + && uniquenessDetectionVisitor.isUnique(expr); + + resultUnique = resultUnique || unique || uniquenessDetectionVisitor.isResultUnique() || clausesRequiredForResultUniqueness != null && clausesRequiredForResultUniqueness.isEmpty(); + realExpressions.add(new OrderByExpression(orderByInfo.ascending, orderByInfo.nullFirst, expr, nullable, unique, resultUnique)); } return realExpressions; @@ -125,7 +159,7 @@ boolean hasComplexOrderBys() { for (int i = 0; i < size; i++) { final OrderByInfo orderByInfo = infos.get(i); AliasInfo aliasInfo = aliasManager.getAliasInfo(orderByInfo.getExpression().toString()); - if (aliasInfo != null && aliasInfo instanceof SelectInfo) { + if (aliasInfo instanceof SelectInfo) { SelectInfo selectInfo = (SelectInfo) aliasInfo; if (!(selectInfo.getExpression() instanceof PathExpression)) { return true; @@ -189,7 +223,7 @@ void buildSelectClauses(StringBuilder sb, boolean allClauses) { final OrderByInfo orderByInfo = infos.get(i); String potentialSelectAlias = orderByInfo.getExpression().toString(); AliasInfo aliasInfo = aliasManager.getAliasInfo(potentialSelectAlias); - if (aliasInfo != null && aliasInfo instanceof SelectInfo) { + if (aliasInfo instanceof SelectInfo) { SelectInfo selectInfo = (SelectInfo) aliasInfo; if (allClauses || !(selectInfo.getExpression() instanceof PathExpression)) { @@ -212,7 +246,7 @@ void buildSelectClauses(StringBuilder sb, boolean allClauses) { * * @return */ - void buildGroupByClauses(Set clauses) { + void buildGroupByClauses(GroupByManager groupByManager) { if (orderByInfos.isEmpty()) { return; } @@ -229,7 +263,7 @@ void buildGroupByClauses(Set clauses) { AliasInfo aliasInfo = aliasManager.getAliasInfo(potentialSelectAlias); Expression expr; - if (aliasInfo != null && aliasInfo instanceof SelectInfo) { + if (aliasInfo instanceof SelectInfo) { SelectInfo selectInfo = (SelectInfo) aliasInfo; expr = selectInfo.getExpression(); } else { @@ -244,13 +278,13 @@ void buildGroupByClauses(Set clauses) { sb.setLength(0); queryGenerator.generate(expression); if (jpaProvider.supportsNullPrecedenceExpression()) { - clauses.add(sb.toString()); + groupByManager.collect(sb.toString(), ClauseType.ORDER_BY); } else { String expressionString = sb.toString(); sb.setLength(0); jpaProvider.renderNullPrecedence(sb, expressionString, expressionString, null, null); - clauses.add(sb.toString()); + groupByManager.collect(sb.toString(), ClauseType.ORDER_BY); } } queryGenerator.setClauseType(null); @@ -261,12 +295,14 @@ void buildGroupByClauses(Set clauses) { groupByExpressionGatheringVisitor.clear(); } - void buildOrderBy(StringBuilder sb, boolean inverseOrder, boolean resolveSelectAliases) { + void buildOrderBy(StringBuilder sb, boolean inverseOrder, boolean resolveSelectAliases, boolean resolveSimpleSelectAliases) { if (orderByInfos.isEmpty()) { return; } queryGenerator.setClauseType(ClauseType.ORDER_BY); queryGenerator.setQueryBuffer(sb); + boolean originalResolveSelectAliases = queryGenerator.isResolveSelectAliases(); + queryGenerator.setResolveSelectAliases(resolveSelectAliases); sb.append(" ORDER BY "); SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.CASE_WHEN); @@ -278,27 +314,30 @@ void buildOrderBy(StringBuilder sb, boolean inverseOrder, boolean resolveSelectA sb.append(", "); } - applyOrderBy(sb, infos.get(i), inverseOrder, resolveSelectAliases); + applyOrderBy(sb, infos.get(i), inverseOrder, resolveSimpleSelectAliases); } queryGenerator.setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext); queryGenerator.setClauseType(null); + queryGenerator.setResolveSelectAliases(originalResolveSelectAliases); } - private void applyOrderBy(StringBuilder sb, OrderByInfo orderBy, boolean inverseOrder, boolean resolveSelectAliases) { + private void applyOrderBy(StringBuilder sb, OrderByInfo orderBy, boolean inverseOrder, boolean resolveSimpleSelectAliases) { + AliasInfo aliasInfo = aliasManager.getAliasInfo(orderBy.getExpression().toString()); if (jpaProvider.supportsNullPrecedenceExpression()) { queryGenerator.setClauseType(ClauseType.ORDER_BY); queryGenerator.setQueryBuffer(sb); - if (resolveSelectAliases) { - AliasInfo aliasInfo = aliasManager.getAliasInfo(orderBy.getExpression().toString()); - // NOTE: Originally we restricted this to path expressions, but since I don't know the reason for that anymore, we - // removed it - if (aliasInfo != null && aliasInfo instanceof SelectInfo) { - queryGenerator.generate(((SelectInfo) aliasInfo).getExpression()); + boolean nullable; + if (aliasInfo instanceof SelectInfo) { + Expression selectExpression = ((SelectInfo) aliasInfo).getExpression(); + if (resolveSimpleSelectAliases && selectExpression instanceof PathExpression) { + queryGenerator.generate(selectExpression); } else { queryGenerator.generate(orderBy.getExpression()); } + nullable = ExpressionUtils.isNullable(metamodel, selectExpression); } else { queryGenerator.generate(orderBy.getExpression()); + nullable = ExpressionUtils.isNullable(metamodel, orderBy.getExpression()); } if (orderBy.ascending == inverseOrder) { @@ -306,10 +345,14 @@ private void applyOrderBy(StringBuilder sb, OrderByInfo orderBy, boolean inverse } else { sb.append(" ASC"); } - if (orderBy.nullFirst == inverseOrder) { - sb.append(" NULLS LAST"); - } else { - sb.append(" NULLS FIRST"); + // If the expression isn't nullable, we don't need the nulls clause + // This is important for databases that don't support the clause and need emulation + if (nullable) { + if (orderBy.nullFirst == inverseOrder) { + sb.append(" NULLS LAST"); + } else { + sb.append(" NULLS FIRST"); + } } } else { String expression; @@ -317,25 +360,30 @@ private void applyOrderBy(StringBuilder sb, OrderByInfo orderBy, boolean inverse String order; String nulls; StringBuilder expressionSb = new StringBuilder(); + Expression orderExpression = orderBy.getExpression(); queryGenerator.setClauseType(ClauseType.ORDER_BY); queryGenerator.setQueryBuffer(expressionSb); - AliasInfo aliasInfo = aliasManager.getAliasInfo(orderBy.getExpression().toString()); - // NOTE: Originally we restricted this to path expressions, but since I don't know the reason for that anymore, we removed - // it - if (aliasInfo != null && aliasInfo instanceof SelectInfo) { - queryGenerator.generate(((SelectInfo) aliasInfo).getExpression()); + boolean nullable; + if (aliasInfo instanceof SelectInfo) { + Expression selectExpression = ((SelectInfo) aliasInfo).getExpression(); + queryGenerator.generate(selectExpression); resolvedExpression = expressionSb.toString(); + if (resolveSimpleSelectAliases && selectExpression instanceof PathExpression) { + orderExpression = selectExpression; + } + nullable = ExpressionUtils.isNullable(metamodel, selectExpression); } else { resolvedExpression = null; + nullable = ExpressionUtils.isNullable(metamodel, orderExpression); } - if (resolveSelectAliases && resolvedExpression != null) { + if (queryGenerator.isResolveSelectAliases() && resolvedExpression != null) { expression = resolvedExpression; } else { expressionSb.setLength(0); - queryGenerator.generate(orderBy.getExpression()); + queryGenerator.generate(orderExpression); expression = expressionSb.toString(); } @@ -344,10 +392,16 @@ private void applyOrderBy(StringBuilder sb, OrderByInfo orderBy, boolean inverse } else { order = "ASC"; } - if (orderBy.nullFirst == inverseOrder) { - nulls = "LAST"; + if (nullable) { + if (orderBy.nullFirst == inverseOrder) { + nulls = "LAST"; + } else { + nulls = "FIRST"; + } } else { - nulls = "FIRST"; + // If the expression isn't nullable, we don't need the nulls clause + // This is important for databases that don't support the clause and need emulation + nulls = null; } jpaProvider.renderNullPrecedence(sb, expression, resolvedExpression, order, nulls); diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedCriteriaBuilderImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedCriteriaBuilderImpl.java index 17ca389d96..5c05a7defa 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedCriteriaBuilderImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedCriteriaBuilderImpl.java @@ -16,15 +16,21 @@ package com.blazebit.persistence.impl; +import com.blazebit.persistence.CaseWhenStarterBuilder; import com.blazebit.persistence.FullQueryBuilder; +import com.blazebit.persistence.HavingOrBuilder; import com.blazebit.persistence.KeysetPage; +import com.blazebit.persistence.MultipleSubqueryInitiator; import com.blazebit.persistence.ObjectBuilder; import com.blazebit.persistence.PagedList; import com.blazebit.persistence.PaginatedCriteriaBuilder; +import com.blazebit.persistence.RestrictionBuilder; import com.blazebit.persistence.SelectObjectBuilder; +import com.blazebit.persistence.SimpleCaseWhenStarterBuilder; +import com.blazebit.persistence.SubqueryBuilder; +import com.blazebit.persistence.SubqueryInitiator; import com.blazebit.persistence.impl.builder.object.DelegatingKeysetExtractionObjectBuilder; import com.blazebit.persistence.impl.builder.object.KeysetExtractionObjectBuilder; -import com.blazebit.persistence.impl.function.count.AbstractCountFunction; import com.blazebit.persistence.impl.function.pageposition.PagePositionFunction; import com.blazebit.persistence.impl.keyset.KeysetMode; import com.blazebit.persistence.impl.keyset.KeysetPaginationHelper; @@ -35,18 +41,14 @@ import com.blazebit.persistence.impl.query.EntityFunctionNode; import com.blazebit.persistence.impl.query.ObjectBuilderTypedQuery; import com.blazebit.persistence.impl.query.QuerySpecification; -import com.blazebit.persistence.impl.transform.ExpressionTransformerGroup; -import com.blazebit.persistence.parser.util.JpaMetamodelUtils; import javax.persistence.Parameter; import javax.persistence.TypedQuery; -import javax.persistence.metamodel.Attribute; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -66,6 +68,7 @@ public class PaginatedCriteriaBuilderImpl extends AbstractFullQueryBuilder extends AbstractFullQueryBuilder, ?, ?, ?> baseBuilder, boolean keysetExtraction, Object entityId, int pageSize) { + public PaginatedCriteriaBuilderImpl(AbstractFullQueryBuilder, ?, ?, ?> baseBuilder, boolean keysetExtraction, Object entityId, int pageSize, String[] identifierExpressions) { super(baseBuilder); if (pageSize <= 0) { throw new IllegalArgumentException("pageSize may not be zero or negative"); @@ -87,10 +90,11 @@ public PaginatedCriteriaBuilderImpl(AbstractFullQueryBuilder, ?, ?, ?> baseBuilder, boolean keysetExtraction, KeysetPage keysetPage, int firstRow, int pageSize) { + public PaginatedCriteriaBuilderImpl(AbstractFullQueryBuilder, ?, ?, ?> baseBuilder, boolean keysetExtraction, KeysetPage keysetPage, int firstRow, int pageSize, String[] identifierExpressions) { super(baseBuilder); if (firstRow < 0) { throw new IllegalArgumentException("firstRow may not be negative"); @@ -103,6 +107,7 @@ public PaginatedCriteriaBuilderImpl(AbstractFullQueryBuilder TypedQuery getCountQuery(String countQueryString, Class resultType, boolean normalQueryMode, Set keyRestrictedLeftJoins) { if (normalQueryMode && isEmpty(keyRestrictedLeftJoins, EnumSet.of(ClauseType.ORDER_BY, ClauseType.SELECT))) { TypedQuery countQuery = em.createQuery(countQueryString, resultType); @@ -257,6 +267,7 @@ public PaginatedTypedQueryImpl getQuery() { entityId, firstResult, maxResults, + identifierExpressions.length, needsNewIdList, keysetExtraction, keysetMode, @@ -366,11 +377,11 @@ protected void prepareAndCheck() { } applyImplicitJoins(null); - applyExpressionTransformers(); + applyExpressionTransformersAndBuildGroupByClauses(true); // Paginated criteria builders always need the last order by expression to be unique - List orderByExpressions = orderByManager.getOrderByExpressions(mainQuery.metamodel); - if (!orderByExpressions.get(orderByExpressions.size() - 1).isUnique()) { + List orderByExpressions = orderByManager.getOrderByExpressions(false, groupByManager.getCollectedGroupByClauses()); + if (!orderByExpressions.get(orderByExpressions.size() - 1).isResultUnique()) { throw new IllegalStateException("The last order by item must be unique!"); } @@ -378,7 +389,7 @@ protected void prepareAndCheck() { keysetManager.initialize(orderByExpressions); } - needsNewIdList = keysetExtraction || orderByManager.hasComplexOrderBys(); + needsNewIdList = keysetExtraction || identifierExpressions.length > 1 || orderByManager.hasComplexOrderBys(); // No need to do the check again if no mutation occurs needsCheck = false; @@ -527,16 +538,8 @@ protected String buildPageCountQueryString(boolean externalRepresentation) { return sbSelectFrom.toString(); } - private String buildPageCountQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { - JoinNode rootNode = joinManager.getRootNodeOrFail("Paginated criteria builders do not support multiple from clause elements!"); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(mainQuery.metamodel.entity(rootNode.getJavaType())); - String idName = idAttribute.getName(); - StringBuilder idClause = new StringBuilder(100); - rootNode.appendDeReference(idClause, idName); - // Spaces are important to be able to reuse the string builder without copying - String countString = jpaProvider.getCustomFunctionInvocation(AbstractCountFunction.FUNCTION_NAME, 1) + "'DISTINCT'," + idClause + ")"; - sbSelectFrom.append("SELECT ").append(countString); - + @Override + protected void appendPageCountQueryStringExtensions(StringBuilder sbSelectFrom) { if (entityId != null) { parameterManager.addParameterMapping(ENTITY_PAGE_POSITION_PARAMETER_NAME, entityId, ClauseType.SELECT); @@ -550,48 +553,17 @@ private String buildPageCountQueryString(StringBuilder sbSelectFrom, boolean ext sbSelectFrom.append(':').append(ENTITY_PAGE_POSITION_PARAMETER_NAME); sbSelectFrom.append(")"); } - - List whereClauseConjuncts = new ArrayList<>(); - // The count query does not have any fetch owners - Set countNodesToFetch = Collections.emptySet(); - // Collect usage of collection join nodes to optimize away the count distinct - Set collectionJoinNodes = joinManager.buildClause(sbSelectFrom, EnumSet.of(ClauseType.ORDER_BY, ClauseType.SELECT), null, true, externalRepresentation, whereClauseConjuncts, null, explicitVersionEntities, countNodesToFetch); - // TODO: Maybe we can improve this and treat array access joins like non-collection join nodes - boolean hasCollectionJoinUsages = collectionJoinNodes.size() > 0; - - whereManager.buildClause(sbSelectFrom, whereClauseConjuncts, null); - - // Count distinct is obviously unnecessary if we have no collection joins - if (!hasCollectionJoinUsages) { - int idx = sbSelectFrom.indexOf(countString); - int endIdx = idx + countString.length() - 1; - String countStar; - if (jpaProvider.supportsCountStar()) { - countStar = "COUNT(*"; - } else { - countStar = jpaProvider.getCustomFunctionInvocation("count_star", 0); - } - for (int i = idx, j = 0; i < endIdx; i++, j++) { - if (j < countStar.length()) { - sbSelectFrom.setCharAt(i, countStar.charAt(j)); - } else { - sbSelectFrom.setCharAt(i, ' '); - } - } - } - - return sbSelectFrom.toString(); } private String appendSimplePageIdQueryString(StringBuilder sbSelectFrom) { queryGenerator.setAliasPrefix(PAGE_POSITION_ID_QUERY_ALIAS_PREFIX); - - JoinNode rootNode = joinManager.getRootNodeOrFail("Paginated criteria builders do not support multiple from clause elements!"); - String idName = JpaMetamodelUtils.getIdAttribute(mainQuery.metamodel.entity(rootNode.getJavaType())).getName(); - StringBuilder idClause = new StringBuilder(PAGE_POSITION_ID_QUERY_ALIAS_PREFIX); - rootNode.appendDeReference(idClause, idName); - sbSelectFrom.append("SELECT ").append(idClause); + sbSelectFrom.append("SELECT "); + for (String identifierExpression : identifierExpressions) { + sbSelectFrom.append(PAGE_POSITION_ID_QUERY_ALIAS_PREFIX); + sbSelectFrom.append(identifierExpression); + } + // TODO: actually we should add the select clauses needed for order bys // TODO: if we do so, the page position function has to omit select items other than the first @@ -603,13 +575,12 @@ private String appendSimplePageIdQueryString(StringBuilder sbSelectFrom) { boolean inverseOrder = false; - Set clauses = new LinkedHashSet(); - clauses.add(idClause.toString()); - orderByManager.buildGroupByClauses(clauses); - groupByManager.buildGroupBy(sbSelectFrom, clauses); + // TODO: At some point, we will have complex group by items, for that case we will have to properly prefix expressions + groupByManager.buildGroupBy(sbSelectFrom, EnumSet.of(ClauseType.SELECT), identifierExpressions, PAGE_POSITION_ID_QUERY_ALIAS_PREFIX); + havingManager.buildClause(sbSelectFrom); // Resolve select aliases because we might omit the select items - orderByManager.buildOrderBy(sbSelectFrom, inverseOrder, true); + orderByManager.buildOrderBy(sbSelectFrom, inverseOrder, true, false); queryGenerator.setAliasPrefix(null); return sbSelectFrom.toString(); @@ -625,13 +596,13 @@ private String buildPageIdQueryString(boolean externalRepresentation) { } private String buildPageIdQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { - JoinNode rootNode = joinManager.getRootNodeOrFail("Paginated criteria builders do not support multiple from clause elements!"); - String idName = JpaMetamodelUtils.getIdAttribute(mainQuery.metamodel.entity(rootNode.getJavaType())).getName(); - StringBuilder idClause = new StringBuilder(100); - rootNode.appendDeReference(idClause, idName); - // TODO: only append if it does not appear in the order by or it may be included twice - sbSelectFrom.append("SELECT ").append(idClause); + sbSelectFrom.append("SELECT "); + for (String identifierExpression : identifierExpressions) { + sbSelectFrom.append(identifierExpression); + sbSelectFrom.append(", "); + } + sbSelectFrom.setLength(sbSelectFrom.length() - 2); if (needsNewIdList) { orderByManager.buildSelectClauses(sbSelectFrom, keysetExtraction); @@ -662,13 +633,11 @@ private String buildPageIdQueryString(StringBuilder sbSelectFrom, boolean extern boolean inverseOrder = keysetMode == KeysetMode.PREVIOUS; - Set clauses = new LinkedHashSet(); - clauses.add(idClause.toString()); - orderByManager.buildGroupByClauses(clauses); - groupByManager.buildGroupBy(sbSelectFrom, clauses); + groupByManager.buildGroupBy(sbSelectFrom, EnumSet.of(ClauseType.SELECT), identifierExpressions, ""); + havingManager.buildClause(sbSelectFrom); // Resolve select aliases to their actual expressions only if the select items aren't included - orderByManager.buildOrderBy(sbSelectFrom, inverseOrder, !needsNewIdList); + orderByManager.buildOrderBy(sbSelectFrom, inverseOrder, !needsNewIdList, needsNewIdList); // execute illegal collection access check orderByManager.acceptVisitor(new IllegalSubqueryDetector(aliasManager)); @@ -688,9 +657,6 @@ protected String buildBaseQueryString(boolean externalRepresentation) { @Override protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean externalRepresentation) { - JoinNode rootNode = joinManager.getRootNodeOrFail("Paginated criteria builders do not support multiple from clause elements!"); - String idName = JpaMetamodelUtils.getIdAttribute(mainQuery.metamodel.entity(rootNode.getJavaType())).getName(); - selectManager.buildSelect(sbSelectFrom, false); /** @@ -702,48 +668,33 @@ protected void buildBaseQueryString(StringBuilder sbSelectFrom, boolean external List whereClauseConjuncts = new ArrayList<>(); joinManager.buildClause(sbSelectFrom, EnumSet.complementOf(EnumSet.of(ClauseType.SELECT, ClauseType.ORDER_BY)), null, false, externalRepresentation, whereClauseConjuncts, null, explicitVersionEntities, nodesToFetch); sbSelectFrom.append(" WHERE "); - rootNode.appendDeReference(sbSelectFrom, idName); - sbSelectFrom.append(" IN :").append(ID_PARAM_NAME).append(""); - for (String conjunct : whereClauseConjuncts) { - sbSelectFrom.append(" AND "); - sbSelectFrom.append(conjunct); - } - - Set clauses = new LinkedHashSet(); - groupByManager.buildGroupByClauses(clauses); - - int size = transformerGroups.size(); - for (int i = 0; i < size; i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); - clauses.addAll(transformerGroup.getRequiredGroupByClauses()); - } - - if (hasGroupBy) { - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromSelectEnabled()) { - selectManager.buildGroupByClauses(mainQuery.metamodel, clauses); - } - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromHavingEnabled()) { - havingManager.buildGroupByClauses(clauses); - } - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromOrderByEnabled()) { - orderByManager.buildGroupByClauses(clauses); + if (identifierExpressions.length == 1) { + sbSelectFrom.append(identifierExpressions[0]); + sbSelectFrom.append(" IN :").append(ID_PARAM_NAME); + } else { + sbSelectFrom.append('('); + for (int i = 0; i < maxResults; i++) { + for (int j = 0; j < identifierExpressions.length; j++) { + sbSelectFrom.append(identifierExpressions[j]); + sbSelectFrom.append(" = :").append(ID_PARAM_NAME); + sbSelectFrom.append('_').append(j).append('_').append(i); + sbSelectFrom.append(" AND "); + } + sbSelectFrom.setLength(sbSelectFrom.length() - " AND ".length()); + sbSelectFrom.append(" OR "); } + sbSelectFrom.setLength(sbSelectFrom.length() - " OR ".length()); + sbSelectFrom.append(')'); } - if (!clauses.isEmpty()) { - for (int i = 0; i < size; i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); - clauses.addAll(transformerGroup.getOptionalGroupByClauses()); - } + for (String conjunct : whereClauseConjuncts) { + sbSelectFrom.append(" AND "); + sbSelectFrom.append(conjunct); } - groupByManager.buildGroupBy(sbSelectFrom, clauses); - - havingManager.buildClause(sbSelectFrom); - queryGenerator.setResolveSelectAliases(false); - orderByManager.buildOrderBy(sbSelectFrom, false, false); - queryGenerator.setResolveSelectAliases(true); + appendGroupByClause(sbSelectFrom); + orderByManager.buildOrderBy(sbSelectFrom, false, false, false); } private String buildObjectQueryString(boolean externalRepresentation) { @@ -785,38 +736,8 @@ private String buildObjectQueryString(StringBuilder sbSelectFrom, boolean extern boolean inverseOrder = keysetMode == KeysetMode.PREVIOUS; - Set clauses = new LinkedHashSet(); - groupByManager.buildGroupByClauses(clauses); - - int size = transformerGroups.size(); - for (int i = 0; i < size; i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); - clauses.addAll(transformerGroup.getRequiredGroupByClauses()); - } - if (hasGroupBy) { - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromSelectEnabled()) { - selectManager.buildGroupByClauses(mainQuery.metamodel, clauses); - } - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromHavingEnabled()) { - havingManager.buildGroupByClauses(clauses); - } - if (mainQuery.getQueryConfiguration().isImplicitGroupByFromOrderByEnabled()) { - orderByManager.buildGroupByClauses(clauses); - } - } - - if (!clauses.isEmpty()) { - for (int i = 0; i < size; i++) { - ExpressionTransformerGroup transformerGroup = transformerGroups.get(i); - clauses.addAll(transformerGroup.getOptionalGroupByClauses()); - } - } - - groupByManager.buildGroupBy(sbSelectFrom, clauses); - - havingManager.buildClause(sbSelectFrom); - - orderByManager.buildOrderBy(sbSelectFrom, inverseOrder, false); + appendGroupByClause(sbSelectFrom); + orderByManager.buildOrderBy(sbSelectFrom, inverseOrder, false, false); // execute illegal collection access check orderByManager.acceptVisitor(new IllegalSubqueryDetector(aliasManager)); @@ -830,13 +751,78 @@ public PaginatedCriteriaBuilder distinct() { } @Override - public PaginatedCriteriaBuilder groupBy(String... paths) { - throw new IllegalStateException("Calling groupBy() on a PaginatedCriteriaBuilder is not allowed."); + public RestrictionBuilder> having(String expression) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public CaseWhenStarterBuilder>> havingCase() { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SimpleCaseWhenStarterBuilder>> havingSimpleCase(String expression) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public HavingOrBuilder> havingOr() { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryInitiator> havingExists() { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryInitiator> havingNotExists() { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryBuilder> havingExists(FullQueryBuilder criteriaBuilder) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryBuilder> havingNotExists(FullQueryBuilder criteriaBuilder) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryInitiator>> havingSubquery() { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryInitiator>> havingSubquery(String subqueryAlias, String expression) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public MultipleSubqueryInitiator>> havingSubqueries(String expression) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryBuilder>> havingSubquery(FullQueryBuilder criteriaBuilder) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public SubqueryBuilder>> havingSubquery(String subqueryAlias, String expression, FullQueryBuilder criteriaBuilder) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); + } + + @Override + public PaginatedCriteriaBuilder setHavingExpression(String expression) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); } @Override - public PaginatedCriteriaBuilder groupBy(String expression) { - throw new IllegalStateException("Calling groupBy() on a PaginatedCriteriaBuilder is not allowed."); + public MultipleSubqueryInitiator> setHavingExpressionSubqueries(String expression) { + throw new IllegalStateException("Calling having() on a PaginatedCriteriaBuilder is not allowed."); } @Override diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedTypedQueryImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedTypedQueryImpl.java index c30ad98280..c84853d15d 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedTypedQueryImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/PaginatedTypedQueryImpl.java @@ -56,12 +56,13 @@ public class PaginatedTypedQueryImpl implements PaginatedTypedQuery { private int firstResult; private int pageSize; + private final int identifierCount; private final boolean needsNewIdList; private final boolean keysetExtraction; private final KeysetMode keysetMode; private final KeysetPage keysetPage; - public PaginatedTypedQueryImpl(boolean withCount, int highestOffset, TypedQuery countQuery, TypedQuery idQuery, TypedQuery objectQuery, KeysetExtractionObjectBuilder objectBuilder, Set> parameters, Object entityId, int firstResult, int pageSize, boolean needsNewIdList, boolean keysetExtraction, KeysetMode keysetMode, KeysetPage keysetPage) { + public PaginatedTypedQueryImpl(boolean withCount, int highestOffset, TypedQuery countQuery, TypedQuery idQuery, TypedQuery objectQuery, KeysetExtractionObjectBuilder objectBuilder, Set> parameters, Object entityId, int firstResult, int pageSize, int identifierCount, boolean needsNewIdList, boolean keysetExtraction, KeysetMode keysetMode, KeysetPage keysetPage) { this.withCount = withCount; this.highestOffset = highestOffset; this.countQuery = countQuery; @@ -72,6 +73,7 @@ public PaginatedTypedQueryImpl(boolean withCount, int highestOffset, TypedQuery< this.entityId = entityId; this.firstResult = firstResult; this.pageSize = pageSize; + this.identifierCount = identifierCount; this.needsNewIdList = needsNewIdList; this.keysetExtraction = keysetExtraction; this.keysetMode = keysetMode; @@ -193,20 +195,43 @@ private PagedList getResultList(int queryFirstResult, int firstRow, long tota if (needsNewIdList) { if (keysetExtraction) { int keysetPageSize = pageSize - highestOffset; - lowest = KeysetPaginationHelper.extractKey((Object[]) ids.get(0), 1); - highest = KeysetPaginationHelper.extractKey((Object[]) (ids.size() >= keysetPageSize ? ids.get(keysetPageSize - 1) : ids.get(ids.size() - 1)), 1); + lowest = KeysetPaginationHelper.extractKey((Object[]) ids.get(0), identifierCount); + highest = KeysetPaginationHelper.extractKey((Object[]) (ids.size() >= keysetPageSize ? ids.get(keysetPageSize - 1) : ids.get(ids.size() - 1)), identifierCount); } List newIds = new ArrayList(ids.size()); - - for (int i = 0; i < ids.size(); i++) { - newIds.add(((Object[]) ids.get(i))[0]); + if (identifierCount > 1) { + for (int i = 0; i < ids.size(); i++) { + Object[] tuple = (Object[]) ids.get(i); + Object newId = new Object[identifierCount]; + System.arraycopy(tuple, 0, newId, 0, identifierCount); + newIds.add(newId); + } + } else { + for (int i = 0; i < ids.size(); i++) { + newIds.add(((Object[]) ids.get(i))[0]); + } } ids = newIds; } - objectQuery.setParameter(AbstractCommonQueryBuilder.ID_PARAM_NAME, ids); + if (identifierCount > 1) { + objectQuery.setParameter(AbstractCommonQueryBuilder.ID_PARAM_NAME, ids); + StringBuilder parameterNameBuilder = new StringBuilder(AbstractCommonQueryBuilder.ID_PARAM_NAME.length() + 10); + parameterNameBuilder.append(AbstractCommonQueryBuilder.ID_PARAM_NAME).append('_'); + int start = parameterNameBuilder.length(); + for (int i = 0; i < ids.size(); i++) { + Object[] tuple = (Object[]) ids.get(i); + for (int j = 0; j < identifierCount; j++) { + parameterNameBuilder.setLength(start); + parameterNameBuilder.append(j).append('_').append(i); + objectQuery.setParameter(parameterNameBuilder.toString(), tuple[j]); + } + } + } else { + objectQuery.setParameter(AbstractCommonQueryBuilder.ID_PARAM_NAME, ids); + } KeysetPage newKeyset = null; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/ResolvingQueryGenerator.java b/core/impl/src/main/java/com/blazebit/persistence/impl/ResolvingQueryGenerator.java index 9faa77fb55..30f0c46a73 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/ResolvingQueryGenerator.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/ResolvingQueryGenerator.java @@ -542,6 +542,10 @@ public void setResolveSelectAliases(boolean replaceSelectAliases) { this.resolveSelectAliases = replaceSelectAliases; } + public boolean isResolveSelectAliases() { + return resolveSelectAliases; + } + public void setAliasPrefix(String aliasPrefix) { this.aliasPrefix = aliasPrefix; } @@ -652,7 +656,7 @@ private boolean renderAssociationIdIfPossible(Expression expression) { if (!jpaProvider.needsBrokenAssociationToIdRewriteInOnClause() || pathExpression.getBaseNode() != null && pathExpression.getField() != null) { Type pathType = pathExpression.getPathReference().getType(); if (pathType instanceof IdentifiableType) { - String idName = JpaMetamodelUtils.getIdAttribute((IdentifiableType) pathType).getName(); + String idName = JpaMetamodelUtils.getSingleIdAttribute((IdentifiableType) pathType).getName(); sb.append('.'); sb.append(idName); return true; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java b/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java index ec1d6f7cc6..951df04aa6 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/SelectManager.java @@ -200,7 +200,7 @@ X acceptVisitor(ResultVisitor v, X stopValue) { * @param m * @return */ - void buildGroupByClauses(final EntityMetamodel m, Set clauses) { + void buildGroupByClauses(final EntityMetamodel m, GroupByManager groupByManager) { SimpleQueryGenerator.BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = queryGenerator.setBooleanLiteralRenderingContext(SimpleQueryGenerator.BooleanLiteralRenderingContext.CASE_WHEN); StringBuilder sb = new StringBuilder(); @@ -221,7 +221,7 @@ void buildGroupByClauses(final EntityMetamodel m, Set clauses) { for (PathExpression pathExpr : componentPaths) { sb.setLength(0); queryGenerator.generate(pathExpr); - clauses.add(sb.toString()); + groupByManager.collect(sb.toString(), ClauseType.SELECT); } queryGenerator.setClauseType(null); } else { @@ -240,7 +240,7 @@ void buildGroupByClauses(final EntityMetamodel m, Set clauses) { for (PathExpression pathExpr : componentPaths) { sb.setLength(0); queryGenerator.generate(pathExpr); - clauses.add(sb.toString()); + groupByManager.collect(sb.toString(), ClauseType.SELECT); } queryGenerator.setClauseType(null); } else { @@ -251,7 +251,7 @@ void buildGroupByClauses(final EntityMetamodel m, Set clauses) { for (Expression expression : extractedGroupByExpressions) { sb.setLength(0); queryGenerator.generate(expression); - clauses.add(sb.toString()); + groupByManager.collect(sb.toString(), ClauseType.SELECT); } queryGenerator.setClauseType(null); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/SimplePathReference.java b/core/impl/src/main/java/com/blazebit/persistence/impl/SimplePathReference.java index abc86d91e4..0cd967c023 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/SimplePathReference.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/SimplePathReference.java @@ -72,6 +72,11 @@ public Class getJavaType() { return type.getJavaType(); } + @Override + public String toString() { + return getPath(); + } + @Override public int hashCode() { final int prime = 31; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/TransientEntityAssociationParameterTransformerFactory.java b/core/impl/src/main/java/com/blazebit/persistence/impl/TransientEntityAssociationParameterTransformerFactory.java index ca2f879996..098ae18099 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/TransientEntityAssociationParameterTransformerFactory.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/TransientEntityAssociationParameterTransformerFactory.java @@ -39,7 +39,7 @@ public TransientEntityAssociationParameterTransformerFactory(EntityMetamodel met @Override public ParameterValueTransformer getToEntityTranformer(Class entityType) { IdentifiableType managedType = (IdentifiableType) metamodel.getManagedType(entityType); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(managedType); + Attribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute(managedType); return AssociationFromIdParameterTransformer.getInstance(entityType, idAttribute); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/UniquenessDetectionVisitor.java b/core/impl/src/main/java/com/blazebit/persistence/impl/UniquenessDetectionVisitor.java new file mode 100644 index 0000000000..7b15184e8f --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/UniquenessDetectionVisitor.java @@ -0,0 +1,303 @@ +/* + * Copyright 2014 - 2018 Blazebit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.blazebit.persistence.impl; + +import com.blazebit.persistence.parser.EntityMetamodel; +import com.blazebit.persistence.parser.expression.AbortableVisitorAdapter; +import com.blazebit.persistence.parser.expression.ArithmeticExpression; +import com.blazebit.persistence.parser.expression.ArrayExpression; +import com.blazebit.persistence.parser.expression.DateLiteral; +import com.blazebit.persistence.parser.expression.EntityLiteral; +import com.blazebit.persistence.parser.expression.EnumLiteral; +import com.blazebit.persistence.parser.expression.Expression; +import com.blazebit.persistence.parser.expression.FunctionExpression; +import com.blazebit.persistence.parser.expression.GeneralCaseExpression; +import com.blazebit.persistence.parser.expression.ListIndexExpression; +import com.blazebit.persistence.parser.expression.MapEntryExpression; +import com.blazebit.persistence.parser.expression.MapKeyExpression; +import com.blazebit.persistence.parser.expression.MapValueExpression; +import com.blazebit.persistence.parser.expression.NullExpression; +import com.blazebit.persistence.parser.expression.NumericLiteral; +import com.blazebit.persistence.parser.expression.ParameterExpression; +import com.blazebit.persistence.parser.expression.PathExpression; +import com.blazebit.persistence.parser.expression.PathReference; +import com.blazebit.persistence.parser.expression.SimpleCaseExpression; +import com.blazebit.persistence.parser.expression.StringLiteral; +import com.blazebit.persistence.parser.expression.SubqueryExpression; +import com.blazebit.persistence.parser.expression.TimeLiteral; +import com.blazebit.persistence.parser.expression.TimestampLiteral; +import com.blazebit.persistence.parser.expression.TrimExpression; +import com.blazebit.persistence.parser.expression.TypeFunctionExpression; +import com.blazebit.persistence.parser.expression.WhenClauseExpression; +import com.blazebit.persistence.parser.predicate.BooleanLiteral; +import com.blazebit.persistence.spi.ExtendedManagedType; + +import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.SingularAttribute; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * + * @author Christian Beikov + * @since 1.3.0 + */ +class UniquenessDetectionVisitor extends AbortableVisitorAdapter { + + private final EntityMetamodel metamodel; + private final Map> orderedPaths; + private boolean resultUnique; + + public UniquenessDetectionVisitor(EntityMetamodel metamodel) { + this.metamodel = metamodel; + this.orderedPaths = new HashMap<>(); + } + + public void clear() { + orderedPaths.clear(); + resultUnique = false; + } + + public boolean isUnique(Expression expression) { + boolean unique = expression.accept(this); + resultUnique = resultUnique || unique; + return unique; + } + + public boolean isResultUnique() { + // TODO: Determine if the order by expressions are a super-set of the group by clause which makes it result unique + return resultUnique; + } + + @Override + public Boolean visit(PathExpression expr) { + if (expr.getField() == null) { + throw new IllegalArgumentException("Ordering by association '" + expr + "' does not make sense! Please order by it's id instead!"); + } + + // First we check if the target attribute is unique, if it isn't, we don't need to check the join structure + PathReference pathReference = expr.getPathReference(); + JoinNode baseNode = (JoinNode) pathReference.getBaseNode(); + ExtendedManagedType managedType = metamodel.getManagedType(ExtendedManagedType.class, baseNode.getJavaType()); + Attribute attr = managedType.getAttribute(pathReference.getField()).getAttribute(); + + // Right now we only support ids, but we actually should check for unique constraints + if (!((SingularAttribute) attr).isId()) { + return false; + } + + // First we initialize the names of the id attributes as set for the join node + Set orderedAttributes = orderedPaths.get(baseNode); + if (orderedAttributes == null) { + orderedAttributes = new HashSet<>(); + for (SingularAttribute singularAttribute : managedType.getIdAttributes()) { + orderedAttributes.add(singularAttribute.getName()); + } + orderedPaths.put(baseNode, orderedAttributes); + } + + // We remove for every id attribute from the initialized set of id attribute names + orderedAttributes.remove(attr.getName()); + // While there still are some attribute names left, we simply report that it isn't unique, yet + if (!orderedAttributes.isEmpty()) { + return false; + } + + // But even now that we order by all id attribute parts, we still have to make sure this join node is uniqueness preserving + + while (baseNode.getParent() != null) { + if (baseNode.getParentTreeNode() == null) { + // Don't assume uniqueness when encountering a cross or entity join + // To support this, we need to find top-level equality predicates between unique keys of the joined relations in the query + return false; + } else { + attr = baseNode.getParentTreeNode().getAttribute(); + // Only one-to-one relation joins i.e. joins having a unique key with unique key equality predicate are uniqueness preserving + + // TODO: With #610 we could actually also support ManyToOne relations here if there is a unique key with constant equality predicate for the parent node which is a root node + // This is relevant for queries like `select e.manyToOne.id, e.manyToOne.name from Entity e order by e.manyToOne.id` + // Normally, the expression `e.manyToOne.id` wouldn't be considered unique, unless there is a unique key with constant equality predicate + // i.e. a predicate like `e.id = 1` that essentially "constantifies" the parent join node + if (attr.getPersistentAttributeType() != Attribute.PersistentAttributeType.ONE_TO_ONE) { + return false; + } + baseNode = baseNode.getParent(); + } + } + + return true; + } + + @Override + public Boolean visit(ArrayExpression expression) { + return false; + } + + @Override + public Boolean visit(NullExpression expression) { + // The actual semantics of NULL are, that NULL != NULL + return true; + } + + @Override + public Boolean visit(FunctionExpression expression) { + switch (expression.getFunctionName().toUpperCase()) { + case "COALESCE": { + List expressions = expression.getExpressions(); + int size = expressions.size(); + for (int i = 0; i < size; i++) { + if (!expressions.get(i).accept(this)) { + return false; + } + } + + return true; + } + case "NULLIF": + // See visit(NullExpression) for reasoning + return expression.getExpressions().get(0).accept(this); + // MIN and MAX are special aggregate functions that preserve uniqueness + case "MIN": + case "MAX": { + Expression expr = expression.getExpressions().get(0); + return expr instanceof PathExpression && visit((PathExpression) expr); + } + default: + // The existing JPA functions don't return unique results regardless of their arguments + return false; + } + } + + @Override + public Boolean visit(GeneralCaseExpression expression) { + List expressions = expression.getWhenClauses(); + int size = expressions.size(); + for (int i = 0; i < size; i++) { + if (!expressions.get(i).accept(this)) { + return false; + } + } + + if (expression.getDefaultExpr() != null) { + return expression.getDefaultExpr().accept(this); + } + + // See visit(NullExpression) for reasoning + return true; + } + + @Override + public Boolean visit(SimpleCaseExpression expression) { + return visit((GeneralCaseExpression) expression); + } + + @Override + public Boolean visit(WhenClauseExpression expression) { + return expression.getResult().accept(this); + } + + /* Other expressions can never be detected to be unique */ + + @Override + public Boolean visit(ParameterExpression expression) { + return false; + } + + @Override + public Boolean visit(ListIndexExpression expression) { + return false; + } + + @Override + public Boolean visit(MapEntryExpression expression) { + return false; + } + + @Override + public Boolean visit(MapKeyExpression expression) { + return false; + } + + @Override + public Boolean visit(MapValueExpression expression) { + return false; + } + + @Override + public Boolean visit(SubqueryExpression expression) { + return false; + } + + @Override + public Boolean visit(TypeFunctionExpression expression) { + return false; + } + + @Override + public Boolean visit(TrimExpression expression) { + return false; + } + + @Override + public Boolean visit(ArithmeticExpression expression) { + return false; + } + + @Override + public Boolean visit(NumericLiteral expression) { + return false; + } + + @Override + public Boolean visit(BooleanLiteral expression) { + return false; + } + + @Override + public Boolean visit(StringLiteral expression) { + return false; + } + + @Override + public Boolean visit(DateLiteral expression) { + return false; + } + + @Override + public Boolean visit(TimeLiteral expression) { + return false; + } + + @Override + public Boolean visit(TimestampLiteral expression) { + return false; + } + + @Override + public Boolean visit(EnumLiteral expression) { + return false; + } + + @Override + public Boolean visit(EntityLiteral expression) { + return false; + } + +} \ No newline at end of file diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DB2DbmsDialect.java b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DB2DbmsDialect.java index a466c15ab1..7547a618de 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DB2DbmsDialect.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DB2DbmsDialect.java @@ -228,7 +228,7 @@ protected boolean isCompatibilityVectorMYS() { @Override protected void appendOrderByElement(StringBuilder sqlSb, OrderByElement element, String[] aliases) { - if ((element.isNullsFirst() && !element.isAscending()) || (!element.isNullsFirst() && element.isAscending())) { + if (!element.isNullable() || (element.isNullsFirst() && !element.isAscending()) || (!element.isNullsFirst() && element.isAscending())) { // The following are ok according to DB2 docs // ASC + NULLS LAST // DESC + NULLS FIRST diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DefaultDbmsDialect.java b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DefaultDbmsDialect.java index c4fa9e77d9..71c11ca326 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DefaultDbmsDialect.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/DefaultDbmsDialect.java @@ -342,10 +342,12 @@ protected void appendOrderByElement(StringBuilder sqlSb, OrderByElement element, } else { sqlSb.append(" desc"); } - if (element.isNullsFirst()) { - sqlSb.append(" nulls first"); - } else { - sqlSb.append(" nulls last"); + if (element.isNullable()) { + if (element.isNullsFirst()) { + sqlSb.append(" nulls first"); + } else { + sqlSb.append(" nulls last"); + } } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MSSQLDbmsDialect.java b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MSSQLDbmsDialect.java index 44c4c77ad5..19ccb7c06b 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MSSQLDbmsDialect.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MSSQLDbmsDialect.java @@ -154,7 +154,7 @@ protected boolean needsAliasInSetOrderBy() { @Override protected void appendOrderByElement(StringBuilder sqlSb, OrderByElement element, String[] aliases) { - if ((element.isAscending() && element.isNullsFirst()) || (!element.isAscending() && !element.isNullsFirst())) { + if (!element.isNullable() || (element.isAscending() && element.isNullsFirst()) || (!element.isAscending() && !element.isNullsFirst())) { // The following are the defaults, so just let them through // ASC + NULLS FIRST // DESC + NULLS LAST diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java index 2eaf367d6e..b1101ae278 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/dialect/MySQLDbmsDialect.java @@ -97,7 +97,11 @@ public DbmsLimitHandler createLimitHandler() { @Override protected void appendOrderByElement(StringBuilder sqlSb, OrderByElement element, String[] aliases) { - appendEmulatedOrderByElementWithNulls(sqlSb, element, aliases); + if (!element.isNullable()) { + super.appendOrderByElement(sqlSb, element, aliases); + } else { + appendEmulatedOrderByElementWithNulls(sqlSb, element, aliases); + } } @Override diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/ExpressionTransformerGroup.java b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/ExpressionTransformerGroup.java index 43f03da68f..dd5df8dd18 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/ExpressionTransformerGroup.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/ExpressionTransformerGroup.java @@ -19,8 +19,6 @@ import com.blazebit.persistence.impl.AbstractManager; import com.blazebit.persistence.parser.expression.modifier.ExpressionModifier; -import java.util.Set; - /** * * @author Christian Beikov @@ -31,9 +29,8 @@ public interface ExpressionTransformerGroup { void applyExpressionTransformer(AbstractManager manager); - void afterGlobalTransformation(); + void afterTransformationGroup(); - Set getRequiredGroupByClauses(); + void afterAllTransformations(); - Set getOptionalGroupByClauses(); } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SimpleTransformerGroup.java b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SimpleTransformerGroup.java index 0923bdfe8f..75e9a02cf4 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SimpleTransformerGroup.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SimpleTransformerGroup.java @@ -19,9 +19,6 @@ import com.blazebit.persistence.impl.AbstractManager; import com.blazebit.persistence.parser.expression.modifier.ExpressionModifier; -import java.util.Collections; -import java.util.Set; - /** * * @author Christian Beikov @@ -41,16 +38,10 @@ public void applyExpressionTransformer(AbstractManager getRequiredGroupByClauses() { - return Collections.emptySet(); + public void afterTransformationGroup() { } @Override - public Set getOptionalGroupByClauses() { - return Collections.emptySet(); + public void afterAllTransformations() { } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformationVisitor.java b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformationVisitor.java index bd6f56cea1..39d1d95e25 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformationVisitor.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformationVisitor.java @@ -49,6 +49,7 @@ import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.PluralAttribute; +import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.Type; import javax.persistence.metamodel.Type.PersistenceType; import java.util.*; @@ -76,8 +77,8 @@ public class SizeTransformationVisitor extends ExpressionModifierCollectingResul private final Set transformedExpressions = new HashSet(); // maps absolute paths to late join entries private final Map lateJoins = new HashMap(); - private final Set requiredGroupBys = new LinkedHashSet<>(); - private final Set subqueryGroupBys = new LinkedHashSet<>(); + private final Map> requiredGroupBys = new LinkedHashMap<>(); + private final Map> subqueryGroupBys = new LinkedHashMap<>(); private JoinNode currentJoinNode; // size expressions with arguments having a blacklisted base node will become subqueries private Set joinNodeBlacklist = new HashSet<>(); @@ -107,11 +108,11 @@ public Map getLateJoins() { return lateJoins; } - public Set getRequiredGroupBys() { + public Map> getRequiredGroupBys() { return requiredGroupBys; } - public Set getSubqueryGroupBys() { + public Map> getSubqueryGroupBys() { return subqueryGroupBys; } @@ -200,12 +201,14 @@ private Expression getSizeExpression(ExpressionModifier parentModifier, PathExpr } // build group by id clause - List pathElementExpr = new ArrayList(); - String rootId = JpaMetamodelUtils.getIdAttribute(startType).getName(); - pathElementExpr.add(new PropertyExpression(sizeArgJoin.getAlias())); - pathElementExpr.add(new PropertyExpression(rootId)); - PathExpression groupByExpr = new PathExpression(pathElementExpr); - String groupByExprString = groupByExpr.toString(); + List groupByExprs = new ArrayList<>(); + for (SingularAttribute idAttribute : JpaMetamodelUtils.getIdAttributes(startType)) { + List pathElementExpr = new ArrayList<>(2); + pathElementExpr.add(new PropertyExpression(sizeArgJoin.getAlias())); + pathElementExpr.add(new PropertyExpression(idAttribute.getName())); + PathExpression groupByExpr = new PathExpression(pathElementExpr); + groupByExprs.add(groupByExpr); + } subqueryRequired = subqueryRequired || // we could also generate counts for collections with IdClass attributes but we do not implement this for now @@ -246,48 +249,54 @@ private Expression getSizeExpression(ExpressionModifier parentModifier, PathExpr } } - joinManager.implicitJoin(groupByExpr, true, null, null, null, false, false, false, false); + for (PathExpression groupByExpr : groupByExprs) { + joinManager.implicitJoin(groupByExpr, true, null, null, null, false, false, false, false); + } - PathExpression originalSizeArg = (PathExpression) sizeArg.clone(false); + PathExpression originalSizeArg = sizeArg.clone(false); originalSizeArg.setPathReference(sizeArg.getPathReference()); sizeArg.setUsedInCollectionFunction(false); List countArguments = new ArrayList(); - Expression keyExpression; + String joinLookupKey = getJoinLookupKey(sizeArg); + LateJoinEntry lateJoin = lateJoins.get(joinLookupKey); + if (lateJoin == null) { + lateJoin = new LateJoinEntry(); + lateJoins.put(joinLookupKey, lateJoin); + } + lateJoin.getExpressionsToJoin().add(sizeArg); + lateJoin.getClauseDependencies().add(clause); + if ((isElementCollection && collectionType != PluralAttribute.CollectionType.MAP) || collectionType == PluralAttribute.CollectionType.SET) { if (IDENTIFIABLE_PERSISTENCE_TYPES.contains(targetAttribute.getElementType().getPersistenceType()) && targetAttribute.isCollection()) { // append id attribute name of joinable size argument PluralAttribute sizeArgTargetAttribute = (PluralAttribute) JpaMetamodelUtils.getAttribute(startType, sizeArg.getPathReference().getField()); - Attribute idAttribute = JpaMetamodelUtils.getIdAttribute(((IdentifiableType) sizeArgTargetAttribute.getElementType())); - sizeArg.getExpressions().add(new PropertyExpression(idAttribute.getName())); + for (Attribute idAttribute : JpaMetamodelUtils.getIdAttributes(((IdentifiableType) sizeArgTargetAttribute.getElementType()))) { + List pathElementExpressions = new ArrayList<>(sizeArg.getExpressions().size() + 1); + pathElementExpressions.addAll(sizeArg.getExpressions()); + pathElementExpressions.add(new PropertyExpression(idAttribute.getName())); + PathExpression pathExpression = new PathExpression(pathElementExpressions); + countArguments.add(pathExpression); + lateJoin.getExpressionsToJoin().add(pathExpression); + } + } else { + countArguments.add(sizeArg); } - - keyExpression = sizeArg; } else { sizeArg.setCollectionKeyPath(true); if (collectionType == PluralAttribute.CollectionType.LIST) { - keyExpression = new ListIndexExpression(sizeArg); + countArguments.add(new ListIndexExpression(sizeArg)); } else { - keyExpression = new MapKeyExpression(sizeArg); + countArguments.add(new MapKeyExpression(sizeArg)); } } - countArguments.add(keyExpression); AggregateExpression countExpr = createCountFunction(distinctRequired, countArguments); transformedExpressions.add(new TransformedExpressionEntry(countExpr, originalSizeArg, parentModifier, aggregateFunctionContext)); - String joinLookupKey = getJoinLookupKey(sizeArg); - LateJoinEntry lateJoin = lateJoins.get(joinLookupKey); - if (lateJoin == null) { - lateJoin = new LateJoinEntry(); - lateJoins.put(joinLookupKey, lateJoin); - } - lateJoin.getPathsToJoin().add(sizeArg); - lateJoin.getClauseDependencies().add(clause); - currentJoinNode = (JoinNode) originalSizeArg.getBaseNode(); if (!distinctRequired) { @@ -312,7 +321,15 @@ private Expression getSizeExpression(ExpressionModifier parentModifier, PathExpr } } - requiredGroupBys.add(groupByExprString); + for (Expression groupByExpr : groupByExprs) { + String groupByExprString = groupByExpr.toString(); + Set clauseTypes = requiredGroupBys.get(groupByExprString); + if (clauseTypes == null) { + requiredGroupBys.put(groupByExprString, EnumSet.of(clause)); + } else { + clauseTypes.add(clause); + } + } return countExpr; } @@ -339,18 +356,20 @@ private SubqueryExpression generateSubquery(PathExpression sizeArg) { } final EntityType startType = (EntityType) nodeType; - Subquery countSubquery = (Subquery) subqueryInitFactory.createSubqueryInitiator(null, new SubqueryBuilderListenerImpl(), false) - .from(sizeArg.clone(true).toString()) + Subquery countSubquery = (Subquery) subqueryInitFactory.createSubqueryInitiator(null, new SubqueryBuilderListenerImpl<>(), false) + .from(sizeArg.getPathReference().toString()) .select("COUNT(*)"); - List pathElementExpr = new ArrayList(); - String rootId = JpaMetamodelUtils.getIdAttribute(startType).getName(); - pathElementExpr.add(new PropertyExpression(sizeArgJoin.getAlias())); - pathElementExpr.add(new PropertyExpression(rootId)); - PathExpression groupByExpr = new PathExpression(pathElementExpr); - String groupByExprString = groupByExpr.toString(); + for (SingularAttribute idAttribute : JpaMetamodelUtils.getIdAttributes(startType)) { + String groupByExprString = sizeArgJoin.getAlias() + "." + idAttribute.getName(); - subqueryGroupBys.add(groupByExprString); + Set clauseTypes = subqueryGroupBys.get(groupByExprString); + if (clauseTypes == null) { + subqueryGroupBys.put(groupByExprString, EnumSet.of(clause)); + } else { + clauseTypes.add(clause); + } + } return new SubqueryExpression(countSubquery); } @@ -407,14 +426,14 @@ public boolean isAggregateFunctionContext() { */ static class LateJoinEntry { private final EnumSet clauseDependencies = EnumSet.noneOf(ClauseType.class); - private final List pathsToJoin = new ArrayList(); + private final List expressionsToJoin = new ArrayList<>(); public EnumSet getClauseDependencies() { return clauseDependencies; } - public List getPathsToJoin() { - return pathsToJoin; + public List getExpressionsToJoin() { + return expressionsToJoin; } } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformerGroup.java b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformerGroup.java index 0859a1b865..fa2c39d437 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformerGroup.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SizeTransformerGroup.java @@ -18,14 +18,12 @@ import com.blazebit.persistence.impl.AbstractManager; import com.blazebit.persistence.impl.ClauseType; +import com.blazebit.persistence.impl.GroupByManager; import com.blazebit.persistence.impl.JoinManager; -import com.blazebit.persistence.impl.JoinNode; import com.blazebit.persistence.impl.OrderByManager; import com.blazebit.persistence.impl.SelectInfo; import com.blazebit.persistence.impl.SelectManager; -import com.blazebit.persistence.parser.expression.PathExpression; -import com.blazebit.persistence.parser.expression.PathReference; -import com.blazebit.persistence.impl.SimplePathReference; +import com.blazebit.persistence.parser.expression.Expression; import com.blazebit.persistence.parser.expression.modifier.ExpressionModifier; import java.util.*; @@ -41,14 +39,16 @@ public class SizeTransformerGroup implements ExpressionTransformerGroup selectManager; private final JoinManager joinManager; + private final GroupByManager groupByManager; private final SizeExpressionTransformer sizeExpressionTransformer; private final SizeSelectInfoTransformer sizeSelectExpressionTransformer; - public SizeTransformerGroup(SizeTransformationVisitor sizeTransformationVisitor, OrderByManager orderByManager, SelectManager selectManager, JoinManager joinManager) { + public SizeTransformerGroup(SizeTransformationVisitor sizeTransformationVisitor, OrderByManager orderByManager, SelectManager selectManager, JoinManager joinManager, GroupByManager groupByManager) { this.sizeTransformationVisitor = sizeTransformationVisitor; this.selectManager = selectManager; this.joinManager = joinManager; this.sizeExpressionTransformer = new SizeExpressionTransformer(sizeTransformationVisitor); + this.groupByManager = groupByManager; this.sizeSelectExpressionTransformer = new SizeSelectInfoTransformer(sizeTransformationVisitor, orderByManager); } @@ -73,27 +73,25 @@ public void applyExpressionTransformer(AbstractManager getRequiredGroupByClauses() { - return sizeTransformationVisitor.getRequiredGroupBys(); + for (Map.Entry> entry : sizeTransformationVisitor.getRequiredGroupBys().entrySet()) { + groupByManager.collect(entry.getKey(), entry.getValue()); + } } @Override - public Set getOptionalGroupByClauses() { - return sizeTransformationVisitor.getSubqueryGroupBys(); + public void afterAllTransformations() { + for (Map.Entry> entry : sizeTransformationVisitor.getSubqueryGroupBys().entrySet()) { + groupByManager.collect(entry.getKey(), entry.getValue()); + } } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SubqueryRecursiveExpressionVisitor.java b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SubqueryRecursiveExpressionVisitor.java index 74fa5033c4..eec4f6e523 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SubqueryRecursiveExpressionVisitor.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/transform/SubqueryRecursiveExpressionVisitor.java @@ -37,7 +37,7 @@ public void visit(ExpressionModifier expressionModifier, ClauseType clauseType) @Override public void visit(SubqueryExpression expression) { // TODO: this is ugly - ((AbstractCommonQueryBuilder) expression.getSubquery()).applyExpressionTransformers(); + ((AbstractCommonQueryBuilder) expression.getSubquery()).applyExpressionTransformersAndBuildGroupByClauses(false); } } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/PathTargetResolvingExpressionVisitor.java b/core/parser/src/main/java/com/blazebit/persistence/parser/PathTargetResolvingExpressionVisitor.java index 5b3121a280..7118f58334 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/PathTargetResolvingExpressionVisitor.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/PathTargetResolvingExpressionVisitor.java @@ -62,6 +62,7 @@ import com.blazebit.reflection.ReflectionUtils; import javax.persistence.metamodel.Attribute; +import javax.persistence.metamodel.BasicType; import javax.persistence.metamodel.EntityType; import javax.persistence.metamodel.ListAttribute; import javax.persistence.metamodel.ManagedType; @@ -198,7 +199,10 @@ private Type getType(Type baseType, Attribute attribute) { @Override public void visit(PropertyExpression expression) { String property = expression.getProperty(); - Attribute attribute = ((ManagedType) currentPosition.getCurrentType()).getAttribute(property); + if (currentPosition.getCurrentType() instanceof BasicType) { + throw new IllegalArgumentException("Can't access property '" + property + "' on basic type '" + JpaMetamodelUtils.getTypeName(currentPosition.getCurrentType()) + "'. Did you forget to add the embeddable type to your persistence.xml?"); + } + Attribute attribute = JpaMetamodelUtils.getAttribute((ManagedType) currentPosition.getCurrentType(), property); // Older Hibernate versions did not throw an exception but returned null instead if (attribute == null) { throw new IllegalArgumentException("Attribute '" + property + "' not found on type '" + JpaMetamodelUtils.getTypeName(currentPosition.getCurrentType()) + "'"); @@ -276,6 +280,10 @@ public void visit(GeneralCaseExpression expression) { @Override public void visit(PathExpression expression) { + if (currentPosition.getCurrentType() == null) { + currentPosition.setCurrentType(expression.getPathReference().getType()); + return; + } List expressions = expression.getExpressions(); int size = expressions.size(); int i = 0; diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/PathExpression.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/PathExpression.java index e6451d9182..9e015c888a 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/PathExpression.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/PathExpression.java @@ -53,9 +53,9 @@ public PathExpression(List pathProperties, PathReference } @Override - public Expression clone(boolean resolved) { + public PathExpression clone(boolean resolved) { if (resolved && pathReference != null) { - return pathReference.getBaseNode().createExpression(pathReference.getField()); + return (PathExpression) pathReference.getBaseNode().createExpression(pathReference.getField()); } int size = pathProperties.size(); diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/util/ExpressionUtils.java b/core/parser/src/main/java/com/blazebit/persistence/parser/util/ExpressionUtils.java index 5742668de2..b0e859fc07 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/util/ExpressionUtils.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/util/ExpressionUtils.java @@ -47,4 +47,14 @@ public static boolean isCustomFunctionInvocation(FunctionExpression e) { return "FUNCTION".equalsIgnoreCase(e.getFunctionName()); } + public static boolean isCountFunction(Expression expression) { + if (expression instanceof FunctionExpression) { + return isCountFunction((FunctionExpression) expression); + } + return false; + } + + public static boolean isCountFunction(FunctionExpression expr) { + return "COUNT".equalsIgnoreCase(expr.getFunctionName()); + } } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/util/JpaMetamodelUtils.java b/core/parser/src/main/java/com/blazebit/persistence/parser/util/JpaMetamodelUtils.java index c832c8c45d..87b9615165 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/util/JpaMetamodelUtils.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/util/JpaMetamodelUtils.java @@ -38,7 +38,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; /** * @author Christian Beikov @@ -226,20 +229,41 @@ public static Class resolveFieldClass(Class baseClass, Attribute att } } - public static SingularAttribute getIdAttribute(IdentifiableType entityType) { + public static SingularAttribute getSingleIdAttribute(IdentifiableType entityType) { + Iterator> iterator = getIdAttributes(entityType).iterator(); + + if (!iterator.hasNext()) { + return null; + } + + SingularAttribute next = iterator.next(); + + if (iterator.hasNext()) { + throw new IllegalStateException("Can't access a single id attribute as the entity has multiple id attributes i.e. uses @IdClass!"); + } + + return next; + } + + public static Set> getIdAttributes(IdentifiableType entityType) { Class idClass = null; + Set> idTypes = new LinkedHashSet<>(); try { Type idType = entityType.getIdType(); if (idType == null) { // Hibernate treats ManyToOne's mapped as @Id differently, we need to scan the type and look for the id.. for (SingularAttribute attribute : entityType.getSingularAttributes()) { if (attribute.isId()) { - return attribute; + idTypes.add(attribute); } } } - idClass = idType.getJavaType(); - return entityType.getId(idClass); + if (idTypes.isEmpty()) { + idClass = idType.getJavaType(); + SingularAttribute id = entityType.getId(idClass); + idTypes.add(id); + } + return idTypes; } catch (IllegalArgumentException e) { /** * Eclipselink returns wrapper types from entityType.getIdType().getJavaType() even if the id type @@ -250,22 +274,30 @@ public static Class resolveFieldClass(Class baseClass, Attribute att if (idClass != null) { final Class primitiveIdClass = ReflectionUtils.getPrimitiveClassOfWrapper(idClass); if (primitiveIdClass != null) { - return entityType.getId(primitiveIdClass); + idTypes.add(entityType.getId(primitiveIdClass)); } } - throw e; + if (idTypes.isEmpty()) { + throw e; + } else { + return idTypes; + } } catch (IllegalStateException e) { // Hibernate 4 treats ManyToOne's mapped as @Id differently, we need to scan the type and look for the id.. for (SingularAttribute attribute : entityType.getSingularAttributes()) { if (attribute.isId()) { - return attribute; + idTypes.add(attribute); } } - throw e; + if (idTypes.isEmpty()) { + throw e; + } else { + return idTypes; + } } catch (RuntimeException e) { // Datanucleus 4 can't properly handle entities for "views" with id columns, so we ignore the id column in this case if (e.getClass().getSimpleName().equals("ClassNotResolvedException")) { - return null; + return Collections.emptySet(); } throw e; } @@ -416,7 +448,7 @@ public static AttributePath getBasicAttributePath(Metamodel metamodel, ManagedTy currentType = metamodel.entity(currentClass); // look ahead Attribute nextAttr = getAttribute(currentType, attributeParts[i + 1]); - if (!getIdAttribute((EntityType) currentType).getName().equals(nextAttr.getName())) { + if (!getSingleIdAttribute((EntityType) currentType).getName().equals(nextAttr.getName())) { throw new IllegalArgumentException("Path joining not allowed in returning expression: " + attributePath); } } @@ -449,7 +481,7 @@ public static AttributePath getJoinTableCollectionAttributePath(Metamodel metamo } else { int dotIndex = trimmedPath.indexOf('.'); if (!trimmedPath.equals(collectionName) && (dotIndex == -1 || !trimmedPath.substring(0, dotIndex).equals(collectionName))) { - SingularAttribute idAttribute = getIdAttribute(type); + SingularAttribute idAttribute = getSingleIdAttribute(type); if (!idAttribute.getName().equals(attributePath)) { throw new IllegalArgumentException("Only access to the owner type's id attribute '" + idAttribute.getName() + "' is allowed. Invalid access to different attribute through the expression: " + attributePath); } @@ -466,7 +498,7 @@ public static AttributePath getJoinTableCollectionAttributePath(Metamodel metamo ManagedType targetManagedType = metamodel.managedType(targetClass); if (targetManagedType instanceof EntityType) { EntityType targetEntityType = (EntityType) targetManagedType; - SingularAttribute idAttribute = getIdAttribute(targetEntityType); + SingularAttribute idAttribute = getSingleIdAttribute(targetEntityType); String actualIdAttributeName = idAttribute.getName(); if (!actualIdAttributeName.equals(collectionElementAttributeName)) { throw new IllegalArgumentException("Only access to the target element type's id attribute '" + actualIdAttributeName + "' is allowed. Invalid access to different attribute through the expression: " + attributePath); diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java index 77e696e3c6..62755b2f18 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/Document.java @@ -175,6 +175,7 @@ public void setVersions(Set versions) { this.versions = versions; } + @Basic(optional = false) public long getAge() { return age; } @@ -372,6 +373,7 @@ public void setNameContainerMap(Map nameContainerMa } @Temporal(TemporalType.DATE) + @Column // DataNucleus assumes this is not nullable when running com.blazebit.persistence.testsuite.JpqlFunctionTest.testGroupByFunction!? public Calendar getCreationDate() { return creationDate; } diff --git a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/TestCTE.java b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/TestCTE.java index 47b7af6fe3..b72ddf485a 100644 --- a/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/TestCTE.java +++ b/core/testsuite/src/main/java/com/blazebit/persistence/testsuite/entity/TestCTE.java @@ -55,7 +55,7 @@ public void setName(String name) { this.name = name; } - @Column(name = "nesting_level") + @Column(name = "nesting_level", nullable = false) public Integer getLevel() { return level; } diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CTETest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CTETest.java index 8b9073f055..e3cf397cf2 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CTETest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/CTETest.java @@ -212,7 +212,7 @@ public void testCTELimit() { .end(); String expected = "" + "WITH " + TestCTE.class.getSimpleName() + "(id, name, level) AS(\n" - + "SELECT e.id, e.name, 0 FROM RecursiveEntity e WHERE e.parent IS NULL ORDER BY " + renderNullPrecedence("e.id", "ASC", "LAST") + " LIMIT 1" + + "SELECT e.id, e.name, 0 FROM RecursiveEntity e WHERE e.parent IS NULL ORDER BY e.id ASC LIMIT 1" + "\n)\n" + "SELECT t FROM " + TestCTE.class.getSimpleName() + " t"; @@ -339,7 +339,7 @@ public void testRecursiveCTEPagination() { + "\nUNION ALL\n" + "SELECT e.id, e.name, t.level + 1 FROM " + TestCTE.class.getSimpleName() + " t" + innerJoinRecursive("RecursiveEntity e", "t.id = e.parent.id") + "\n)\n" - + "SELECT t FROM " + TestCTE.class.getSimpleName() + " t WHERE t.level < 2 ORDER BY " + renderNullPrecedence("t.level", "ASC", "LAST") + ", " + renderNullPrecedence("t.id", "ASC", "LAST"); + + "SELECT t FROM " + TestCTE.class.getSimpleName() + " t WHERE t.level < 2 ORDER BY t.level ASC, t.id ASC"; assertEquals(expectedCountQuery, pcb.getPageCountQueryString()); assertEquals(expectedObjectQuery, pcb.getQueryString()); @@ -417,7 +417,7 @@ public void testRecursiveCTEPaginationIdQuery() { + "\nUNION ALL\n" + "SELECT e.id, e.name, t.level + 1 FROM TestCTE t" + innerJoinRecursive("RecursiveEntity e", "t.id = e.parent.id") + "\n)\n" - + "SELECT r.id FROM RecursiveEntity r WHERE r.id IN (SELECT t.id FROM TestCTE t WHERE t.level < 2) GROUP BY r.id ORDER BY " + renderNullPrecedence("r.id", "ASC", "LAST"); + + "SELECT r.id FROM RecursiveEntity r WHERE r.id IN (SELECT t.id FROM TestCTE t WHERE t.level < 2) GROUP BY r.id ORDER BY r.id ASC"; String expectedObjectQuery = "" + "WITH RECURSIVE TestCTE(id, name, level) AS(\n" @@ -425,7 +425,7 @@ public void testRecursiveCTEPaginationIdQuery() { + "\nUNION ALL\n" + "SELECT e.id, e.name, t.level + 1 FROM TestCTE t" + innerJoinRecursive("RecursiveEntity e", "t.id = e.parent.id") + "\n)\n" - + "SELECT r.name, children_1.name FROM RecursiveEntity r LEFT JOIN r.children children_1 WHERE r.id IN :ids ORDER BY " + renderNullPrecedence("r.id", "ASC", "LAST"); + + "SELECT r.name, children_1.name FROM RecursiveEntity r LEFT JOIN r.children children_1 WHERE r.id IN :ids ORDER BY r.id ASC"; assertEquals(expectedCountQuery, pcb.getPageCountQueryString()); assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); @@ -501,7 +501,7 @@ public void testRecursiveCTEPaginationIdQueryLeftJoin() { + "SELECT e.id, e.name, t.level + 1 FROM TestCTE t" + innerJoinRecursive("RecursiveEntity e", "t.id = e.parent.id") + "\n)\n" + "SELECT r.id FROM RecursiveEntity r" + innerJoin("TestCTE t", "r.id = t.id AND t.level < 2") - + " GROUP BY r.id ORDER BY " + renderNullPrecedence("r.id", "ASC", "LAST"); + + " GROUP BY r.id ORDER BY r.id ASC"; String expectedObjectQuery = "" + "WITH RECURSIVE TestCTE(id, name, level) AS(\n" @@ -510,7 +510,7 @@ public void testRecursiveCTEPaginationIdQueryLeftJoin() { + "SELECT e.id, e.name, t.level + 1 FROM TestCTE t" + innerJoinRecursive("RecursiveEntity e", "t.id = e.parent.id") + "\n)\n" + "SELECT r.name, children_1.name FROM RecursiveEntity r LEFT JOIN r.children children_1" + innerJoin("TestCTE t", "r.id = t.id AND t.level < 2", "r.id IN :ids") - + " ORDER BY " + renderNullPrecedence("r.id", "ASC", "LAST"); + + " ORDER BY r.id ASC"; assertEquals(expectedCountQuery, pcb.getPageCountQueryString()); assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); @@ -593,7 +593,7 @@ public void testBindEmbeddable() { + "), " + TestAdvancedCTE2.class.getSimpleName() + "(id, embeddable) AS(\n" + "SELECT testAdvancedCTE1.id, testAdvancedCTE1.embeddable FROM TestAdvancedCTE1 testAdvancedCTE1\n" + ")\n" - + "SELECT testAdvancedCTE2 FROM TestAdvancedCTE2 testAdvancedCTE2 ORDER BY " + renderNullPrecedence("testAdvancedCTE2.id", "ASC", "LAST"); + + "SELECT testAdvancedCTE2 FROM TestAdvancedCTE2 testAdvancedCTE2 ORDER BY testAdvancedCTE2.id ASC"; assertEquals(expected, cb.getQueryString()); List results = cb.getResultList(); @@ -626,7 +626,7 @@ public void testBindEmbeddableWithNullBindingsForJoinableAttributes() { + "WITH " + TestAdvancedCTE1.class.getSimpleName() + "(id, embeddable.name, embeddable.description, embeddable.recursiveEntity, level, parent) AS(\n" + "SELECT e.id, e.name, 'desc', NULLIF(1,1), 0, NULLIF(1,1) FROM RecursiveEntity e\n" + ")\n" - + "SELECT testAdvancedCTE1 FROM TestAdvancedCTE1 testAdvancedCTE1 ORDER BY " + renderNullPrecedence("testAdvancedCTE1.id", "ASC", "LAST"); + + "SELECT testAdvancedCTE1 FROM TestAdvancedCTE1 testAdvancedCTE1 ORDER BY testAdvancedCTE1.id ASC"; assertEquals(expected, cb.getQueryString()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/DeleteTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/DeleteTest.java index 0c551923e1..a994532041 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/DeleteTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/DeleteTest.java @@ -215,7 +215,7 @@ public void work(EntityManager em) { .select("idHolder.id") .end(); String expected = "WITH IdHolderCTE(id) AS(\n" - + "SELECT subDoc.id FROM Document subDoc ORDER BY " + renderNullPrecedence("subDoc.id", "ASC", "LAST") + " LIMIT 2\n" + + "SELECT subDoc.id FROM Document subDoc ORDER BY subDoc.id ASC LIMIT 2\n" + ")\n" + "DELETE FROM Document d WHERE d.id IN (SELECT idHolder.id FROM IdHolderCTE idHolder)"; @@ -249,7 +249,7 @@ public void work(EntityManager em) { .select("idHolder.id") .end(); String expected = "WITH IdHolderCTE(id) AS(\n" - + "SELECT subDoc.id FROM Document subDoc ORDER BY " + renderNullPrecedence("subDoc.id", "ASC", "LAST") + " LIMIT 2\n" + + "SELECT subDoc.id FROM Document subDoc ORDER BY subDoc.id ASC LIMIT 2\n" + ")\n" + "DELETE FROM Document d WHERE d.id IN (SELECT idHolder.id FROM IdHolderCTE idHolder)"; diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/EntityJoinTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/EntityJoinTest.java index 2071e9e613..42031ad259 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/EntityJoinTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/EntityJoinTest.java @@ -87,7 +87,7 @@ public void testEntityInnerJoin() { .orderByAsc("d.name"); assertEquals("SELECT d.name, p.name FROM Document d JOIN Person p" + onClause("p.age >= d.age") - + " ORDER BY " + renderNullPrecedence("d.name", "ASC", "LAST"), crit.getQueryString()); + + " ORDER BY d.name ASC", crit.getQueryString()); List results = crit.getResultList(); assertEquals(2, results.size()); @@ -109,7 +109,7 @@ public void testEntityLeftJoin() { .orderByAsc("d.name"); assertEquals("SELECT d.name, p.name FROM Document d LEFT JOIN Person p" + onClause("p.name = d.name") + - " ORDER BY " + renderNullPrecedence("d.name", "ASC", "LAST"), crit.getQueryString()); + " ORDER BY d.name ASC", crit.getQueryString()); List results = crit.getResultList(); assertEquals(3, results.size()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/InsertTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/InsertTest.java index f83579abdf..1067f8ef4c 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/InsertTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/InsertTest.java @@ -97,7 +97,7 @@ public void work(EntityManager em) { cb.bind("owner").select("p"); cb.orderByAsc("p.id"); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -122,7 +122,7 @@ public void work(EntityManager em) { cb.orderByAsc("p.id"); cb.setMaxResults(1); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -148,7 +148,7 @@ public void work(EntityManager em) { cb.setFirstResult(1); cb.setMaxResults(1); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -173,7 +173,7 @@ public void work(EntityManager em) { cb.bind("owner").select("p"); cb.orderByAsc("p.id"); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT :param_0, :param_1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT :param_0, :param_1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -201,7 +201,7 @@ public ReturningResult work(EntityManager em) { cb.orderByAsc("p.id"); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -230,7 +230,7 @@ public ReturningResult work(EntityManager em) { cb.orderByAsc("p.id"); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -258,7 +258,7 @@ public ReturningResult work(EntityManager em) { cb.orderByAsc("p.id"); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p WHERE p.name = :param_0 ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p WHERE p.name = :param_0 ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -287,7 +287,7 @@ public ReturningResult work(EntityManager em) { cb.setMaxResults(1); String expected = "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), p FROM Person p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -328,7 +328,7 @@ public ReturningResult work(EntityManager em) { + "SELECT p.id, CONCAT(p.name,'s document'), p.age, 1, p.id FROM Person p\n" + ")\n" + "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, p.idx, p.name, p.owner FROM PersonCTE p WHERE p.name = :param_0 ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, p.idx, p.name, p.owner FROM PersonCTE p WHERE p.name = :param_0 ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -369,7 +369,7 @@ public ReturningResult work(EntityManager em) { + "SELECT p.id, CONCAT(p.name,'s document'), p.age, 1, p.id FROM Person p\n" + ")\n" + "INSERT INTO Document(age, idx, name, owner)\n" - + "SELECT p.age, p.idx, p.name, p.owner FROM PersonCTE p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, p.idx, p.name, p.owner FROM PersonCTE p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); @@ -412,7 +412,7 @@ public ReturningResult work(EntityManager em) { + "DELETE FROM Person p WHERE p.name = :param_0 RETURNING id, name, age, id\n" + ")\n" + "INSERT INTO Document(age, idx, name, nonJoinable, owner)\n" - + "SELECT p.age, 1, CONCAT(p.name,'s document'), CONCAT('PersonId=',p.owner.id), :param_1 FROM DeletePersonCTE p ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + + "SELECT p.age, 1, CONCAT(p.name,'s document'), CONCAT('PersonId=',p.owner.id), :param_1 FROM DeletePersonCTE p ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java index f65f07104d..f72acb9944 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JoinTest.java @@ -475,7 +475,7 @@ public void testPaginatedJoinFetch(){ .orderByAsc("d.id") .page(0, 10); - assertEquals("SELECT d FROM Document d LEFT JOIN FETCH d.contacts c WHERE d.id IN :ids ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"), crit.getQueryString()); + assertEquals("SELECT d FROM Document d LEFT JOIN FETCH d.contacts c WHERE d.id IN :ids ORDER BY d.id ASC", crit.getQueryString()); } // NOTE: DB2 9.7 which is what we've got on Travis CI does not support subqueries in the on clause. See http://www-01.ibm.com/support/knowledgecenter/SSEPGG_9.7.0/com.ibm.db2.luw.messages.sql.doc/doc/msql00338n.html?cp=SSEPGG_9.7.0 diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JpqlFunctionTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JpqlFunctionTest.java index 137f249b45..ad58a3ee8f 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JpqlFunctionTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/JpqlFunctionTest.java @@ -76,7 +76,7 @@ public void testLimit() { .orderByAsc("subDoc.name") .end(); String expected = "SELECT d FROM Document d WHERE d.id IN (" + function("LIMIT", - "(SELECT subDoc.id FROM Document subDoc ORDER BY " + renderNullPrecedence("subDoc.name", "ASC", "LAST") + ")" + "(SELECT subDoc.id FROM Document subDoc ORDER BY subDoc.name ASC)" ,"1") + ")"; assertEquals(expected, cb.getQueryString()); @@ -97,7 +97,7 @@ public void testLimitOffset() { .orderByAsc("subDoc.name") .end(); String expected = "SELECT d FROM Document d WHERE d.id IN (" + function("LIMIT", - "(SELECT subDoc.id FROM Document subDoc ORDER BY " + renderNullPrecedence("subDoc.name", "ASC", "LAST") + ")" + "(SELECT subDoc.id FROM Document subDoc ORDER BY subDoc.name ASC)" ,"1", "1") + ")"; assertEquals(expected, cb.getQueryString()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationNullsTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationNullsTest.java index 55834e9a8c..c88b78d94d 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationNullsTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationNullsTest.java @@ -330,17 +330,17 @@ public void matrixText() { "k.id", groupByClause("k.a", aAsc, aNullsFirst), groupByClause("k.b", bAsc, bNullsFirst), - groupByClause("k.id", idAsc, idNullsFirst) + "k.id" ) + " ORDER BY " + orderByClause("k.a", aAsc, aNullsFirst) + ", " + orderByClause("k.b", bAsc, bNullsFirst) + ", " - + orderByClause("k.id", idAsc, idNullsFirst); + + "k.id " + (idAsc ? "ASC" : "DESC"); String expectedObjectQueryStart = "SELECT k.id, k.a, k.b, k.id FROM KeysetEntity k" + (keysetCondition.isEmpty() ? "" : " WHERE "); String expectedObjectQueryEnd = " ORDER BY " + orderByClause("k.a", aAsc, aNullsFirst) + ", " + orderByClause("k.b", bAsc, bNullsFirst) + ", " - + orderByClause("k.id", idAsc, idNullsFirst); + + "k.id " + (idAsc ? "ASC" : "DESC"); CriteriaBuilder crit = cbf.create(em, Tuple.class).from(KeysetEntity.class, "k") .select("id"); crit.orderBy("a", this.aAsc, this.aNullsFirst) diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationTest.java index 470e626740..bdf2ff5231 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/KeysetPaginationTest.java @@ -166,8 +166,8 @@ public void forwardBackwardsPaginationResultSetOrder() { assertEquals( "SELECT d.id, owner_1.name, d.name, d.id FROM Document d JOIN d.owner owner_1 " + "WHERE (owner_1.name < :_keysetParameter_0 OR (owner_1.name = :_keysetParameter_0 AND (d.name < :_keysetParameter_1 OR (d.name = :_keysetParameter_1 AND d.id > :_keysetParameter_2)))) " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"), + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name DESC, d.name DESC, d.id ASC", pcb.getPageIdQueryString() ); result = pcb.getResultList(); @@ -180,8 +180,8 @@ public void forwardBackwardsPaginationResultSetOrder() { assertEquals( "SELECT d.id, owner_1.name, d.name, d.id FROM Document d JOIN d.owner owner_1 " + "WHERE (owner_1.name > :_keysetParameter_0 OR (owner_1.name = :_keysetParameter_0 AND (d.name > :_keysetParameter_1 OR (d.name = :_keysetParameter_1 AND d.id < :_keysetParameter_2)))) " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "ASC", "FIRST") + ", " + renderNullPrecedence("d.name", "ASC", "FIRST") + ", " + renderNullPrecedence("d.id", "DESC", "FIRST"), + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name ASC, d.name ASC, d.id DESC", pcb.getPageIdQueryString() ); result = pcb.getResultList(); @@ -208,8 +208,8 @@ public void testWithReferenceObject() { "(SELECT _page_position_d.id " + "FROM Document _page_position_d " + "JOIN _page_position_d.owner _page_position_owner_1 " - + "GROUP BY " + groupBy("_page_position_d.id", renderNullPrecedenceGroupBy("_page_position_owner_1.name"), renderNullPrecedenceGroupBy("_page_position_d.name"), renderNullPrecedenceGroupBy("_page_position_d.id")) - + " ORDER BY " + renderNullPrecedence("_page_position_owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("_page_position_d.name", "ASC", "LAST") + ", " + renderNullPrecedence("_page_position_d.id", "ASC", "LAST") + ")", + + "GROUP BY " + groupBy("_page_position_d.name", "_page_position_owner_1.name", "_page_position_d.id") + + " ORDER BY _page_position_owner_1.name DESC, _page_position_d.name ASC, _page_position_d.id ASC)", ":_entityPagePositionParameter" ) + " FROM Document d"; @@ -244,8 +244,8 @@ public void testWithNotExistingReferenceObject() { + "FROM Document _page_position_d " + "JOIN _page_position_d.owner _page_position_owner_1 " + "WHERE _page_position_d.name <> :param_0 " - + "GROUP BY " + groupBy("_page_position_d.id", renderNullPrecedenceGroupBy("_page_position_owner_1.name"), renderNullPrecedenceGroupBy("_page_position_d.name"), renderNullPrecedenceGroupBy("_page_position_d.id")) - + " ORDER BY " + renderNullPrecedence("_page_position_owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("_page_position_d.name", "ASC", "LAST") + ", " + renderNullPrecedence("_page_position_d.id", "ASC", "LAST") + ")", + + "GROUP BY " + groupBy("_page_position_d.name", "_page_position_owner_1.name", "_page_position_d.id") + + " ORDER BY _page_position_owner_1.name DESC, _page_position_d.name ASC, _page_position_d.id ASC)", ":_entityPagePositionParameter" ) + " FROM Document d " @@ -291,8 +291,8 @@ public void simpleTest(CriteriaBuilder crit, PaginatedCriteriaBuilder crit, PaginatedCriteriaBuilder :_keysetParameter_1 OR (d.name = :_keysetParameter_1 AND d.id > :_keysetParameter_2)))) " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.name", "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name DESC, d.name ASC, d.id ASC"; assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); // Same page again key set @@ -313,8 +313,8 @@ public void simpleTest(CriteriaBuilder crit, PaginatedCriteriaBuilder :_keysetParameter_1 OR (d.name = :_keysetParameter_1 AND d.id >= :_keysetParameter_2)))) " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.name", "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name DESC, d.name ASC, d.id ASC"; assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); assertEquals(1, result.size()); @@ -325,8 +325,8 @@ public void simpleTest(CriteriaBuilder crit, PaginatedCriteriaBuilder result = cb.getResultList(); @@ -84,7 +84,7 @@ public void testSubqueryAndOuterQueryLimit() { .end() .orderByAsc("p.id") .setMaxResults(1); - String expected = "SELECT p FROM Person p WHERE p.id IN (" + function("LIMIT", "(SELECT pSub.id FROM Person pSub ORDER BY " + renderNullPrecedence("pSub.id", "ASC", "LAST") + ")", "2") + ") ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + String expected = "SELECT p FROM Person p WHERE p.id IN (" + function("LIMIT", "(SELECT pSub.id FROM Person pSub ORDER BY pSub.id ASC)", "2") + ") ORDER BY p.id ASC"; assertEquals(expected, cb.getQueryString()); List result = cb.getResultList(); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationNullsTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationNullsTest.java index defc7253cb..d1d515c36c 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationNullsTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationNullsTest.java @@ -403,22 +403,22 @@ public void matrixText() { String expectedIdQueryEnd = " GROUP BY " + groupBy( "k.id", - groupByClause("k.a", aAsc, aNullsFirst), + "k.a", groupByClause("k.b", bAsc, bNullsFirst), groupByClause("k.c", cAsc, cNullsFirst), - groupByClause("k.id", idAsc, idNullsFirst) + "k.id" ) + " ORDER BY " - + orderByClause("k.a", aAsc, aNullsFirst) + ", " + + "k.a " + (aAsc ? "ASC" : "DESC") + ", " + orderByClause("k.b", bAsc, bNullsFirst) + ", " + orderByClause("k.c", cAsc, cNullsFirst) + ", " - + orderByClause("k.id", idAsc, idNullsFirst); + + "k.id " + (idAsc ? "ASC" : "DESC"); String expectedObjectQueryStart = "SELECT k.id, k.a, k.b, k.c, k.id FROM KeysetEntity2 k" + (keysetCondition.isEmpty() ? "" : " WHERE "); String expectedObjectQueryEnd = " ORDER BY " - + orderByClause("k.a", aAsc, aNullsFirst) + ", " + + "k.a " + (aAsc ? "ASC" : "DESC") + ", " + orderByClause("k.b", bAsc, bNullsFirst) + ", " + orderByClause("k.c", cAsc, cNullsFirst) + ", " - + orderByClause("k.id", idAsc, idNullsFirst); + + "k.id " + (idAsc ? "ASC" : "DESC"); CriteriaBuilder crit = cbf.create(em, Tuple.class).from(KeysetEntity2.class, "k") .select("id"); crit.orderBy("a", this.aAsc, this.aNullsFirst) diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationRowValueConstructorTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationRowValueConstructorTest.java index 10f22a431f..b5498bb7b7 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationRowValueConstructorTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/OptimizedKeysetPaginationRowValueConstructorTest.java @@ -171,8 +171,8 @@ public void forwardBackwardsPaginationResultSetOrder() { assertEquals( "SELECT d.id, owner_1.name, d.name, d.id FROM Document d JOIN d.owner owner_1 " + "WHERE " + function("compare_row_value", "'<'", "owner_1.name", "d.name", ":_keysetParameter_2", ":_keysetParameter_0", ":_keysetParameter_1", "d.id") + " = true " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"), + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name DESC, d.name DESC, d.id ASC", pcb.getPageIdQueryString() ); @@ -186,8 +186,8 @@ public void forwardBackwardsPaginationResultSetOrder() { assertEquals( "SELECT d.id, owner_1.name, d.name, d.id FROM Document d JOIN d.owner owner_1 " + "WHERE " + function("compare_row_value", "'<'", ":_keysetParameter_0", ":_keysetParameter_1", ":_keysetParameter_2", "owner_1.name", "d.name", "d.id") + " = true " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "ASC", "FIRST") + ", " + renderNullPrecedence("d.name", "ASC", "FIRST") + ", " + renderNullPrecedence("d.id", "DESC", "FIRST"), + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name ASC, d.name ASC, d.id DESC", pcb.getPageIdQueryString() ); result = pcb.getResultList(); @@ -214,8 +214,8 @@ public void testWithReferenceObject() { "(SELECT _page_position_d.id " + "FROM Document _page_position_d " + "JOIN _page_position_d.owner _page_position_owner_1 " - + "GROUP BY " + groupBy("_page_position_d.id", renderNullPrecedenceGroupBy("_page_position_owner_1.name"), renderNullPrecedenceGroupBy("_page_position_d.name"), renderNullPrecedenceGroupBy("_page_position_d.id")) - + " ORDER BY " + renderNullPrecedence("_page_position_owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("_page_position_d.name", "ASC", "LAST") + ", " + renderNullPrecedence("_page_position_d.id", "ASC", "LAST") + ")", + + "GROUP BY " + groupBy("_page_position_d.name", "_page_position_owner_1.name", "_page_position_d.id") + + " ORDER BY _page_position_owner_1.name DESC, _page_position_d.name ASC, _page_position_d.id ASC)", ":_entityPagePositionParameter" ) + " FROM Document d"; @@ -250,8 +250,8 @@ public void testWithNotExistingReferenceObject() { + "FROM Document _page_position_d " + "JOIN _page_position_d.owner _page_position_owner_1 " + "WHERE _page_position_d.name <> :param_0 " - + "GROUP BY " + groupBy("_page_position_d.id", renderNullPrecedenceGroupBy("_page_position_owner_1.name"), renderNullPrecedenceGroupBy("_page_position_d.name"), renderNullPrecedenceGroupBy("_page_position_d.id")) - + " ORDER BY " + renderNullPrecedence("_page_position_owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("_page_position_d.name", "ASC", "LAST") + ", " + renderNullPrecedence("_page_position_d.id", "ASC", "LAST") + ")", + + "GROUP BY " + groupBy("_page_position_d.name", "_page_position_owner_1.name", "_page_position_d.id") + + " ORDER BY _page_position_owner_1.name DESC, _page_position_d.name ASC, _page_position_d.id ASC)", ":_entityPagePositionParameter" ) + " FROM Document d " @@ -288,8 +288,8 @@ public void testWithNotExistingReferenceObject() { public void simpleTest(CriteriaBuilder crit, PaginatedCriteriaBuilder pcb, PagedList result) { // The first time we have to use the offset String expectedIdQuery = "SELECT d.id, owner_1.name, d.name, d.id FROM Document d JOIN d.owner owner_1 " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("owner_1.name"), renderNullPrecedenceGroupBy("d.name"), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("owner_1.name", "DESC", "LAST") + ", " + renderNullPrecedence("d.name", "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.name", "owner_1.name", "d.id") + + " ORDER BY owner_1.name DESC, d.name ASC, d.id ASC"; assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); assertEquals(1, result.size()); @@ -301,8 +301,8 @@ public void simpleTest(CriteriaBuilder crit, PaginatedCriteriaBuilder crit, PaginatedCriteriaBuilder criteria = cbf.create(em, Document.class, "d"); criteria.orderByAsc("CONCAT(:prefix, name)"); assertEquals("SELECT d FROM Document d ORDER BY " + renderNullPrecedence("CONCAT(:prefix,d.name)", "ASC", "LAST"), criteria.getQueryString()); @@ -169,14 +209,14 @@ public void testOrderByConcatParameter(){ } @Test - public void testOrderByFunctionExperimental(){ + public void testOrderByFunctionExperimental() { CriteriaBuilder criteria = cbf.create(em, Document.class, "d"); criteria.orderByDesc("FUNCTION('zero',FUNCTION('zero',d.id,FUNCTION('zero',FUNCTION('zero',:colors))),1)"); assertEquals("SELECT d FROM Document d ORDER BY " + renderNullPrecedence(function("zero", function("zero", "d.id", function("zero", function("zero", ":colors"))), "1"), "DESC", "LAST"), criteria.getQueryString()); } @Test - public void testOrderBySize(){ + public void testOrderBySize() { CriteriaBuilder criteria = cbf.create(em, Document.class, "d"); criteria.select("d.id").orderByAsc("SIZE(d.partners)"); @@ -186,7 +226,7 @@ public void testOrderBySize(){ } @Test - public void testOrderBySizeMultiple(){ + public void testOrderBySizeMultiple() { CriteriaBuilder criteria = cbf.create(em, Document.class, "d"); criteria.select("d.id").orderByAsc("SIZE(d.partners)").orderByDesc("SIZE(d.versions)"); @@ -194,4 +234,25 @@ public void testOrderBySizeMultiple(){ assertEquals(expected, criteria.getQueryString()); criteria.getResultList(); } + + @Test + public void testOrderByAliasedSubqueryWithEmulatedNullPrecedence() { + // DB2 does not support correlated subqueries in the ORDER BY clause + // This test is to ensure, we don't copy the correlated subquery into the order by if we can prove the expression is not nullable + CriteriaBuilder criteria = cbf.create(em, Tuple.class); + criteria.from(Document.class, "d"); + criteria.select("d.id"); + criteria.selectSubquery("childCount") + .from(Person.class, "p") + .select("COUNT(*)") + .where("p.partnerDocument").eqExpression("d") + .end(); + criteria.orderByAsc("childCount", true); + + String subquery = "(SELECT " + countStar() + " FROM Person p WHERE p.partnerDocument = d)"; + final String expected = "SELECT d.id, " + subquery + " AS childCount FROM Document d" + + " ORDER BY childCount ASC"; + assertEquals(expected, criteria.getQueryString()); + criteria.getResultList(); + } } diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationEmbeddedIdTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationEmbeddedIdTest.java index b56ae93de5..5be77256e3 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationEmbeddedIdTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationEmbeddedIdTest.java @@ -63,12 +63,12 @@ public void simpleTest() { + "LEFT JOIN e.embeddable.elementCollection elementCollection_test_1" + onClause("KEY(elementCollection_test_1) = 'test'") + " WHERE " + joinAliasValue("elementCollection_test_1", "primaryName") + " = :param_0" - + " GROUP BY " + groupBy("e.id", renderNullPrecedenceGroupBy("e.id")) - + " ORDER BY " + renderNullPrecedence("e.id", "ASC", "LAST"); + + " GROUP BY " + groupBy("e.id", "e.id") + + " ORDER BY e.id ASC"; String expectedObjectQuery = "SELECT e FROM EmbeddableTestEntity e" + " WHERE e.id IN :ids" - + " ORDER BY " + renderNullPrecedence("e.id", "ASC", "LAST"); + + " ORDER BY e.id ASC"; PaginatedCriteriaBuilder pcb = crit.page(0, 2); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationTest.java index dbfa726048..c584bccf57 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/PaginationTest.java @@ -122,8 +122,8 @@ public void simpleTest() { + onClause("KEY(localized_1_1) = 1") + " WHERE UPPER(d.name) LIKE UPPER(:param_0) AND owner_1.name LIKE :param_1 AND UPPER(" + joinAliasValue("localized_1_1") + ") LIKE UPPER(:param_2) " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.id", "d.id") + + " ORDER BY d.id ASC"; String expectedObjectQuery = "SELECT d.name, CONCAT(owner_1.name,' user'), COALESCE(" + joinAliasValue("localized_1_1") + ",'no item'), partnerDocument_1.name FROM Document d " @@ -131,7 +131,7 @@ public void simpleTest() { + onClause("KEY(localized_1_1) = 1") + " LEFT JOIN owner_1.partnerDocument partnerDocument_1 " + "WHERE d.id IN :ids " - + "ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "ORDER BY d.id ASC"; PaginatedCriteriaBuilder pcb = crit.page(0, 2); @@ -154,7 +154,6 @@ public void simpleTest() { assertEquals("DOC5", result.get(0).getName()); } - @Test public void simpleTestFetch() { PaginatedCriteriaBuilder cb = cbf.create(em, Document.class, "d") @@ -171,11 +170,11 @@ public void simpleTestFetch() { @Test public void testSelectIndexedWithParameter() { String expectedCountQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d JOIN d.owner owner_1 WHERE owner_1.name = :param_0"; - String expectedIdQuery = "SELECT d.id FROM Document d JOIN d.owner owner_1 WHERE owner_1.name = :param_0 GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + String expectedIdQuery = "SELECT d.id FROM Document d JOIN d.owner owner_1 WHERE owner_1.name = :param_0 GROUP BY " + groupBy("d.id", "d.id") + " ORDER BY d.id ASC"; String expectedObjectQuery = "SELECT " + joinAliasValue("contacts_contactNr_1", "name") + " FROM Document d " + "LEFT JOIN d.contacts contacts_contactNr_1" + onClause("KEY(contacts_contactNr_1) = :contactNr") - + " WHERE d.id IN :ids ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + + " WHERE d.id IN :ids ORDER BY d.id ASC"; PaginatedCriteriaBuilder cb = cbf.create(em, Document.class, "d") .where("owner.name").eq("Karl1") .select("contacts[:contactNr].name") @@ -208,8 +207,8 @@ public void testPaginationWithReferenceObject() { + function("PAGE_POSITION", "(SELECT _page_position_d.id " + "FROM Document _page_position_d " - + "GROUP BY " + groupBy("_page_position_d.id", renderNullPrecedenceGroupBy("_page_position_d.name"), renderNullPrecedenceGroupBy("_page_position_d.id")) - + " ORDER BY " + renderNullPrecedence("_page_position_d.name", "ASC", "LAST") + ", " + renderNullPrecedence("_page_position_d.id", "ASC", "LAST") + ")", + + "GROUP BY " + groupBy("_page_position_d.id", "_page_position_d.name", "_page_position_d.id") + + " ORDER BY _page_position_d.name ASC, _page_position_d.id ASC)", ":_entityPagePositionParameter") + " " + "FROM Document d"; @@ -251,8 +250,8 @@ public void testPaginationWithNotExistingReferenceObject() { "(SELECT _page_position_d.id " + "FROM Document _page_position_d " + "WHERE _page_position_d.name <> :param_0 " - + "GROUP BY " + groupBy("_page_position_d.id", renderNullPrecedenceGroupBy("_page_position_d.name"), renderNullPrecedenceGroupBy("_page_position_d.id")) - + " ORDER BY " + renderNullPrecedence("_page_position_d.name", "ASC", "LAST") + ", " + renderNullPrecedence("_page_position_d.id", "ASC", "LAST") + ")", + + "GROUP BY " + groupBy("_page_position_d.id", "_page_position_d.name", "_page_position_d.id") + + " ORDER BY _page_position_d.name ASC, _page_position_d.id ASC)", ":_entityPagePositionParameter") + " " + "FROM Document d " @@ -283,9 +282,19 @@ public void testPaginationWithNotExistingReferenceObject() { @Test public void testPaginatedWithGroupBy1() { + String expectedCountQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d"; + String expectedIdQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", "d.id") + " ORDER BY d.id ASC"; + String expectedObjectQuery = "SELECT d.id, COUNT(contacts_1.id) FROM Document d LEFT JOIN d.contacts contacts_1" + + " WHERE d.id IN :ids" + + " GROUP BY " + groupBy("d.id", "d.id") + + " ORDER BY d.id ASC"; CriteriaBuilder cb = cbf.create(em, Tuple.class).from(Document.class, "d") - .select("d.id").select("COUNT(contacts.id)").groupBy("id"); - verifyException(cb, IllegalStateException.class).page(0, 1); + .select("d.id").select("COUNT(contacts.id)").groupBy("id").orderByAsc("d.id"); + PaginatedCriteriaBuilder pcb = cb.page(0, 1); + assertEquals(expectedCountQuery, pcb.getPageCountQueryString()); + assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); + assertEquals(expectedObjectQuery, pcb.getQueryString()); + pcb.getResultList(); } @Test @@ -345,8 +354,8 @@ public void testOrderByExpression() { .page(0, 1); String expectedIdQuery = "SELECT d.id FROM Document d LEFT JOIN d.contacts contacts_contactNr_1" + onClause("KEY(contacts_contactNr_1) = :contactNr") - + " GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy(joinAliasValue("contacts_contactNr_1", "name")), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence(joinAliasValue("contacts_contactNr_1", "name"), "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + " GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy(joinAliasValue("contacts_contactNr_1", "name")), "d.id") + + " ORDER BY " + renderNullPrecedence(joinAliasValue("contacts_contactNr_1", "name"), "ASC", "LAST") + ", d.id ASC"; assertEquals(expectedIdQuery, cb.getPageIdQueryString()); cb.getResultList(); } @@ -363,8 +372,8 @@ public void testOrderBySelectAlias() { .page(0, 1); String expectedIdQuery = "SELECT d.id FROM Document d LEFT JOIN d.contacts contacts_contactNr_1" + onClause("KEY(contacts_contactNr_1) = :contactNr") - + " GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy(joinAliasValue("contacts_contactNr_1", "name")), renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence(joinAliasValue("contacts_contactNr_1", "name"), "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + " GROUP BY " + groupBy(renderNullPrecedenceGroupBy(joinAliasValue("contacts_contactNr_1", "name")), "d.id") + + " ORDER BY " + renderNullPrecedence(joinAliasValue("contacts_contactNr_1", "name"), "ASC", "LAST") + ", d.id ASC"; assertEquals(expectedIdQuery, cb.getPageIdQueryString()); cb.getResultList(); } @@ -382,8 +391,8 @@ public void testOrderBySubquery() { .page(0, 1); String expectedSubQuery = "(SELECT COUNT(" + joinAliasValue("contacts_1", "id") + ") FROM Document d2 LEFT JOIN d2.contacts contacts_1 WHERE d2.id = d.id)"; String expectedIdQuery = "SELECT d.id, " + expectedSubQuery + " AS contactCount FROM Document d " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("contactCount", expectedSubQuery, "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.id", "d.id") + + " ORDER BY contactCount ASC, d.id ASC"; assertEquals(expectedIdQuery, cb.getPageIdQueryString()); cb.getResultList(); } @@ -398,9 +407,9 @@ public void testOrderBySize() { .orderByAsc("SIZE(d.contacts)") .orderByAsc("id") .page(0, 1); - String expectedIdQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("SIZE(d.contacts)"), renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("SIZE(d.contacts)", "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + String expectedIdQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", "SIZE(d.contacts)", "d.id") + " ORDER BY SIZE(d.contacts) ASC, d.id ASC"; String expectedCountQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d"; - String expectedObjectQuery = "SELECT COUNT(" + joinAliasValue("contacts_1") + ") FROM Document d LEFT JOIN d.contacts contacts_1 WHERE d.id IN :ids GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("SIZE(d.contacts)"), renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("SIZE(d.contacts)", "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + String expectedObjectQuery = "SELECT COUNT(" + joinAliasValue("contacts_1") + ") FROM Document d LEFT JOIN d.contacts contacts_1 WHERE d.id IN :ids GROUP BY " + groupBy("d.id", "SIZE(d.contacts)", "d.id") + " ORDER BY SIZE(d.contacts) ASC, d.id ASC"; assertEquals(expectedIdQuery, cb.getPageIdQueryString()); assertEquals(expectedCountQuery, cb.getPageCountQueryString()); @@ -416,12 +425,12 @@ public void testOrderBySizeAlias() { .orderByAsc("id") .page(0, 1); String expectedIdQuery = "SELECT d.id, " + function("COUNT_TUPLE", "KEY(contacts_1)") + " AS contactCount FROM Document d LEFT JOIN d.contacts contacts_1 " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("contactCount", function("COUNT_TUPLE", "KEY(contacts_1)"), "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.id", "d.id") + + " ORDER BY contactCount ASC, d.id ASC"; String expectedCountQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d"; String expectedObjectQuery = "SELECT " + function("COUNT_TUPLE", "KEY(contacts_1)") + " AS contactCount FROM Document d LEFT JOIN d.contacts contacts_1 WHERE d.id IN :ids " - + "GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("contactCount", function("COUNT_TUPLE", "KEY(contacts_1)"), "ASC", "LAST") + ", " + renderNullPrecedence("d.id", "ASC", "LAST"); + + "GROUP BY " + groupBy("d.id", "d.id") + + " ORDER BY contactCount ASC, d.id ASC"; assertEquals(expectedIdQuery, cb.getPageIdQueryString()); assertEquals(expectedCountQuery, cb.getPageCountQueryString()); @@ -507,12 +516,19 @@ public void testPaginationWithoutOrderBy() { PaginatedCriteriaBuilder cb = cbf.create(em, Tuple.class).from(Document.class, "d").page(0, 10); verifyException(cb, IllegalStateException.class).getResultList(); } + + @Test + public void testPaginationWithoutUniqueOrderBy() { + PaginatedCriteriaBuilder cb = cbf.create(em, Tuple.class).from(Document.class, "d") + .orderByAsc("d.name").page(0, 10); + verifyException(cb, IllegalStateException.class).getResultList(); + } @Test public void testPaginationWithoutUniqueLastOrderBy() { PaginatedCriteriaBuilder cb = cbf.create(em, Tuple.class).from(Document.class, "d") .orderByAsc("d.id").orderByAsc("d.name").page(0, 10); - verifyException(cb, IllegalStateException.class).getResultList(); + cb.getResultList(); } @Test @@ -522,7 +538,7 @@ public void testPaginationObjectQueryClauseExclusions() { .innerJoinDefault("contacts", "c") .where("c.name").eq("Karl1") .orderByAsc("d.id").page(0, 10); - String query = "SELECT d.id FROM Document d JOIN d.contacts c WHERE d.id IN :ids ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + String query = "SELECT d.id FROM Document d JOIN d.contacts c WHERE d.id IN :ids ORDER BY d.id ASC"; assertEquals(query, cb.getQueryString()); cb.getResultList(); } @@ -541,10 +557,10 @@ public void testPaginationWithExplicitRestrictingJoin() { + onClause("KEY(c) = 1"); String idQuery = "SELECT d.id FROM Document d JOIN d.contacts c" + onClause("KEY(c) = 1") + - " GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + " GROUP BY " + groupBy("d.id", "d.id") + " ORDER BY d.id ASC"; String objectQuery = "SELECT d.id, " + joinAliasValue("c", "name") + " FROM Document d JOIN d.contacts c" + onClause("KEY(c) = 1") + - " WHERE d.id IN :ids ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + " WHERE d.id IN :ids ORDER BY d.id ASC"; assertEquals(countQuery, cb.getPageCountQueryString()); assertEquals(idQuery, cb.getPageIdQueryString()); assertEquals(objectQuery, cb.getQueryString()); @@ -565,9 +581,9 @@ public void testPaginationWithJoinFromSelectSubquery() { String countQuery = "SELECT " + countPaginated("d.id", true) + " FROM Document d LEFT JOIN d.contacts contacts_1"; String idQuery = "SELECT d.id FROM Document d LEFT JOIN d.contacts contacts_1" + - " GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + " GROUP BY " + groupBy("d.id", "d.id") + " ORDER BY d.id ASC"; String objectQuery = "SELECT (SELECT " + countStar() + " FROM Person pSub WHERE pSub.id = " + joinAliasValue("contacts_1", "id") + ") FROM Document d LEFT JOIN d.contacts contacts_1" + - " WHERE d.id IN :ids ORDER BY " + renderNullPrecedence("d.id", "ASC", "LAST"); + " WHERE d.id IN :ids ORDER BY d.id ASC"; assertEquals(countQuery, cb.getPageCountQueryString()); assertEquals(idQuery, cb.getPageIdQueryString()); assertEquals(objectQuery, cb.getQueryString()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectNewTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectNewTest.java index d34fdbc75b..f88637148e 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectNewTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectNewTest.java @@ -74,7 +74,7 @@ public void testSelectNewDocumentViewModel() { CriteriaBuilder criteria = cbf.create(em, Document.class) .selectNew(DocumentViewModel.class).with("name").end().orderByAsc("name"); - assertEquals("SELECT document.name FROM Document document ORDER BY " + renderNullPrecedence("document.name", "ASC", "LAST"), criteria + assertEquals("SELECT document.name FROM Document document ORDER BY document.name ASC", criteria .getQueryString()); List actual = criteria.getQuery().getResultList(); @@ -91,7 +91,7 @@ public void testSelectNewDocumentViewModel() { public void testSelectNewDocument() { CriteriaBuilder criteria = cbf.create(em, Document.class, "d"); criteria.selectNew(Document.class).with("d.name").end().where("LENGTH(d.name)").le(4).orderByAsc("d.name"); - assertEquals("SELECT d.name FROM Document d WHERE LENGTH(d.name) <= :param_0 ORDER BY " + renderNullPrecedence("d.name", "ASC", "LAST"), criteria + assertEquals("SELECT d.name FROM Document d WHERE LENGTH(d.name) <= :param_0 ORDER BY d.name ASC", criteria .getQueryString()); List actual = criteria.getQuery().getResultList(); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectTest.java index e68698e3ab..d3ef61d17b 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SelectTest.java @@ -394,7 +394,7 @@ public void testSelectAggregate() { .select("owner.name") .orderByDesc("id"); - String objectQuery = "SELECT " + function("COUNT_TUPLE", "versions_1.id") + ", owner_1.name FROM Document d JOIN d.owner owner_1 LEFT JOIN d.versions versions_1 GROUP BY " + groupBy("d.id", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + String objectQuery = "SELECT " + function("COUNT_TUPLE", "versions_1.id") + ", owner_1.name FROM Document d JOIN d.owner owner_1 LEFT JOIN d.versions versions_1 GROUP BY " + groupBy("d.id", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY d.id DESC"; assertEquals(objectQuery, cb.getQueryString()); cb.getResultList(); } @@ -408,8 +408,8 @@ public void testSelectAggregatePaginated() { .page(0, 10); String countQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d"; - String idQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); - String objectQuery = "SELECT " + function("COUNT_TUPLE", "versions_1.id") + ", owner_1.name FROM Document d JOIN d.owner owner_1 LEFT JOIN d.versions versions_1 WHERE d.id IN :ids GROUP BY " + groupBy("d.id", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + String idQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY d.id DESC"; + String objectQuery = "SELECT " + function("COUNT_TUPLE", "versions_1.id") + ", owner_1.name FROM Document d JOIN d.owner owner_1 LEFT JOIN d.versions versions_1 WHERE d.id IN :ids GROUP BY " + groupBy("d.id", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY d.id DESC"; assertEquals(countQuery, cb.getPageCountQueryString()); assertEquals(idQuery, cb.getPageIdQueryString()); @@ -428,7 +428,7 @@ public void testSelectNestedAggregate() { String objectQuery = "SELECT CASE WHEN MIN(d.lastModified) > d.creationDate THEN MIN(d.lastModified) ELSE CURRENT_TIMESTAMP END, owner_1.name " + "FROM Document d JOIN d.owner owner_1 " + "GROUP BY " + groupBy("d.creationDate", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + + " ORDER BY d.id DESC"; assertEquals(objectQuery, cb.getQueryString()); cb.getResultList(); } @@ -442,9 +442,9 @@ public void testSelectNestedAggregatePaginated() { .page(0, 10); String countQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d"; - String idQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + String idQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY d.id DESC"; String objectQuery = "SELECT CASE WHEN MIN(d.lastModified) > d.creationDate THEN MIN(d.lastModified) ELSE CURRENT_TIMESTAMP END, owner_1.name FROM Document d JOIN d.owner owner_1 " - + "GROUP BY " + groupBy("d.creationDate", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + + "GROUP BY " + groupBy("d.creationDate", "owner_1.name", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY d.id DESC"; assertEquals(countQuery, cb.getPageCountQueryString()); assertEquals(idQuery, cb.getPageIdQueryString()); @@ -463,7 +463,7 @@ public void testSelectAggregateEntitySelect() { String objectQuery = "SELECT CASE WHEN MIN(d.lastModified) > d.creationDate THEN MIN(d.lastModified) ELSE CURRENT_TIMESTAMP END, owner_1 FROM Document d " + "JOIN d.owner owner_1 " + "GROUP BY " + groupBy("d.creationDate", "owner_1.age", "owner_1.defaultLanguage", "owner_1.friend", "owner_1.id", "owner_1.name", "owner_1.nameObject", "owner_1.partnerDocument", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + + " ORDER BY d.id DESC"; assertEquals(objectQuery, cb.getQueryString()); @@ -479,11 +479,11 @@ public void testSelectAggregateEntitySelectPaginated() { .page(0, 10); String countQuery = "SELECT " + countPaginated("d.id", false) + " FROM Document d"; - String idQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + String idQuery = "SELECT d.id FROM Document d GROUP BY " + groupBy("d.id", renderNullPrecedenceGroupBy("d.id")) + " ORDER BY d.id DESC"; String objectQuery = "SELECT CASE WHEN MIN(d.lastModified) > d.creationDate THEN MIN(d.lastModified) ELSE CURRENT_TIMESTAMP END, owner_1 FROM Document d " + "JOIN d.owner owner_1 " + "GROUP BY " + groupBy("d.creationDate", "owner_1.age", "owner_1.defaultLanguage", "owner_1.friend", "owner_1.id", "owner_1.name", "owner_1.nameObject", "owner_1.partnerDocument", renderNullPrecedenceGroupBy("d.id")) - + " ORDER BY " + renderNullPrecedence("d.id", "DESC", "LAST"); + + " ORDER BY d.id DESC"; assertEquals(countQuery, cb.getPageCountQueryString()); assertEquals(idQuery, cb.getPageIdQueryString()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SetOperationTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SetOperationTest.java index 1eac71b056..ce831707b7 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SetOperationTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SetOperationTest.java @@ -166,7 +166,7 @@ public void testUnionAllOrderBy() { + "SELECT d1 FROM Document d1 WHERE d1.name = :param_0\n" + "UNION ALL\n" + "SELECT d2 FROM Document d2 WHERE d2.name = :param_1\n" - + "ORDER BY name ASC NULLS LAST"; + + "ORDER BY name ASC"; assertEquals(expected, cb.getQueryString()); List resultList = cb.getResultList(); @@ -201,11 +201,11 @@ public void testUnionAllOrderBySubqueryLimit() { "SET_UNION_ALL", "(SELECT d1.id FROM Document d1 WHERE d1.name = :param_0)", function("LIMIT", - "(SELECT d2.id FROM Document d2 WHERE d2.name <> :param_1 ORDER BY " + renderNullPrecedence("d2.name", "ASC", "LAST") + ")", + "(SELECT d2.id FROM Document d2 WHERE d2.name <> :param_1 ORDER BY d2.name ASC)", "1" ), "'ORDER_BY'", - "'1 DESC NULLS LAST'", + "'1 DESC'", "'LIMIT'", "1" ) @@ -237,8 +237,8 @@ public void testUnionAllOrderByOperandLimit() { String expected = "" + "SELECT d1 FROM Document d1 WHERE d1.name = :param_0\n" + "UNION ALL\n" - + "SELECT d2 FROM Document d2 WHERE d2.name <> :param_1 ORDER BY " + renderNullPrecedence("d2.name", "ASC", "LAST") + " LIMIT 1\n" - + "ORDER BY name DESC NULLS LAST LIMIT 1"; + + "SELECT d2 FROM Document d2 WHERE d2.name <> :param_1 ORDER BY d2.name ASC LIMIT 1\n" + + "ORDER BY name DESC LIMIT 1"; assertEquals(expected, cb.getQueryString()); List resultList = cb.getResultList(); @@ -395,7 +395,7 @@ public void testNestedIntersectWithUnion() { + "SELECT d2 FROM Document d2 WHERE d2.name <> :param_1)\n" + "UNION\n" + "SELECT d3 FROM Document d3 WHERE d3.name = :param_2\n" - + "ORDER BY name ASC NULLS LAST"; + + "ORDER BY name ASC"; assertEquals(expected, cb.getQueryString()); List resultList = cb.getResultList(); @@ -623,9 +623,9 @@ public void testAttributeOrderByLimit() { + "(SELECT d3 FROM Document d3 WHERE d3.name = :param_2\n" + "UNION\n" + "SELECT d4 FROM Document d4 WHERE d4.name = :param_3\n" - + "ORDER BY name DESC NULLS LAST" + + "ORDER BY name DESC" + " LIMIT 1)\n" - + "ORDER BY name DESC NULLS LAST" + + "ORDER BY name DESC" + " LIMIT 1"; assertEquals(expected, cb.getQueryString()); @@ -643,18 +643,18 @@ public void testAliasOrderByLimit() { .where("d1.name").eq("D1") .union() .from(Document.class, "d2") - .select("d2.name") + .select("d2.name", "docName") .where("d2.name").eq("D2") .startExcept() .from(Document.class, "d3") - .select("d3.name", "dName") + .select("d3.name", "docName") .where("d3.name").eq("D2") .union() .from(Document.class, "d4") - .select("d4.name") + .select("d4.name", "docName") .where("d4.name").eq("D3") .endSetWith() - .orderByDesc("dName") + .orderByDesc("docName") .setMaxResults(1) .endSet() .endSet() @@ -663,14 +663,14 @@ public void testAliasOrderByLimit() { String expected = "" + "SELECT d1.name AS docName FROM Document d1 WHERE d1.name = :param_0\n" + "UNION\n" - + "SELECT d2.name FROM Document d2 WHERE d2.name = :param_1\n" + + "SELECT d2.name AS docName FROM Document d2 WHERE d2.name = :param_1\n" + "EXCEPT\n" - + "(SELECT d3.name AS dName FROM Document d3 WHERE d3.name = :param_2\n" + + "(SELECT d3.name AS docName FROM Document d3 WHERE d3.name = :param_2\n" + "UNION\n" - + "SELECT d4.name FROM Document d4 WHERE d4.name = :param_3\n" - + "ORDER BY dName DESC NULLS LAST" + + "SELECT d4.name AS docName FROM Document d4 WHERE d4.name = :param_3\n" + + "ORDER BY docName DESC" + " LIMIT 1)\n" - + "ORDER BY docName DESC NULLS LAST" + + "ORDER BY docName DESC" + " LIMIT 1"; assertEquals(expected, cb.getQueryString()); @@ -1011,18 +1011,18 @@ public void testSubqueryOrderByLimit() { .where("d1.name").eq("D1") .union() .from(Document.class, "d2") - .select("d2.name") + .select("d2.name", "docName") .where("d2.name").eq("D2") .startExcept() .from(Document.class, "d3") - .select("d3.name", "dName") + .select("d3.name", "docName") .where("d3.name").eq("D2") .union() .from(Document.class, "d4") - .select("d4.name") + .select("d4.name", "docName") .where("d4.name").eq("D3") .endSetWith() - .orderByDesc("dName") + .orderByDesc("docName") .setMaxResults(1) .endSet() .endSet() @@ -1036,19 +1036,19 @@ public void testSubqueryOrderByLimit() { function( "SET_UNION", "(SELECT d1.name AS docName FROM Document d1 WHERE d1.name = :param_0)", - "(SELECT d2.name FROM Document d2 WHERE d2.name = :param_1)" + "(SELECT d2.name AS docName FROM Document d2 WHERE d2.name = :param_1)" ), function( "SET_UNION", - "(SELECT d3.name AS dName FROM Document d3 WHERE d3.name = :param_2)", - "(SELECT d4.name FROM Document d4 WHERE d4.name = :param_3)", + "(SELECT d3.name AS docName FROM Document d3 WHERE d3.name = :param_2)", + "(SELECT d4.name AS docName FROM Document d4 WHERE d4.name = :param_3)", "'ORDER_BY'", - "'1 DESC NULLS LAST'", + "'1 DESC'", "'LIMIT'", "1" ), "'ORDER_BY'", - "'1 DESC NULLS LAST'", + "'1 DESC'", "'LIMIT'", "1" ) @@ -1079,7 +1079,7 @@ public void testWithStartSetEmpty() { + "WITH IdHolderCTE(id) AS(\n" + "SELECT d.id FROM Document d\n" + ")\n" - + "SELECT idHolderCTE FROM IdHolderCTE idHolderCTE ORDER BY " + renderNullPrecedence("idHolderCTE.id", "ASC", "LAST"); + + "SELECT idHolderCTE FROM IdHolderCTE idHolderCTE ORDER BY idHolderCTE.id ASC"; assertEquals(expected, cb.getQueryString()); final List resultList = cb.getResultList(); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SizeTransformationTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SizeTransformationTest.java index 710a85f735..86c454d8a6 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SizeTransformationTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SizeTransformationTest.java @@ -257,7 +257,7 @@ public void work(EntityManager em) { .orderByAsc("ownedDocument.id"); String expectedQuery = "SELECT p.id, ownedDocument.id, " + function("COUNT_TUPLE", "'DISTINCT'", "versions_1.id") + ", (SELECT " + countStar() + " FROM p.ownedDocuments document) FROM Person p LEFT JOIN p.ownedDocuments ownedDocument LEFT JOIN ownedDocument.versions versions_1 GROUP BY " + groupBy("ownedDocument.id", "p.id", renderNullPrecedenceGroupBy("p.id"), renderNullPrecedenceGroupBy("ownedDocument.id")) + - " ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST") + ", " + renderNullPrecedence("ownedDocument.id", "ASC", "LAST"); + " ORDER BY p.id ASC, " + renderNullPrecedence("ownedDocument.id", "ASC", "LAST"); Assert.assertEquals(expectedQuery, cb.getQueryString()); List result = cb.getResultList(); Assert.assertEquals(3, result.size()); @@ -277,7 +277,7 @@ public void testSizeToCountTransformationMultiLevel2() { .orderByAsc("p.id"); String expectedQuery = "SELECT p.id, " + function("COUNT_TUPLE", "'DISTINCT'", "versions_1.id") + ", (SELECT " + countStar() + " FROM p.ownedDocuments document) FROM Person p LEFT JOIN p.ownedDocuments ownedDocument LEFT JOIN ownedDocument.versions versions_1 GROUP BY " + groupBy("ownedDocument.id", "p.id", renderNullPrecedenceGroupBy("p.id")) + - " ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + " ORDER BY p.id ASC"; Assert.assertEquals(expectedQuery, cb.getQueryString()); cb.getResultList(); @@ -298,7 +298,7 @@ public void testSizeToCountTransformationSubqueryCorrelationGroupBy() { "LEFT JOIN ownedDocument.partners partner " + "LEFT JOIN partner.favoriteDocuments favoriteDocuments_1 " + "GROUP BY " + groupBy("partner.id", "p.id", renderNullPrecedenceGroupBy("p.id"), "ownedDocument.id") + - " ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST"); + " ORDER BY p.id ASC"; Assert.assertEquals(expectedQuery, cb.getQueryString()); cb.getResultList(); @@ -320,7 +320,7 @@ public void testSizeToCountTransformationMultiBranches() { .orderByAsc("favoriteDocument.id"); String expectedQuery = "SELECT p.id, ownedDocument.id, favoriteDocument.id, (SELECT " + countStar() + " FROM ownedDocument.versions version), (SELECT " + countStar() + " FROM favoriteDocument.versions version), (SELECT " + countStar() + " FROM p.ownedDocuments document) FROM Person p LEFT JOIN p.favoriteDocuments favoriteDocument LEFT JOIN p.ownedDocuments ownedDocument " + - "ORDER BY " + renderNullPrecedence("p.id", "ASC", "LAST") + ", " + renderNullPrecedence("ownedDocument.id", "ASC", "LAST") + ", " + renderNullPrecedence("favoriteDocument.id", "ASC", "LAST"); + "ORDER BY p.id ASC, " + renderNullPrecedence("ownedDocument.id", "ASC", "LAST") + ", " + renderNullPrecedence("favoriteDocument.id", "ASC", "LAST"); Assert.assertEquals(expectedQuery, cb.getQueryString()); } diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java index 0705869a6a..7cb425ef7f 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/SubqueryTest.java @@ -167,7 +167,7 @@ public void testMultipleSubqueriesWithSelectAliases() { String expected = "SELECT d.name AS n FROM Document d WHERE d.id IN " + "(SELECT person.id FROM Person person WHERE person.name = d.name) AND d.id NOT IN " + "(SELECT person.id FROM Person person WHERE d.name LIKE person.name) " - + "ORDER BY " + renderNullPrecedence("n", "d.name", "ASC", "LAST"); + + "ORDER BY n ASC"; assertEquals(expected, crit.getQueryString()); crit.getResultList(); @@ -285,6 +285,18 @@ public void render(FunctionRenderContext context) { crit.getResultList(); } + @Test + public void testSubqueryCorrelatesOverRelation() { + CriteriaBuilder crit = cbf.create(em, Document.class, "d") + .whereExists() + .from("d.owner.ownedDocuments", "dSub") + .where("dSub").notEqExpression("d") + .end(); + String expectedQuery = "SELECT d FROM Document d JOIN d.owner owner_1 WHERE EXISTS (SELECT 1 FROM owner_1.ownedDocuments dSub WHERE dSub <> d)"; + assertEquals(expectedQuery, crit.getQueryString()); + crit.getResultList(); + } + @Test // TODO: Report datanucleus issue @Category({ NoDatanucleus.class }) @@ -369,7 +381,7 @@ public void testSubqueryUsesOuterJoin() { String expectedSubQuery = "ABS((SELECT COUNT(" + joinAliasValue("localized_1") + ") FROM Person p LEFT JOIN p.localized localized_1 WHERE p.id = " + joinAliasValue("c", "id") + "))"; String expectedQuery = "SELECT d.id, " + expectedSubQuery + " AS localizedCount " - + "FROM Document d LEFT JOIN d.contacts c GROUP BY d.id, " + joinAliasValue("c", "id") + " ORDER BY " + renderNullPrecedence("localizedCount", expectedSubQuery, "ASC", "LAST"); + + "FROM Document d LEFT JOIN d.contacts c GROUP BY d.id, " + joinAliasValue("c", "id") + " ORDER BY localizedCount ASC"; assertEquals(expectedQuery, cb.getQueryString()); cb.getResultList(); } @@ -388,7 +400,7 @@ public void testSubqueryAddsJoin() { String expectedSubQuery = "ABS((SELECT COUNT(" + joinAliasValue("localized_1") + ") FROM Person p LEFT JOIN p.localized localized_1 WHERE p.id = " + joinAliasValue("contacts_1", "id") + "))"; String expectedQuery = "SELECT d.id, " + expectedSubQuery + " AS localizedCount " - + "FROM Document d LEFT JOIN d.contacts contacts_1 GROUP BY d.id, " + joinAliasValue("contacts_1", "id") + " ORDER BY " + renderNullPrecedence("localizedCount", expectedSubQuery, "ASC", "LAST"); + + "FROM Document d LEFT JOIN d.contacts contacts_1 GROUP BY d.id, " + joinAliasValue("contacts_1", "id") + " ORDER BY localizedCount ASC"; assertEquals(expectedQuery, cb.getQueryString()); cb.getResultList(); } @@ -466,9 +478,9 @@ public void testEquallyAliasedSingleValuedAssociationSelectInSubqueryAsInParentQ .orderByAsc("id") .page(0, 10); - String expectedIdQuery = "SELECT document.id FROM Document document" + singleValuedAssociationIdJoin("document.owner", "owner_1", false) + " WHERE " + singleValuedAssociationIdPath("document.owner.id", "owner_1") + " = :param_0 AND document.id NOT IN (SELECT " + singleValuedAssociationIdPath("versions_1.document.id", "document_1") + " FROM Document c2 LEFT JOIN c2.versions versions_1" + singleValuedAssociationIdJoin("versions_1.document", "document_1", true) + " WHERE c2.id = :param_1) GROUP BY " + groupBy("document.id", renderNullPrecedenceGroupBy("document.id")) + " ORDER BY " + renderNullPrecedence("document.id", "ASC", "LAST"); + String expectedIdQuery = "SELECT document.id FROM Document document" + singleValuedAssociationIdJoin("document.owner", "owner_1", false) + " WHERE " + singleValuedAssociationIdPath("document.owner.id", "owner_1") + " = :param_0 AND document.id NOT IN (SELECT " + singleValuedAssociationIdPath("versions_1.document.id", "document_1") + " FROM Document c2 LEFT JOIN c2.versions versions_1" + singleValuedAssociationIdJoin("versions_1.document", "document_1", true) + " WHERE c2.id = :param_1) GROUP BY " + groupBy("document.id", "document.id") + " ORDER BY document.id ASC"; String expectedCountQuery = "SELECT " + countPaginated("document.id", false) + " FROM Document document" + singleValuedAssociationIdJoin("document.owner", "owner_1", false) + " WHERE " + singleValuedAssociationIdPath("document.owner.id", "owner_1") + " = :param_0 AND document.id NOT IN (SELECT " + singleValuedAssociationIdPath("versions_1.document.id", "document_1") + " FROM Document c2 LEFT JOIN c2.versions versions_1" + singleValuedAssociationIdJoin("versions_1.document", "document_1", true) + " WHERE c2.id = :param_1)"; - String expectedObjectQuery = "SELECT document FROM Document document" + singleValuedAssociationIdJoin("document.owner", "owner_1", false) + " WHERE " + singleValuedAssociationIdPath("document.owner.id", "owner_1") + " = :param_0 AND document.id NOT IN (SELECT " + singleValuedAssociationIdPath("versions_1.document.id", "document_1") + " FROM Document c2 LEFT JOIN c2.versions versions_1" + singleValuedAssociationIdJoin("versions_1.document", "document_1", true) + " WHERE c2.id = :param_1) ORDER BY " + renderNullPrecedence("document.id", "ASC", "LAST"); + String expectedObjectQuery = "SELECT document FROM Document document" + singleValuedAssociationIdJoin("document.owner", "owner_1", false) + " WHERE " + singleValuedAssociationIdPath("document.owner.id", "owner_1") + " = :param_0 AND document.id NOT IN (SELECT " + singleValuedAssociationIdPath("versions_1.document.id", "document_1") + " FROM Document c2 LEFT JOIN c2.versions versions_1" + singleValuedAssociationIdJoin("versions_1.document", "document_1", true) + " WHERE c2.id = :param_1) ORDER BY document.id ASC"; assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); assertEquals(expectedCountQuery, pcb.getPageCountQueryString()); assertEquals(expectedObjectQuery, pcb.getQueryString()); @@ -484,9 +496,9 @@ public void testRequirementForFullyQualifyingSubqueryAlias() { .where("version.document.id").eqExpression("OUTER(id)") // we have to fully qualify version.document.id .end().orderByAsc("id").page(0, 10); - String expectedIdQuery = "SELECT document.id FROM Document document GROUP BY " + groupBy("document.id", renderNullPrecedenceGroupBy("document.id")) + " ORDER BY " + renderNullPrecedence("document.id", "ASC", "LAST"); + String expectedIdQuery = "SELECT document.id FROM Document document GROUP BY " + groupBy("document.id", "document.id") + " ORDER BY document.id ASC"; String expectedCountQuery = "SELECT " + countPaginated("document.id", false) + " FROM Document document"; - String expectedObjectQuery = "SELECT (SELECT COUNT(version.id) FROM Version version" + singleValuedAssociationIdJoin("version.document", "document_1", true) + " WHERE " + singleValuedAssociationIdPath("version.document.id", "document_1") + " = document.id) FROM Document document ORDER BY " + renderNullPrecedence("document.id", "ASC", "LAST"); + String expectedObjectQuery = "SELECT (SELECT COUNT(version.id) FROM Version version" + singleValuedAssociationIdJoin("version.document", "document_1", true) + " WHERE " + singleValuedAssociationIdPath("version.document.id", "document_1") + " = document.id) FROM Document document ORDER BY document.id ASC"; assertEquals(expectedIdQuery, pcb.getPageIdQueryString()); assertEquals(expectedCountQuery, pcb.getPageCountQueryString()); assertEquals(expectedObjectQuery, pcb.getQueryString()); diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/UpdateTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/UpdateTest.java index a1acdeb204..fea7f4bd04 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/UpdateTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/UpdateTest.java @@ -223,7 +223,7 @@ public void work(EntityManager em) { .select("idHolder.id") .end(); String expected = "WITH IdHolderCTE(id) AS(\n" - + "SELECT subDoc.id FROM Document subDoc ORDER BY " + renderNullPrecedence("subDoc.id", "ASC", "LAST") + " LIMIT 2\n" + + "SELECT subDoc.id FROM Document subDoc ORDER BY subDoc.id ASC LIMIT 2\n" + ")\n" + "UPDATE Document d SET d.name = :param_0 WHERE d.id IN (SELECT idHolder.id FROM IdHolderCTE idHolder)"; diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java index cc5c056f45..43da78b0ae 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/ValuesClauseTest.java @@ -194,7 +194,7 @@ public void testValuesEntityFunctionLeftJoin() { String expected = "" + "SELECT TREAT_LONG(allowedAge.value), doc.name FROM Long(3 VALUES) allowedAge LEFT JOIN Document doc" + onClause("TREAT_LONG(allowedAge.value) = :allowedAge_value_0 OR TREAT_LONG(allowedAge.value) = :allowedAge_value_1 OR TREAT_LONG(allowedAge.value) = :allowedAge_value_2 AND doc.age = TREAT_LONG(allowedAge.value)") + - " ORDER BY " + renderNullPrecedence("TREAT_LONG(allowedAge.value)", "ASC", "LAST"); + " ORDER BY TREAT_LONG(allowedAge.value) ASC"; assertEquals(expected, cb.getQueryString()); List resultList = cb.getResultList(); diff --git a/documentation/src/main/asciidoc/core/manual/en_US/11_expressions.adoc b/documentation/src/main/asciidoc/core/manual/en_US/11_expressions.adoc index 438bf9adeb..20d744c93a 100644 --- a/documentation/src/main/asciidoc/core/manual/en_US/11_expressions.adoc +++ b/documentation/src/main/asciidoc/core/manual/en_US/11_expressions.adoc @@ -645,7 +645,7 @@ For further information on `OUTER` take a look into the < T find(EntityManager entityManager, Class entityViewClass, Object public T find(EntityManager entityManager, EntityViewSetting> entityViewSetting, Object entityId) { ViewTypeImpl managedViewType = metamodel.view(entityViewSetting.getEntityViewClass()); EntityType entityType = (EntityType) managedViewType.getJpaManagedType(); - SingularAttribute idAttribute = JpaMetamodelUtils.getIdAttribute(entityType); + SingularAttribute idAttribute = JpaMetamodelUtils.getSingleIdAttribute(entityType); CriteriaBuilder cb = cbf.create(entityManager, managedViewType.getEntityClass()) .where(idAttribute.getName()).eq(entityId); List resultList = applySetting(entityViewSetting, cb).getResultList(); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityLoader.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityLoader.java index 611987b108..96ca848f62 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityLoader.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/AbstractEntityLoader.java @@ -59,7 +59,7 @@ public AbstractEntityLoader(Class entityClass, javax.persistence.metamodel.Si protected static javax.persistence.metamodel.SingularAttribute jpaIdOf(EntityViewManagerImpl evm, ManagedViewType subviewType) { if (subviewType instanceof ViewType) { - return JpaMetamodelUtils.getIdAttribute(evm.getMetamodel().getEntityMetamodel().entity(subviewType.getEntityClass())); + return JpaMetamodelUtils.getSingleIdAttribute(evm.getMetamodel().getEntityMetamodel().entity(subviewType.getEntityClass())); } return null; } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityLoaderFetchGraphNode.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityLoaderFetchGraphNode.java index 92bea3a081..4eada021b9 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityLoaderFetchGraphNode.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/DefaultEntityLoaderFetchGraphNode.java @@ -45,7 +45,7 @@ public class DefaultEntityLoaderFetchGraphNode extends AbstractEntityLoader impl public DefaultEntityLoaderFetchGraphNode(EntityViewManagerImpl evm, String attributeName, EntityType entityType, Map> fetchGraph) { // TODO: view id mapper?! - super(entityType.getJavaType(), JpaMetamodelUtils.getIdAttribute(entityType), null, evm.getEntityIdAccessor()); + super(entityType.getJavaType(), JpaMetamodelUtils.getSingleIdAttribute(entityType), null, evm.getEntityIdAccessor()); this.attributeName = attributeName; this.fetchGraph = fetchGraph; this.queryString = createQueryString(evm, entityType, fetchGraph); @@ -101,7 +101,7 @@ private String createQueryString(EntityViewManagerImpl evm, EntityType entity } else { return cbf.create(em, entityClass) .fetch(paths) - .where(JpaMetamodelUtils.getIdAttribute(entityType).getName()).eqExpression(":id") + .where(JpaMetamodelUtils.getSingleIdAttribute(entityType).getName()).eqExpression(":id") .getQueryString(); } } finally { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/FullEntityLoader.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/FullEntityLoader.java index 8c11d25c1c..f425fc0675 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/FullEntityLoader.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/FullEntityLoader.java @@ -73,7 +73,7 @@ private String createQueryString(EntityViewManagerImpl evm, ManagedViewType s } else { return cbf.create(em, entityClass) .fetch(fetchJoinableRelations.toArray(new String[fetchJoinableRelations.size()])) - .where(JpaMetamodelUtils.getIdAttribute(entityType).getName()).eqExpression(":id") + .where(JpaMetamodelUtils.getSingleIdAttribute(entityType).getName()).eqExpression(":id") .getQueryString(); } } finally { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java index 39ce3ddc81..a2d8f1de51 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseEntityToEntityMapper.java @@ -43,7 +43,7 @@ public class InverseEntityToEntityMapper implements InverseElementToEntityMap public InverseEntityToEntityMapper(EntityViewManagerImpl evm, EntityType entityType, Mapper parentEntityOnChildEntityMapper, DirtyAttributeFlusher inverseAttributeFlusher) { this.updatePrefixString = "UPDATE " + entityType.getName() + " e SET "; - this.updatePostfixString = " WHERE e." + JpaMetamodelUtils.getIdAttribute(entityType).getName() + " = :" + ID_PARAM_NAME; + this.updatePostfixString = " WHERE e." + JpaMetamodelUtils.getSingleIdAttribute(entityType).getName() + " = :" + ID_PARAM_NAME; this.parentEntityOnChildEntityMapper = parentEntityOnChildEntityMapper; this.inverseAttributeFlusher = inverseAttributeFlusher; this.fullUpdateQueryString = createQueryString(null, inverseAttributeFlusher); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java index 8a7f07f1b7..f783d36aed 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/entity/InverseViewToEntityMapper.java @@ -62,7 +62,7 @@ public InverseViewToEntityMapper(EntityViewManagerImpl evm, ViewType childVie this.parentEntityOnChildViewMapper = parentEntityOnChildViewMapper; this.parentEntityOnChildEntityMapper = parentEntityOnChildEntityMapper; this.updatePrefixString = "UPDATE " + entityType.getName() + " e SET "; - this.updatePostfixString = " WHERE e." + JpaMetamodelUtils.getIdAttribute(entityType).getName() + " = :" + ID_PARAM_NAME; + this.updatePostfixString = " WHERE e." + JpaMetamodelUtils.getSingleIdAttribute(entityType).getName() + " = :" + ID_PARAM_NAME; this.parentReferenceAttributeFlusher = parentReferenceAttributeFlusher; this.idAttributeFlusher = idAttributeFlusher; this.fullUpdateQueryString = createQueryString(null, parentReferenceAttributeFlusher); diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/mapper/TupleElementMapperBuilder.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/mapper/TupleElementMapperBuilder.java index dca8bc492b..c43400513a 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/mapper/TupleElementMapperBuilder.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/mapper/TupleElementMapperBuilder.java @@ -153,7 +153,7 @@ private String getMapping(String prefixParts, String mapping, Class expressio return getMapping(prefixParts, mapping); } - javax.persistence.metamodel.SingularAttribute idAttr = JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedType); + javax.persistence.metamodel.SingularAttribute idAttr = JpaMetamodelUtils.getSingleIdAttribute((IdentifiableType) managedType); if (mapping.isEmpty()) { return getMapping(prefixParts, idAttr.getName()); } else { diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/transformer/correlation/AbstractCorrelatedTupleListTransformer.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/transformer/correlation/AbstractCorrelatedTupleListTransformer.java index 7e67d6c91d..393a318c27 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/transformer/correlation/AbstractCorrelatedTupleListTransformer.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/objectbuilder/transformer/correlation/AbstractCorrelatedTupleListTransformer.java @@ -94,7 +94,7 @@ public AbstractCorrelatedTupleListTransformer(ExpressionFactory ef, Correlator c protected String getEntityIdName(Class entityClass) { ManagedType managedType = entityViewConfiguration.getCriteriaBuilder().getMetamodel().managedType(entityClass); if (managedType instanceof IdentifiableType) { - return JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedType).getName(); + return JpaMetamodelUtils.getSingleIdAttribute((IdentifiableType) managedType).getName(); } else { return null; } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java index 3960acc31c..4a2d09e748 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/proxy/ProxyFactory.java @@ -1424,12 +1424,10 @@ private CtMethod addSetter(AbstractMethodAttribute attribute, CtClass cc, } else { SingularAttribute singularAttribute = (SingularAttribute) attribute; Type type = singularAttribute.getType(); - sb.append(" && "); if (attribute.isSubview()) { - if (type instanceof FlatViewType) { - sb.append("true"); - } else { + if (!(type instanceof FlatViewType)) { String idMethodName = ((ViewType) type).getIdAttribute().getJavaMethod().getName(); + sb.append(" && "); sb.append("($1 == null || (tmp = $1."); sb.append(idMethodName); sb.append("()) == null || !java.util.Objects.equals(tmp, $0."); @@ -1440,20 +1438,21 @@ private CtMethod addSetter(AbstractMethodAttribute attribute, CtClass cc, } else { BasicTypeImpl basicType = (BasicTypeImpl) type; boolean jpaEntity = basicType.isJpaEntity(); - if (!jpaEntity) { - sb.append("true"); - } else { + if (jpaEntity) { IdentifiableType identifiableType = (IdentifiableType) basicType.getManagedType(); - javax.persistence.metamodel.SingularAttribute idAttribute = JpaMetamodelUtils.getIdAttribute(identifiableType); - Class idClass = JpaMetamodelUtils.resolveFieldClass(basicType.getJavaType(), idAttribute); - String idAccessor = addIdAccessor(cc, identifiableType, idAttribute, pool.get(idClass.getName())); - sb.append("($1 == null || (tmp = "); - sb.append(idAccessor); - sb.append("($1)) == null || !java.util.Objects.equals(tmp, "); - sb.append(idAccessor); - sb.append("($0."); - sb.append(fieldName); - sb.append(")))"); + + for (javax.persistence.metamodel.SingularAttribute idAttribute : JpaMetamodelUtils.getIdAttributes(identifiableType)) { + Class idClass = JpaMetamodelUtils.resolveFieldClass(basicType.getJavaType(), idAttribute); + String idAccessor = addIdAccessor(cc, identifiableType, idAttribute, pool.get(idClass.getName())); + sb.append(" && "); + sb.append("($1 == null || (tmp = "); + sb.append(idAccessor); + sb.append("($1)) == null || !java.util.Objects.equals(tmp, "); + sb.append(idAccessor); + sb.append("($0."); + sb.append(fieldName); + sb.append(")))"); + } } } } diff --git a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java index 4c4e1ed4cc..bcf74be145 100644 --- a/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java +++ b/entity-view/impl/src/main/java/com/blazebit/persistence/view/impl/update/EntityViewUpdaterImpl.java @@ -194,7 +194,7 @@ public EntityViewUpdaterImpl(EntityViewManagerImpl evm, ManagedViewTypeImplement idAttribute = (AbstractMethodAttribute) ((ViewType) viewType).getIdAttribute(); versionAttribute = (AbstractMethodAttribute) ((ViewType) viewType).getVersionAttribute(); versionFlusher = versionAttribute != null ? createVersionFlusher(evm, entityType, versionAttribute) : null; - jpaIdAttribute = JpaMetamodelUtils.getIdAttribute(entityMetamodel.entity(entityClass)); + jpaIdAttribute = JpaMetamodelUtils.getSingleIdAttribute(entityMetamodel.entity(entityClass)); idAttributeName = jpaIdAttribute.getName(); String mapping = idAttribute.getMapping(); // Read only entity views don't have this restriction @@ -508,7 +508,7 @@ private static void buildComponentFlushers(EntityMetamodel metamodel, Class r if (managedType instanceof EmbeddableType) { subAttributes = (Set>) (Set) managedType.getAttributes(); } else { - subAttributes = (Set>) (Set) Collections.singleton(JpaMetamodelUtils.getIdAttribute((IdentifiableType) managedType)); + subAttributes = (Set>) (Set) Collections.singleton(JpaMetamodelUtils.getSingleIdAttribute((IdentifiableType) managedType)); } buildComponentFlushers( metamodel, diff --git a/integration/deltaspike-data/impl-1.7/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java b/integration/deltaspike-data/impl-1.7/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java index 195b3afbd3..b474cc0ecf 100644 --- a/integration/deltaspike-data/impl-1.7/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java +++ b/integration/deltaspike-data/impl-1.7/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java @@ -96,6 +96,6 @@ protected String entityName() { protected String idAttribute() { Class entityClass = context.getEntityViewManager().getMetamodel().view(viewClass()).getEntityClass(); EntityType entityType = context.getEntityManager().getMetamodel().entity(entityClass); - return JpaMetamodelUtils.getIdAttribute(entityType).getName(); + return JpaMetamodelUtils.getSingleIdAttribute(entityType).getName(); } } \ No newline at end of file diff --git a/integration/deltaspike-data/impl-1.8/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java b/integration/deltaspike-data/impl-1.8/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java index 2d1163ac28..4034e98f27 100644 --- a/integration/deltaspike-data/impl-1.8/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java +++ b/integration/deltaspike-data/impl-1.8/src/main/java/com/blazebit/persistence/deltaspike/data/impl/handler/EntityViewRepositoryHandler.java @@ -96,6 +96,6 @@ protected String entityName() { protected String idAttribute() { Class entityClass = context.getEntityViewManager().getMetamodel().view(viewClass()).getEntityClass(); EntityType entityType = context.getEntityManager().getMetamodel().entity(entityClass); - return JpaMetamodelUtils.getIdAttribute(entityType).getName(); + return JpaMetamodelUtils.getSingleIdAttribute(entityType).getName(); } } \ No newline at end of file diff --git a/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpa21Provider.java b/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpa21Provider.java index 6f853d0386..a16a8bbb8e 100644 --- a/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpa21Provider.java +++ b/integration/hibernate-base/src/main/java/com/blazebit/persistence/integration/hibernate/base/HibernateJpa21Provider.java @@ -55,13 +55,15 @@ public HibernateJpa21Provider(PersistenceUnitUtil persistenceUnitUtil, String db @Override public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { AbstractEntityPersister entityPersister = getEntityPersister(ownerType); - EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); - Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); - if (index != null) { - try { - return (boolean) HAS_ORPHAN_DELETE_METHOD.invoke(entityMetamodel.getCascadeStyles()[index]); - } catch (Exception ex) { - throw new RuntimeException("Could not access orphan removal information. Please report your version of hibernate so we can provide support for it!", ex); + if (entityPersister != null) { + EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); + Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); + if (index != null) { + try { + return (boolean) HAS_ORPHAN_DELETE_METHOD.invoke(entityMetamodel.getCascadeStyles()[index]); + } catch (Exception ex) { + throw new RuntimeException("Could not access orphan removal information. Please report your version of hibernate so we can provide support for it!", ex); + } } } @@ -71,13 +73,15 @@ public boolean isOrphanRemoval(ManagedType ownerType, String attributeName) { @Override public boolean isDeleteCascaded(ManagedType ownerType, String attributeName) { AbstractEntityPersister entityPersister = getEntityPersister(ownerType); - EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); - Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); - if (index != null) { - try { - return (boolean) DO_CASCADE_METHOD.invoke(entityMetamodel.getCascadeStyles()[index], DELETE_CASCADE); - } catch (Exception ex) { - throw new RuntimeException("Could not access orphan removal information. Please report your version of hibernate so we can provide support for it!", ex); + if (entityPersister != null) { + EntityMetamodel entityMetamodel = entityPersister.getEntityMetamodel(); + Integer index = entityMetamodel.getPropertyIndexOrNull(attributeName); + if (index != null) { + try { + return (boolean) DO_CASCADE_METHOD.invoke(entityMetamodel.getCascadeStyles()[index], DELETE_CASCADE); + } catch (Exception ex) { + throw new RuntimeException("Could not access orphan removal information. Please report your version of hibernate so we can provide support for it!", ex); + } } } diff --git a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/OrderByTest.java b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/OrderByTest.java index 65b9ae11c1..262e114fbc 100644 --- a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/OrderByTest.java +++ b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/OrderByTest.java @@ -61,14 +61,14 @@ public void simpleDescAscOrderBy() { assertEquals("SELECT (" + "SELECT sub.id FROM Document sub ORDER BY " + renderNullPrecedence("sub.creationDate", "DESC", "LAST") + ", " + - renderNullPrecedence("sub.id", "ASC", "LAST") + ", " + + "sub.id ASC, " + renderNullPrecedence("sub.creationDate", "DESC", "FIRST") + ", " + - renderNullPrecedence("sub.id", "ASC", "FIRST") + + "sub.id ASC" + ") FROM Document document ORDER BY " + renderNullPrecedence("document.creationDate", "DESC", "LAST") + ", " + - renderNullPrecedence("document.id", "ASC", "LAST") + ", " + + "document.id ASC, " + renderNullPrecedence("document.creationDate", "DESC", "FIRST") + ", " + - renderNullPrecedence("document.id", "ASC", "FIRST"), + "document.id ASC", criteriaBuilder.getQueryString()); }