diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Avg.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Avg.java index 01163d36e..841e7785c 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Avg.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Avg.java @@ -23,8 +23,9 @@ package org.apache.openjpa.jdbc.kernel.exps; * * @author Abe White */ +@SuppressWarnings("serial") class Avg - extends UnaryOp { + extends NullableAggregateUnaryOp { // OPENJPA-1794 /** * Constructor. Provide the value to operate on. @@ -41,4 +42,3 @@ class Avg return true; } } - diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Max.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Max.java index a6c6e2cbd..0cccfab90 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Max.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Max.java @@ -23,8 +23,9 @@ package org.apache.openjpa.jdbc.kernel.exps; * * @author Abe White */ +@SuppressWarnings("serial") class Max - extends UnaryOp { + extends NullableAggregateUnaryOp { // OPENJPA-1794 /** * Constructor. Provide the value to operate on. diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Min.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Min.java index c79171b05..97c7de0ae 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Min.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Min.java @@ -23,8 +23,9 @@ package org.apache.openjpa.jdbc.kernel.exps; * * @author Abe White */ +@SuppressWarnings("serial") class Min - extends UnaryOp { + extends NullableAggregateUnaryOp { // OPENJPA-1794 /** * Constructor. Provide the value to operate on. diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullableAggregateUnaryOp.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullableAggregateUnaryOp.java new file mode 100644 index 000000000..b69c0187f --- /dev/null +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullableAggregateUnaryOp.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.openjpa.jdbc.kernel.exps; + +/** + * OPENJPA-1794 + * An aggregate unary operation that can indicate whether a null value from the data store + * should be returned as null. + */ +@SuppressWarnings("serial") +public abstract class NullableAggregateUnaryOp extends UnaryOp { + + public NullableAggregateUnaryOp(Val val) { + super(val); + } + + public NullableAggregateUnaryOp(Val val, boolean noParen) { + super(val, noParen); + } + + @Override + protected boolean nullableValue(ExpContext ctx, ExpState state) { + // If this is a simple operator (no joins involved), check compatibility to determine + // whether 'null' should be returned for the aggregate operation + if (ctx != null && ctx.store != null && (state.joins == null || state.joins.isEmpty())) { + return ctx.store.getConfiguration().getCompatibilityInstance().getReturnNullOnEmptyAggregateResult(); + } + return false; + } +} diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Sum.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Sum.java index ad0f9c663..292547efa 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Sum.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/Sum.java @@ -25,8 +25,9 @@ import org.apache.openjpa.kernel.Filters; * * @author Abe White */ +@SuppressWarnings("serial") class Sum - extends UnaryOp { + extends NullableAggregateUnaryOp { // OPENJPA-1794 /** * Constructor. Provide the value to operate on. diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/UnaryOp.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/UnaryOp.java index 642a67c6b..46a01e942 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/UnaryOp.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/UnaryOp.java @@ -119,8 +119,13 @@ abstract class UnaryOp throws SQLException { Object value = res.getObject(this, JavaSQLTypes.JDBC_DEFAULT, null); Class type = getType(); - if (value == null && (type.isPrimitive() || Number.class.isAssignableFrom(type))) { - value = Filters.getDefaultForNull(Filters.wrap(type)); + if (value == null) { + if (nullableValue(ctx, state)) { // OPENJPA-1794 + return null; + } + else if (type.isPrimitive() || Number.class.isAssignableFrom(type)) { + value = Filters.getDefaultForNull(Filters.wrap(type)); + } } return Filters.convert(value, type); } @@ -171,5 +176,10 @@ abstract class UnaryOp _val.acceptVisit(visitor); visitor.exit(this); } -} + + // OPENJPA-1794 + protected boolean nullableValue(ExpContext ctx, ExpState state) { + return false; + } +} diff --git a/openjpa-kernel/src/main/java/org/apache/openjpa/conf/Compatibility.java b/openjpa-kernel/src/main/java/org/apache/openjpa/conf/Compatibility.java index 4cb6da5ca..a422597a7 100644 --- a/openjpa-kernel/src/main/java/org/apache/openjpa/conf/Compatibility.java +++ b/openjpa-kernel/src/main/java/org/apache/openjpa/conf/Compatibility.java @@ -76,7 +76,8 @@ public class Compatibility { private boolean _resetFlushFlagForCascadePersist = true;//OPENJPA-2051 private boolean _singletonLifecycleEventManager = false; private boolean _filterPCRegistryClasses = false; // OPENJPA-2288 - + private boolean _returnNullOnEmptyAggregateResult = true; // OPENJPA-1794 + /** * Whether to require exact identity value types when creating object * ids from a class and value. Defaults to false. @@ -728,4 +729,38 @@ public class Compatibility { public void setFilterPCRegistryClasses(boolean bool) { _filterPCRegistryClasses = bool; } + + /** + * This property is used to specify whether the aggregate query functions + * SUM, AVG, MAX, and MIN return null if there is no query result. This will occur + * if no rows are returned for the specified query predicate. The default is + * false, meaning that 0 will be returned for functions operating on numeric + * data. + * + * In compliance with the JPA specification, the default value is true. + * + * @return true if the result of an aggregate with an empty query result returns null. + * @since + * + */ + public boolean getReturnNullOnEmptyAggregateResult() { + return _returnNullOnEmptyAggregateResult; + } + + /** + * This property is used to specify whether the aggregate query functions + * SUM, AVG, MAX, and MIN return null if there is no query result. This will occur + * if no rows are returned for the specified query predicate. The default is + * false, meaning that 0 will be returned for functions operating on numeric + * data. + * + * In compliance with the JPA specification, the default value is true. + * + * @since + * @param returnNullOnAggregate whether OpenJPA will return null for aggregate + * expressions when the query result is empty. + */ + public void setReturnNullOnAggregateResult(boolean returnNullOnEmptyAggregateResult) { + _returnNullOnEmptyAggregateResult = returnNullOnEmptyAggregateResult; + } } diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/AggEntity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/AggEntity.java new file mode 100644 index 000000000..8478e3c6a --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/AggEntity.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.openjpa.jira1794; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "j1794_ae") +public class AggEntity { + + @Id + @GeneratedValue + private int id; + + private short pshortVal; + private Short shortVal; + + private int pintVal; + private Integer intVal; + + private long plongVal; + private Long longVal; + + private float pfloatVal; + private Float floatVal; + + private double pdblVal; + private Double dblVal; + + private String stringVal; + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setPshortVal(short pshortVal) { + this.pshortVal = pshortVal; + } + + public short getPshortVal() { + return pshortVal; + } + + public void setShortVal(Short pShortVal) { + this.shortVal = pShortVal; + } + + public Short getShortVal() { + return shortVal; + } + + public void setPintVal(int pintVal) { + this.pintVal = pintVal; + } + + public int getPintVal() { + return pintVal; + } + + public void setIntVal(Integer intVal) { + this.intVal = intVal; + } + + public Integer getIntVal() { + return intVal; + } + + public void setPlongVal(long plongVal) { + this.plongVal = plongVal; + } + + public long getPlongVal() { + return plongVal; + } + + public void setLongVal(Long longVal) { + this.longVal = longVal; + } + + public Long getLongVal() { + return longVal; + } + + public void setPfloatVal(float pfloatVal) { + this.pfloatVal = pfloatVal; + } + + public float getPfloatVal() { + return pfloatVal; + } + + public void setFloatVal(Float floatVal) { + this.floatVal = floatVal; + } + + public Float getFloatVal() { + return floatVal; + } + + public void setPdblVal(double pdblVal) { + this.pdblVal = pdblVal; + } + + public double getPdblVal() { + return pdblVal; + } + + public void setDblVal(Double dblVal) { + this.dblVal = dblVal; + } + + public Double getDblVal() { + return dblVal; + } + + public void setStringVal(String stringVal) { + this.stringVal = stringVal; + } + + public String getStringVal() { + return stringVal; + } + + public void init() { + setPshortVal((short) 1); + setShortVal(Short.valueOf((short) 1)); + setIntVal(1); + setPintVal(1); + setLongVal(1L); + setPlongVal(1L); + setDblVal(1d); + setPdblVal(1d); + setFloatVal(1f); + setPfloatVal(1f); + setStringVal("1"); + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/AggEntity_.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/AggEntity_.java new file mode 100644 index 000000000..18a5cbf95 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/AggEntity_.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Generated by OpenJPA MetaModel Generator Tool. +**/ +package org.apache.openjpa.jira1794; + +import javax.persistence.metamodel.SingularAttribute; + +@javax.persistence.metamodel.StaticMetamodel +(value=org.apache.openjpa.jira1794.AggEntity.class) +public class AggEntity_ { + public static volatile SingularAttribute pshortVal; + public static volatile SingularAttribute shortVal; + public static volatile SingularAttribute pintVal; + public static volatile SingularAttribute intVal; + public static volatile SingularAttribute plongVal; + public static volatile SingularAttribute longVal; + public static volatile SingularAttribute pfloatVal; + public static volatile SingularAttribute floatVal; + public static volatile SingularAttribute pdblVal; + public static volatile SingularAttribute dblVal; + public static volatile SingularAttribute stringVal; +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/TestAggregateFunctions.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/TestAggregateFunctions.java new file mode 100644 index 000000000..aa7cc4e37 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/TestAggregateFunctions.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.openjpa.jira1794; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Root; +import javax.persistence.metamodel.Metamodel; +import javax.persistence.metamodel.SingularAttribute; + +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * OPENJPA-1794 Verifies the return value of aggregate functions when a query + * result set is empty. In this set of variations, the compatibility flag is not + * set so null is expected. + */ +public class TestAggregateFunctions extends SingleEMFTestCase { + + private static final int MAX = 0; + private static final int MIN = 1; + private static final int SUM = 2; + + private static final String[] numericAggregateFunctions = { "MAX", "AVG", + "MIN", "SUM" }; + + private static final String[] stringAggregateFunctions = { "MAX", "MIN" }; + + private static final String[] numericAttributes = { "ae.pintVal", + "ae.intVal", "ae.shortVal", "ae.pshortVal", "ae.pintVal", + "ae.intVal", "ae.plongVal", "ae.longVal", "ae.pfloatVal", + "ae.floatVal", "ae.pdblVal", "ae.dblVal" }; + + @Override + public void setUp() { + super.setUp(CLEAR_TABLES, AggEntity.class); + } + + protected boolean nullResultExpected() { + return true; + } + + public void testAggregateJPQL() { + EntityManager em = emf.createEntityManager(); + + // Verify all numeric types for all aggregate functions return null + // if there is no query result + verifyResult(em, numericAggregateFunctions, numericAttributes, true); + + // Verify a string for all applicable aggregate functions return null + // if there is no query result + verifyResult(em, stringAggregateFunctions, + new String[] { "ae.stringVal" }, true, true); + + // Add a row to the table and re-test + AggEntity ae = new AggEntity(); + ae.init(); + em.getTransaction().begin(); + em.persist(ae); + em.getTransaction().commit(); + + // Verify all numeric types for all aggregate functions return a + // non-null value when there is a query result + verifyResult(em, numericAggregateFunctions, numericAttributes, false); + // Verify string types for all applicable aggregate functions return a + // non-null value when there is a query result + verifyResult(em, stringAggregateFunctions, + new String[] { "ae.stringVal" }, false); + + em.close(); + } + + public void testAggregateCriteria() { + EntityManager em = emf.createEntityManager(); + Metamodel mm = emf.getMetamodel(); + mm.getEntities(); + + Query q = null; + // Verify all types of criteria query that return a Numeric type + for (int agg = MAX; agg <= SUM; agg++) { + CriteriaQuery cqs = buildNumericCriteriaQuery(em, + Short.class, AggEntity_.shortVal, agg); + q = em.createQuery(cqs); + verifyQueryResult(q, true); + + cqs = buildNumericCriteriaQuery(em, Short.class, + AggEntity_.pshortVal, agg); + q = em.createQuery(cqs); + verifyQueryResult(q, true); + + CriteriaQuery cqi = buildNumericCriteriaQuery(em, + Integer.class, AggEntity_.intVal, agg); + q = em.createQuery(cqi); + verifyQueryResult(q, true); + + cqi = buildNumericCriteriaQuery(em, Integer.class, + AggEntity_.pintVal, agg); + q = em.createQuery(cqi); + verifyQueryResult(q, true); + + CriteriaQuery cqf = buildNumericCriteriaQuery(em, + Float.class, AggEntity_.floatVal, agg); + q = em.createQuery(cqf); + verifyQueryResult(q, true); + + cqf = buildNumericCriteriaQuery(em, Float.class, + AggEntity_.pfloatVal, agg); + q = em.createQuery(cqi); + verifyQueryResult(q, true); + + CriteriaQuery cqd = buildNumericCriteriaQuery(em, + Double.class, AggEntity_.dblVal, agg); + q = em.createQuery(cqd); + verifyQueryResult(q, true); + + cqd = buildNumericCriteriaQuery(em, Double.class, + AggEntity_.pdblVal, agg); + q = em.createQuery(cqi); + verifyQueryResult(q, true); + } + + // Verify AVG criteria query - it strictly returns type 'Double' so + // unlike other aggregates, + // it cannot be handled generically (as Numeric). + CriteriaQuery cqd = buildAvgCriteriaQuery(em, Double.class, + AggEntity_.dblVal); + q = em.createQuery(cqd); + verifyQueryResult(q, true); + + cqd = buildAvgCriteriaQuery(em, Double.class, AggEntity_.pdblVal); + q = em.createQuery(cqd); + verifyQueryResult(q, true); + + em.close(); + } + + private CriteriaQuery buildNumericCriteriaQuery( + EntityManager em, Class type, + SingularAttribute sa, int at) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(type); + Root aer = cq.from(AggEntity.class); + Path path = aer.get(sa); + Expression exp = null; + switch (at) { + case MAX: + exp = cb.max(path); + break; + case MIN: + exp = cb.min(path); + break; + case SUM: + exp = cb.sum(path); + break; + } + cq.select(exp); + return cq; + } + + private CriteriaQuery buildAvgCriteriaQuery(EntityManager em, + Class type, SingularAttribute sa) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(type); + Root aer = cq.from(AggEntity.class); + return cq.select(cb.avg(aer.get(sa))); + } + + private void verifyResult(EntityManager em, String[] aggregates, + String[] attributes, boolean expectNull) { + verifyResult(em, aggregates, attributes, expectNull, false); + } + + private void verifyResult(EntityManager em, String[] aggregates, + String[] attributes, boolean expectNull, boolean isString) { + for (String func : aggregates) { + for (String attr : attributes) { + // JPQL with aggregate and aggregate in subselect + String sql = "SELECT " + func + "(" + attr + ")" + + " FROM AggEntity ae WHERE " + attr + " <= " + + "(SELECT " + func + "(" + + attr.replaceFirst("^ae.", "ae2.") + + ") FROM AggEntity ae2)"; + ; + Query q = em.createQuery(sql); + verifyQueryResult(q, expectNull, isString); + } + } + } + + private void verifyQueryResult(Query q, boolean emptyRs) { + verifyQueryResult(q, emptyRs, false); + } + + private void verifyQueryResult(Query q, boolean emptyRs, boolean isString) { + Object result = q.getSingleResult(); + if (!emptyRs && !isString) { + assertNotNull(result); + } else if (isString || nullResultExpected()) { + assertNull(result); + } else { + assertNotNull(result); + } + List resultList = q.getResultList(); + assertEquals(1, resultList.size()); + if (!emptyRs && !isString) { + assertNotNull(result); + } else if (isString || nullResultExpected()) { + assertNull(result); + } else { + assertNotNull(result); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/TestCompatAggregateFunctions.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/TestCompatAggregateFunctions.java new file mode 100644 index 000000000..08ddb70f3 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/jira1794/TestCompatAggregateFunctions.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.openjpa.jira1794; + +/** + * OPENJPA-1794 + * Verifies the return value of aggregate functions when a query result + * set is empty. In this variation, the compatibility flag + * is set so 0 is expected. + */ +public class TestCompatAggregateFunctions extends TestAggregateFunctions { + + @Override + public void setUp() { + super.setUp(CLEAR_TABLES, + "openjpa.Compatibility", "ReturnNullOnAggregateResult=false", + AggEntity.class); + } + + @Override + // In compatibility mode a null result is not expected. + protected boolean nullResultExpected() { + return false; + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/TestAggregateQueryWithNoResult.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/TestAggregateQueryWithNoResult.java index 5b095a8da..547a247a1 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/TestAggregateQueryWithNoResult.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/TestAggregateQueryWithNoResult.java @@ -38,7 +38,9 @@ import org.apache.openjpa.persistence.test.SingleEMFTestCase; public class TestAggregateQueryWithNoResult extends SingleEMFTestCase { EntityManager em; public void setUp() { - super.setUp(CLEAR_TABLES, Game.class, IndoorGame.class, Scrabble.class, + super.setUp(CLEAR_TABLES, + "openjpa.Compatibility", "ReturnNullOnAggregateResult=false", //OPENJPA-1794 + Game.class, IndoorGame.class, Scrabble.class, Chess.class); em = emf.createEntityManager(); assertTrue(em.createQuery("select p from Scrabble p").getResultList().isEmpty()); diff --git a/openjpa-project/src/doc/manual/migration_considerations.xml b/openjpa-project/src/doc/manual/migration_considerations.xml index 84182901c..ce523ab38 100644 --- a/openjpa-project/src/doc/manual/migration_considerations.xml +++ b/openjpa-project/src/doc/manual/migration_considerations.xml @@ -557,6 +557,25 @@ RequiresSearchStringEscapeForLike property to be true if the old behavior is desired. +
+ + Return value of aggregate functions in SELECT clause + + + + The JPA specification states "If SUM, AVG, MAX, or MIN is used, and there are no values to which the aggregate function can be + applied, the result of the aggregate function is NULL." Prior to this update, OpenJPA incorrectly returned 0 for SUM, AVG, MIN, + and MAX when a state field being aggregated is numeric. This behavior affects both JPQL and Criteria queries. With this update, + OpenJPA will return a null result value for these aggregate functions when a query returns no result. + + + To re-enable the prior behavior, you need to set the following persistence property in your persistence.xml or when + creating an EntityManagerFactory. + + <property name="openjpa.Compatibility" value="ReturnNullOnAggregateResult=false"/> + + +