From 44b227f3c5895457d4b754b257032be521d825f9 Mon Sep 17 00:00:00 2001 From: Moritz Becker Date: Sun, 6 May 2018 20:59:36 +0200 Subject: [PATCH] [#513] fixes #513, prevent literal group bys, fixed test logging --- .../impl/AbstractCommonQueryBuilder.java | 2 +- .../CriteriaBuilderConfigurationImpl.java | 7 + .../impl/CriteriaBuilderFactoryImpl.java | 6 + .../persistence/impl/ExpressionUtils.java | 6 +- .../GroupByExpressionGatheringVisitor.java | 7 +- .../blazebit/persistence/impl/MainQuery.java | 4 + .../function/subquery/SubqueryFunction.java | 52 +++ .../transform/SizeTransformationVisitor.java | 47 ++- .../impl/transform/SizeTransformerGroup.java | 5 +- .../CombiningResultVisitorAdapter.java | 298 ++++++++++++++++++ .../parser/expression/EntityLiteral.java | 3 +- .../parser/expression/EnumLiteral.java | 3 +- .../parser/expression/LiteralExpression.java | 26 ++ .../parser/expression/NullExpression.java | 6 +- .../parser/expression/NumericLiteral.java | 3 +- .../parser/expression/StringLiteral.java | 3 +- .../parser/expression/TemporalLiteral.java | 3 +- core/testsuite/.gitignore | 1 + .../persistence/testsuite/CTETest.java | 23 +- .../persistence/testsuite/GroupByTest.java | 12 + .../testsuite/SizeTransformationTest.java | 41 +++ .../core/manual/en_US/18_jpql_functions.adoc | 8 + .../JpqlFunctionExpressionOperator.java | 5 +- .../persistence/criteria/SubqueryTest.java | 2 +- parent/pom.xml | 2 +- roadmap.asciidoc | 2 +- .../base/jpa/AbstractJpaPersistenceTest.java | 4 + website/pom.xml | 7 +- 28 files changed, 561 insertions(+), 27 deletions(-) create mode 100644 core/impl/src/main/java/com/blazebit/persistence/impl/function/subquery/SubqueryFunction.java create mode 100644 core/parser/src/main/java/com/blazebit/persistence/parser/expression/CombiningResultVisitorAdapter.java create mode 100644 core/parser/src/main/java/com/blazebit/persistence/parser/expression/LiteralExpression.java create mode 100644 core/testsuite/.gitignore 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 756f74a442..ed76e28aea 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 @@ -262,7 +262,7 @@ protected AbstractCommonQueryBuilder(MainQuery mainQuery, boolean isMainQuery, D this.transformerGroups = Arrays.>asList( new SimpleTransformerGroup(new OuterFunctionVisitor(joinManager)), new SimpleTransformerGroup(new SubqueryRecursiveExpressionVisitor()), - new SizeTransformerGroup(sizeTransformationVisitor, orderByManager, selectManager, joinManager, groupByManager)); + new SizeTransformerGroup(sizeTransformationVisitor, orderByManager, selectManager, joinManager)); this.resultType = resultClazz; this.finalSetOperationBuilder = finalSetOperationBuilder; diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java index a79a24e9f4..b446f13acd 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderConfigurationImpl.java @@ -24,6 +24,7 @@ import com.blazebit.persistence.impl.dialect.MySQLDbmsDialect; import com.blazebit.persistence.impl.dialect.OracleDbmsDialect; import com.blazebit.persistence.impl.dialect.PostgreSQLDbmsDialect; +import com.blazebit.persistence.impl.function.subquery.SubqueryFunction; import com.blazebit.persistence.parser.expression.ConcurrentHashMapExpressionCache; import com.blazebit.persistence.impl.function.cast.CastFunction; import com.blazebit.persistence.impl.function.count.AbstractCountFunction; @@ -490,6 +491,12 @@ private void loadFunctions() { jpqlFunctionGroup.add("oracle", new LpadRepeatFunction()); jpqlFunctionGroup.add("microsoft", new ReplicateRepeatFunction()); registerFunction(jpqlFunctionGroup); + + // subquery + + jpqlFunctionGroup = new JpqlFunctionGroup(SubqueryFunction.FUNCTION_NAME, false); + jpqlFunctionGroup.add(null, new SubqueryFunction()); + registerFunction(jpqlFunctionGroup); } private void loadDbmsDialects() { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java index 224f075b8e..5cacb3a093 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/CriteriaBuilderFactoryImpl.java @@ -139,6 +139,12 @@ private static Set resolveAggregateFunctions(Map extractGroupByExpressions(Expression expression) { + // grouping by a literal does not make sense and even causes errors in some DBs such as PostgreSQL + if (expression instanceof LiteralExpression) { + return Collections.emptySet(); + } clear(); if (!dbmsDialect.supportsGroupByExpressionInHavingMatching()) { expression.accept(new VisitorAdapter() { diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java b/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java index 0762324b90..fac37a48fd 100644 --- a/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/MainQuery.java @@ -105,4 +105,8 @@ public QueryConfiguration getMutableQueryConfiguration() { public QueryConfiguration getQueryConfiguration() { return queryConfiguration; } + + public CriteriaBuilderFactoryImpl getCbf() { + return cbf; + } } diff --git a/core/impl/src/main/java/com/blazebit/persistence/impl/function/subquery/SubqueryFunction.java b/core/impl/src/main/java/com/blazebit/persistence/impl/function/subquery/SubqueryFunction.java new file mode 100644 index 0000000000..8b183357be --- /dev/null +++ b/core/impl/src/main/java/com/blazebit/persistence/impl/function/subquery/SubqueryFunction.java @@ -0,0 +1,52 @@ +/* + * 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.function.subquery; + +import com.blazebit.persistence.spi.FunctionRenderContext; +import com.blazebit.persistence.spi.JpqlFunction; + +/** + * This is an internal FUNCTION that is used to bypass the Hibernate parser for rendering subqueries as + * aggregate function arguments. + * + * @author Moritz Becker + * @since 1.2.0 + */ +public class SubqueryFunction implements JpqlFunction { + + public static final String FUNCTION_NAME = "subquery"; + + @Override + public boolean hasArguments() { + return true; + } + + @Override + public boolean hasParenthesesIfNoArguments() { + return true; + } + + @Override + public Class getReturnType(Class firstArgumentType) { + return firstArgumentType; + } + + @Override + public void render(FunctionRenderContext context) { + context.addArgument(0); + } +} 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 0a52d5a3fa..2e7b4dedb3 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 @@ -18,6 +18,7 @@ import com.blazebit.persistence.impl.AttributeHolder; import com.blazebit.persistence.impl.ClauseType; +import com.blazebit.persistence.impl.function.subquery.SubqueryFunction; import com.blazebit.persistence.parser.EntityMetamodel; import com.blazebit.persistence.impl.JoinManager; import com.blazebit.persistence.impl.JoinNode; @@ -80,6 +81,7 @@ public class SizeTransformationVisitor extends ExpressionModifierCollectingResul private JoinNode currentJoinNode; // size expressions with arguments having a blacklisted base node will become subqueries private Set joinNodeBlacklist = new HashSet<>(); + private boolean aggregateFunctionContext; public SizeTransformationVisitor(MainQuery mainQuery, SubqueryInitiatorFactory subqueryInitFactory, JoinManager joinManager, JpaProvider jpaProvider) { this.mainQuery = mainQuery; @@ -141,9 +143,17 @@ public Boolean visit(FunctionExpression expression) { if (clause != ClauseType.WHERE && ExpressionUtils.isSizeFunction(expression)) { return true; } - return super.visit(expression); + if (!aggregateFunctionContext && mainQuery.getCbf().getAggregateFunctions().contains(expression.getFunctionName().toLowerCase())) { + aggregateFunctionContext = true; + Boolean result = super.visit(expression); + aggregateFunctionContext = false; + return result; + } else { + return super.visit(expression); + } } + @Override protected void onModifier(ExpressionModifier parentModifier) { PathExpression sizeArg = (PathExpression) ((FunctionExpression) parentModifier.get()).getExpressions().get(0); parentModifier.set(getSizeExpression(parentModifier, sizeArg)); @@ -206,22 +216,24 @@ private Expression getSizeExpression(ExpressionModifier parentModifier, PathExpr // a subquery is required for bags when other collections are joined as well because we cannot rely on distinctness for bags // for now, we always generate a subquery when a bag is encountered jpaProvider.isBag((EntityType) targetAttribute.getDeclaringType(), targetAttribute.getName()) || - requiresBlacklistedNode(sizeArg); + requiresBlacklistedNode(sizeArg) || + aggregateFunctionContext; if (subqueryRequired) { - return generateSubquery(sizeArg); + return wrapSubqueryConditionally(generateSubquery(sizeArg), aggregateFunctionContext); } else { if (currentJoinNode != null && (!currentJoinNode.equals(sizeArgJoin))) { int currentJoinDepth = currentJoinNode.getJoinDepth(); int sizeArgJoinDepth = sizeArgJoin.getJoinDepth(); if (currentJoinDepth > sizeArgJoinDepth) { - return generateSubquery(sizeArg); + return wrapSubqueryConditionally(generateSubquery(sizeArg), aggregateFunctionContext); } else { // we have to change all transformed expressions to subqueries for (TransformedExpressionEntry transformedExpressionEntry : transformedExpressions) { PathExpression originalSizeArg = transformedExpressionEntry.getOriginalSizeArg(); - transformedExpressionEntry.getParentModifier().set(generateSubquery(originalSizeArg)); + Expression subquery = wrapSubqueryConditionally(generateSubquery(originalSizeArg), transformedExpressionEntry.isAggregateFunctionContext()); + transformedExpressionEntry.getParentModifier().set(subquery); } transformedExpressions.clear(); requiredGroupBys.clear(); @@ -229,7 +241,7 @@ private Expression getSizeExpression(ExpressionModifier parentModifier, PathExpr distinctRequired = false; if (currentJoinDepth == sizeArgJoinDepth) { - return generateSubquery(sizeArg); + return wrapSubqueryConditionally(generateSubquery(sizeArg), aggregateFunctionContext); } } } @@ -265,7 +277,7 @@ private Expression getSizeExpression(ExpressionModifier parentModifier, PathExpr countArguments.add(keyExpression); AggregateExpression countExpr = createCountFunction(distinctRequired, countArguments); - transformedExpressions.add(new TransformedExpressionEntry(countExpr, originalSizeArg, parentModifier)); + transformedExpressions.add(new TransformedExpressionEntry(countExpr, originalSizeArg, parentModifier, aggregateFunctionContext)); String joinLookupKey = getJoinLookupKey(sizeArg); LateJoinEntry lateJoin = lateJoins.get(joinLookupKey); @@ -342,6 +354,19 @@ private SubqueryExpression generateSubquery(PathExpression sizeArg) { return new SubqueryExpression(countSubquery); } + private Expression wrapSubqueryConditionally(SubqueryExpression subquery, boolean wrap) { + if (wrap) { + // we need to wrap subqueries in aggregate functions in COALESCE to trick the Hibernate parser + // see https://hibernate.atlassian.net/browse/HHH-9331 + List subqueryFunctionArguments = new ArrayList<>(1); + subqueryFunctionArguments.add(new StringLiteral(SubqueryFunction.FUNCTION_NAME)); + subqueryFunctionArguments.add(subquery); + return new FunctionExpression("FUNCTION", subqueryFunctionArguments); + } else { + return subquery; + } + } + /** * @author Christian Beikov * @since 1.2.0 @@ -350,11 +375,13 @@ private static class TransformedExpressionEntry { private final AggregateExpression transformedExpression; private final PathExpression originalSizeArg; private final ExpressionModifier parentModifier; + private final boolean aggregateFunctionContext; - public TransformedExpressionEntry(AggregateExpression transformedExpression, PathExpression originalSizeArg, ExpressionModifier parentModifier) { + public TransformedExpressionEntry(AggregateExpression transformedExpression, PathExpression originalSizeArg, ExpressionModifier parentModifier, boolean aggregateFunctionContext) { this.transformedExpression = transformedExpression; this.originalSizeArg = originalSizeArg; this.parentModifier = parentModifier; + this.aggregateFunctionContext = aggregateFunctionContext; } public AggregateExpression getTransformedExpression() { @@ -368,6 +395,10 @@ public PathExpression getOriginalSizeArg() { public ExpressionModifier getParentModifier() { return parentModifier; } + + public boolean isAggregateFunctionContext() { + return aggregateFunctionContext; + } } /** 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 57ff78d4df..5bd5486d01 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,7 +18,6 @@ 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; @@ -42,15 +41,13 @@ 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, GroupByManager groupByManager) { + public SizeTransformerGroup(SizeTransformationVisitor sizeTransformationVisitor, OrderByManager orderByManager, SelectManager selectManager, JoinManager joinManager) { this.sizeTransformationVisitor = sizeTransformationVisitor; this.selectManager = selectManager; this.joinManager = joinManager; - this.groupByManager = groupByManager; this.sizeExpressionTransformer = new SizeExpressionTransformer(sizeTransformationVisitor); this.sizeSelectExpressionTransformer = new SizeSelectInfoTransformer(sizeTransformationVisitor, orderByManager); } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/CombiningResultVisitorAdapter.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/CombiningResultVisitorAdapter.java new file mode 100644 index 0000000000..cc34fcf9db --- /dev/null +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/CombiningResultVisitorAdapter.java @@ -0,0 +1,298 @@ +/* + * 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.parser.expression; + +import com.blazebit.persistence.parser.predicate.BetweenPredicate; +import com.blazebit.persistence.parser.predicate.BinaryExpressionPredicate; +import com.blazebit.persistence.parser.predicate.BooleanLiteral; +import com.blazebit.persistence.parser.predicate.CompoundPredicate; +import com.blazebit.persistence.parser.predicate.EqPredicate; +import com.blazebit.persistence.parser.predicate.ExistsPredicate; +import com.blazebit.persistence.parser.predicate.GePredicate; +import com.blazebit.persistence.parser.predicate.GtPredicate; +import com.blazebit.persistence.parser.predicate.InPredicate; +import com.blazebit.persistence.parser.predicate.IsEmptyPredicate; +import com.blazebit.persistence.parser.predicate.IsNullPredicate; +import com.blazebit.persistence.parser.predicate.LePredicate; +import com.blazebit.persistence.parser.predicate.LikePredicate; +import com.blazebit.persistence.parser.predicate.LtPredicate; +import com.blazebit.persistence.parser.predicate.MemberOfPredicate; +import com.blazebit.persistence.parser.predicate.Predicate; + +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.2.0 + */ +public abstract class CombiningResultVisitorAdapter implements Expression.ResultVisitor { + + protected abstract T getDefaultResult(); + + protected abstract T updateResult(T result, T update); + + @Override + public T visit(PathExpression expression) { + T result = getDefaultResult(); + List expressions = expression.getExpressions(); + int size = expressions.size(); + for (int i = 0; i < size; i++) { + result = updateResult(result, expressions.get(i).accept(this)); + } + return result; + } + + @Override + public T visit(ArrayExpression expression) { + T result = expression.getBase().accept(this); + return updateResult(result, expression.getIndex().accept(this)); + } + + @Override + public T visit(TreatExpression expression) { + return expression.getExpression().accept(this); + } + + @Override + public T visit(PropertyExpression expression) { + return getDefaultResult(); + } + + @Override + public T visit(ParameterExpression expression) { + return getDefaultResult(); + } + + @Override + public T visit(ListIndexExpression expression) { + return expression.getPath().accept(this); + } + + @Override + public T visit(MapEntryExpression expression) { + return expression.getPath().accept(this); + } + + @Override + public T visit(MapKeyExpression expression) { + return expression.getPath().accept(this); + } + + @Override + public T visit(MapValueExpression expression) { + return expression.getPath().accept(this); + } + + @Override + public T visit(NullExpression expression) { + return getDefaultResult(); + } + + @Override + public T visit(SubqueryExpression expression) { + return getDefaultResult(); + } + + @Override + public T visit(FunctionExpression expression) { + T result = getDefaultResult(); + List expressions = expression.getExpressions(); + int size = expressions.size(); + for (int i = 0; i < size; i++) { + result = updateResult(result, expressions.get(i).accept(this)); + } + return result; + } + + @Override + public T visit(TypeFunctionExpression expression) { + return visit((FunctionExpression) expression); + } + + @Override + public T visit(TrimExpression expression) { + T result; + if (expression.getTrimCharacter() == null) { + result = getDefaultResult(); + } else { + result = expression.getTrimCharacter().accept(this); + } + return updateResult(result, expression.getTrimSource().accept(this)); + } + + @Override + public T visit(GeneralCaseExpression expression) { + T result = getDefaultResult(); + List expressions = expression.getWhenClauses(); + int size = expressions.size(); + for (int i = 0; i < size; i++) { + result = updateResult(result, expressions.get(i).accept(this)); + } + + if (expression.getDefaultExpr() != null) { + result = updateResult(result, expression.getDefaultExpr().accept(this)); + } + + return result; + } + + @Override + public T visit(SimpleCaseExpression expression) { + T result = expression.getCaseOperand().accept(this); + return updateResult(result, visit((GeneralCaseExpression) expression)); + } + + @Override + public T visit(WhenClauseExpression expression) { + T result = expression.getCondition().accept(this); + return updateResult(result, expression.getResult().accept(this)); + } + + @Override + public T visit(ArithmeticExpression expression) { + T result = expression.getLeft().accept(this); + return updateResult(result, expression.getRight().accept(this)); + } + + @Override + public T visit(ArithmeticFactor expression) { + return expression.getExpression().accept(this); + } + + @Override + public T visit(NumericLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(BooleanLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(StringLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(DateLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(TimeLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(TimestampLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(EnumLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(EntityLiteral expression) { + return getDefaultResult(); + } + + @Override + public T visit(CompoundPredicate predicate) { + T result = getDefaultResult(); + List children = predicate.getChildren(); + int size = children.size(); + for (int i = 0; i < size; i++) { + result = updateResult(result, children.get(i).accept(this)); + } + return result; + } + + @Override + public T visit(EqPredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + @Override + public T visit(IsNullPredicate predicate) { + return predicate.getExpression().accept(this); + } + + @Override + public T visit(IsEmptyPredicate predicate) { + return predicate.getExpression().accept(this); + } + + @Override + public T visit(MemberOfPredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + @Override + public T visit(LikePredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + @Override + public T visit(BetweenPredicate predicate) { + T result = predicate.getLeft().accept(this); + result = updateResult(result, predicate.getStart().accept(this)); + return updateResult(result, predicate.getEnd().accept(this)); + } + + @Override + public T visit(InPredicate predicate) { + T result = predicate.getLeft().accept(this); + for (Expression right : predicate.getRight()) { + result = updateResult(result, right.accept(this)); + } + return result; + } + + @Override + public T visit(GtPredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + @Override + public T visit(GePredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + @Override + public T visit(LtPredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + @Override + public T visit(LePredicate predicate) { + return visit((BinaryExpressionPredicate) predicate); + } + + protected T visit(BinaryExpressionPredicate predicate) { + T result = predicate.getLeft().accept(this); + return updateResult(result, predicate.getRight().accept(this)); + } + + @Override + public T visit(ExistsPredicate predicate) { + return getDefaultResult(); + } + +} \ No newline at end of file diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EntityLiteral.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EntityLiteral.java index 06df88fc92..2f304c3520 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EntityLiteral.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EntityLiteral.java @@ -21,7 +21,7 @@ * @author Moritz Becker * @since 1.2.0 */ -public class EntityLiteral extends AbstractExpression { +public class EntityLiteral extends AbstractExpression implements LiteralExpression> { private final Class value; private final String originalExpression; @@ -31,6 +31,7 @@ public EntityLiteral(Class value, String originalExpression) { this.originalExpression = originalExpression; } + @Override public Class getValue() { return value; } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EnumLiteral.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EnumLiteral.java index da3ef4d0fd..dd8d1eb8b3 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EnumLiteral.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/EnumLiteral.java @@ -21,7 +21,7 @@ * @author Moritz Becker * @since 1.2.0 */ -public class EnumLiteral extends AbstractExpression { +public class EnumLiteral extends AbstractExpression implements LiteralExpression> { private final Enum value; private final String originalExpression; @@ -31,6 +31,7 @@ public EnumLiteral(Enum value, String originalExpression) { this.originalExpression = originalExpression; } + @Override public Enum getValue() { return value; } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/LiteralExpression.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/LiteralExpression.java new file mode 100644 index 0000000000..0144c89c20 --- /dev/null +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/LiteralExpression.java @@ -0,0 +1,26 @@ +/* + * 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.parser.expression; + +/** + * @author Moritz Becker + * @since 1.2.0 + */ +public interface LiteralExpression { + + T getValue(); +} diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NullExpression.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NullExpression.java index 1f594bbc86..7687ec340c 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NullExpression.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NullExpression.java @@ -21,7 +21,7 @@ * @author Christian Beikov * @since 1.0.1 */ -public class NullExpression extends AbstractExpression { +public class NullExpression extends AbstractExpression implements LiteralExpression { @Override public NullExpression clone(boolean resolved) { @@ -53,4 +53,8 @@ public int hashCode() { return 31; } + @Override + public Object getValue() { + return null; + } } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NumericLiteral.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NumericLiteral.java index 4e0fc5cf0a..a88f9d2b55 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NumericLiteral.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/NumericLiteral.java @@ -21,7 +21,7 @@ * @author Moritz Becker * @since 1.2.0 */ -public class NumericLiteral extends AbstractNumericExpression { +public class NumericLiteral extends AbstractNumericExpression implements LiteralExpression { private final String value; @@ -30,6 +30,7 @@ public NumericLiteral(String value, NumericType numericType) { this.value = value; } + @Override public String getValue() { return value; } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/StringLiteral.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/StringLiteral.java index 1e7ac51d89..3447fe6552 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/StringLiteral.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/StringLiteral.java @@ -21,7 +21,7 @@ * @author Moritz Becker * @since 1.2.0 */ -public class StringLiteral extends AbstractExpression { +public class StringLiteral extends AbstractExpression implements LiteralExpression { private final String value; @@ -29,6 +29,7 @@ public StringLiteral(String value) { this.value = value; } + @Override public String getValue() { return value; } diff --git a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/TemporalLiteral.java b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/TemporalLiteral.java index 2fc3df906f..666c74ed95 100644 --- a/core/parser/src/main/java/com/blazebit/persistence/parser/expression/TemporalLiteral.java +++ b/core/parser/src/main/java/com/blazebit/persistence/parser/expression/TemporalLiteral.java @@ -23,7 +23,7 @@ * @author Moritz Becker * @since 1.2.0 */ -public abstract class TemporalLiteral extends AbstractExpression { +public abstract class TemporalLiteral extends AbstractExpression implements LiteralExpression { protected final Date value; @@ -31,6 +31,7 @@ public TemporalLiteral(Date value) { this.value = value; } + @Override public Date getValue() { return value; } diff --git a/core/testsuite/.gitignore b/core/testsuite/.gitignore new file mode 100644 index 0000000000..416395ffea --- /dev/null +++ b/core/testsuite/.gitignore @@ -0,0 +1 @@ +transaction.log \ No newline at end of file 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 3a06369e65..8b9073f055 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 @@ -40,6 +40,7 @@ import com.blazebit.persistence.testsuite.base.jpa.category.NoMySQL; import com.blazebit.persistence.testsuite.base.jpa.category.NoOpenJPA; import com.blazebit.persistence.testsuite.base.jpa.category.NoOracle; +import com.blazebit.persistence.testsuite.entity.Document; import com.blazebit.persistence.testsuite.entity.TestAdvancedCTE1; import com.blazebit.persistence.testsuite.entity.TestAdvancedCTE2; import com.blazebit.persistence.testsuite.tx.TxVoidWork; @@ -739,6 +740,27 @@ public void testBuilderEndTracking() { verifyException(cb, BuilderChainingException.class).end(); } + // from issue #513 + @Test + @Category({ NoDatanucleus.class, NoEclipselink.class, NoOpenJPA.class, NoMySQL.class }) + public void testNestedSizeInCte() { + CriteriaBuilder cb = cbf.create(em, Long.class) + .with(TestCTE.class) + .from(RecursiveEntity.class, "r") + .bind("id").select("r.id") + .bind("level").select("FUNCTION('greatest', SIZE(r.children), 1) * r.id") + .bind("name").select("''") + .end() + .from(TestCTE.class) + .select("level"); + + String expected = "WITH TestCTE(id, level, name) AS(\n" + + "SELECT r.id, " + function("greatest", function("count_tuple", "children_1.id"), "1") + " * r.id, '' FROM RecursiveEntity r LEFT JOIN r.children children_1 GROUP BY r.id\n" + + ")\n" + + "SELECT testCTE.level FROM TestCTE testCTE"; + assertEquals(expected, cb.getQueryString()); + } + private String innerJoin(String entityFragment, String onPredicate) { if (jpaProvider.supportsEntityJoin()) { return " JOIN " + entityFragment + onClause(onPredicate); @@ -762,5 +784,4 @@ private String innerJoinRecursive(String entityFragment, String onPredicate) { return ", " + entityFragment + " WHERE " + onPredicate; } } - } diff --git a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java index d428cc3bad..5042580c47 100644 --- a/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java +++ b/core/testsuite/src/test/java/com/blazebit/persistence/testsuite/GroupByTest.java @@ -105,4 +105,16 @@ public boolean supportsComplexGroupBy() { assertEquals("SELECT KEY(contacts_1) FROM Document d LEFT JOIN d.contacts contacts_1 GROUP BY KEY(contacts_1)", criteria.getQueryString()); criteria.getResultList(); } + + @Test + public void testOmitGroupByLiteral() { + CriteriaBuilder cb = cbf.create(em, Long.class) + .from(Document.class, "d") + .select("d.id") + .select("SIZE(d.people)") + .select("''"); + + assertEquals("SELECT d.id, " + function("count_tuple", "INDEX(people_1)") + ", '' FROM Document d LEFT JOIN d.people people_1 GROUP BY d.id", cb.getQueryString()); + 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 ab2c99aee1..710a85f735 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 @@ -18,8 +18,10 @@ import com.blazebit.persistence.CriteriaBuilder; import com.blazebit.persistence.impl.ConfigurationProperties; +import com.blazebit.persistence.impl.function.subquery.SubqueryFunction; import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus; import com.blazebit.persistence.testsuite.base.jpa.category.NoDatanucleus4; +import com.blazebit.persistence.testsuite.base.jpa.category.NoMSSQL; import com.blazebit.persistence.testsuite.entity.Document; import com.blazebit.persistence.testsuite.entity.Person; import com.blazebit.persistence.testsuite.entity.Version; @@ -33,6 +35,8 @@ import javax.persistence.Tuple; import java.util.List; +import static org.junit.Assert.assertEquals; + /** * * @author Moritz Becker @@ -356,4 +360,41 @@ public void testSizeTransformationMultiLevelCorrelatedFromClause() { cb.getResultList(); } + // from issue #513 + @Test + @Category({ NoMSSQL.class }) + public void testSumSize() { + cleanDatabase(); + transactional(new TxVoidWork() { + @Override + public void work(EntityManager em) { + Person p1 = new Person("p1"); + Person p2 = new Person("p2"); + Person p3 = new Person("p3"); + Person p4 = new Person("p4"); + Document d1 = new Document("d1"); + Document d2 = new Document("d2"); + d1.setOwner(p1); + d2.setOwner(p3); + em.persist(p1); + em.persist(p2); + em.persist(p3); + em.persist(p4); + d1.getPeople().add(p1); + d1.getPeople().add(p2); + d2.getPeople().add(p3); + d2.getPeople().add(p4); + em.persist(d1); + em.persist(d2); + } + }); + + CriteriaBuilder cb = cbf.create(em, Number.class) + .from(Document.class, "d") + .select("SUM(SIZE(d.people))"); + + String expected = "SELECT SUM(" + function(SubqueryFunction.FUNCTION_NAME, "(SELECT " + countStar() + " FROM d.people person)") + ") FROM Document d"; + assertEquals(expected, cb.getQueryString()); + assertEquals(4L, cb.getResultList().get(0).longValue()); + } } diff --git a/documentation/src/main/asciidoc/core/manual/en_US/18_jpql_functions.adoc b/documentation/src/main/asciidoc/core/manual/en_US/18_jpql_functions.adoc index eb69de0f29..52a143f2ae 100644 --- a/documentation/src/main/asciidoc/core/manual/en_US/18_jpql_functions.adoc +++ b/documentation/src/main/asciidoc/core/manual/en_US/18_jpql_functions.adoc @@ -331,6 +331,14 @@ Produces a DBMS native row value comparison expression such as `(row_value_1_1, WARNING: This is an internal function that is used to implement optimized keyset pagination. It is not intended for direct use and might change without notice. +==== SUBQUERY function + +Syntax: `FUNCTION ( 'SUBQUERY', subquery)` + +Simply renders the subquery argument. + +WARNING: This is an internal function that is used to bypass the Hibernate parser for rendering subqueries as aggregate function arguments. + === Custom JPQL functions Apart from providing many useful functions out of the box, {projectname} also allows to implement custom JPQL functions that can be called just like any other non-standard function, diff --git a/integration/eclipselink/src/main/java/com/blazebit/persistence/integration/eclipselink/function/JpqlFunctionExpressionOperator.java b/integration/eclipselink/src/main/java/com/blazebit/persistence/integration/eclipselink/function/JpqlFunctionExpressionOperator.java index c95df040a5..c80eda3815 100644 --- a/integration/eclipselink/src/main/java/com/blazebit/persistence/integration/eclipselink/function/JpqlFunctionExpressionOperator.java +++ b/integration/eclipselink/src/main/java/com/blazebit/persistence/integration/eclipselink/function/JpqlFunctionExpressionOperator.java @@ -35,6 +35,7 @@ import org.eclipse.persistence.mappings.DatabaseMapping; import java.io.IOException; +import java.lang.reflect.Field; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -193,7 +194,9 @@ private Class getExpressionType(Expression expression) { private Class getReturnTypeFromSubSelectExpression(SubSelectExpression subSelectExpression) { try { - return (Class) SubSelectExpression.class.getField("returnType").get(subSelectExpression); + Field returnTypeField = SubSelectExpression.class.getDeclaredField("returnType"); + returnTypeField.setAccessible(true); + return (Class) returnTypeField.get(subSelectExpression); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/SubqueryTest.java b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/SubqueryTest.java index 4696b1552b..aad8d8aaf6 100644 --- a/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/SubqueryTest.java +++ b/jpa-criteria/testsuite/src/test/java/com/blazebit/persistence/criteria/SubqueryTest.java @@ -79,7 +79,7 @@ public void correlateSelectLiteralWhereGroupByHaving() { cq.select(root.get(Document_.id)); CriteriaBuilder criteriaBuilder = cq.createCriteriaBuilder(); - assertEquals("SELECT document.id FROM Document document WHERE EXISTS (SELECT 1 FROM document.owner subOwner WHERE subOwner.id = 0L GROUP BY subOwner.age, 1 HAVING COUNT(subOwner.id) > 2L)", criteriaBuilder.getQueryString()); + assertEquals("SELECT document.id FROM Document document WHERE EXISTS (SELECT 1 FROM document.owner subOwner WHERE subOwner.id = 0L GROUP BY subOwner.age HAVING COUNT(subOwner.id) > 2L)", criteriaBuilder.getQueryString()); assertEquals(1, subquery.getCorrelatedJoins().size()); assertEquals(root, correlatedRoot.getCorrelationParent()); assertTrue(correlatedRoot.isCorrelated()); diff --git a/parent/pom.xml b/parent/pom.xml index 0d589f4afc..e2f674fdcc 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -53,7 +53,7 @@ 5.2.9.Final 5.3.0.CR1 - + 5.2.9.Final diff --git a/roadmap.asciidoc b/roadmap.asciidoc index da6ea4c6d4..90518db67a 100644 --- a/roadmap.asciidoc +++ b/roadmap.asciidoc @@ -50,7 +50,7 @@ As a side note, we are naming releases after http://marvel.com/comics/characters = 1.3 Fury -_Scheduled for end of June 2018_ because of long holiday trips +_Scheduled for end of July 2018_ because of long holiday trips ** EMBEDDING_VIEW function support ** Use of entity collection DML for updatable entity views diff --git a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/jpa/AbstractJpaPersistenceTest.java b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/jpa/AbstractJpaPersistenceTest.java index 32df306421..11886517f0 100644 --- a/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/jpa/AbstractJpaPersistenceTest.java +++ b/testsuite-base/jpa/src/main/java/com/blazebit/persistence/testsuite/base/jpa/AbstractJpaPersistenceTest.java @@ -104,6 +104,10 @@ public abstract class AbstractJpaPersistenceTest { private boolean schemaChanged; + static { + System.setProperty("org.jboss.logging.provider", "jdk"); + } + @BeforeClass public static void initLogging() { try { diff --git a/website/pom.xml b/website/pom.xml index 735e2d845c..58aa66b9bc 100644 --- a/website/pom.xml +++ b/website/pom.xml @@ -204,7 +204,12 @@ org.jbake jbake-core - 2.6.0 + 2.6.1 + + + org.jruby + jruby-complete + 9.1.15.0