OPENJPA-1794 aggregate function with no result set must return null

git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@1506198 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jeremy Bauer 2013-07-23 18:26:42 +00:00
parent 5b3fea7cd4
commit ca76712e38
13 changed files with 602 additions and 10 deletions

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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.

View File

@ -119,9 +119,14 @@ 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))) {
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;
}
}

View File

@ -76,6 +76,7 @@ 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
@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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<AggEntity,Short> pshortVal;
public static volatile SingularAttribute<AggEntity,Short> shortVal;
public static volatile SingularAttribute<AggEntity,Integer> pintVal;
public static volatile SingularAttribute<AggEntity,Integer> intVal;
public static volatile SingularAttribute<AggEntity,Long> plongVal;
public static volatile SingularAttribute<AggEntity,Long> longVal;
public static volatile SingularAttribute<AggEntity,Float> pfloatVal;
public static volatile SingularAttribute<AggEntity,Float> floatVal;
public static volatile SingularAttribute<AggEntity,Double> pdblVal;
public static volatile SingularAttribute<AggEntity,Double> dblVal;
public static volatile SingularAttribute<AggEntity,String> stringVal;
}

View File

@ -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<Short> 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<Integer> 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<Float> 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<Double> 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<Double> 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 <T extends Number> CriteriaQuery<T> buildNumericCriteriaQuery(
EntityManager em, Class<T> type,
SingularAttribute<AggEntity, T> sa, int at) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<T> cq = cb.createQuery(type);
Root<AggEntity> aer = cq.from(AggEntity.class);
Path<T> path = aer.get(sa);
Expression<T> 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<Double> buildAvgCriteriaQuery(EntityManager em,
Class<Double> type, SingularAttribute<AggEntity, Double> sa) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Double> cq = cb.createQuery(type);
Root<AggEntity> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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());

View File

@ -557,6 +557,25 @@
RequiresSearchStringEscapeForLike property to be true if the old behavior is desired.
</para>
</section>
<section id="ReturnNullOnEmptyAggregateResult">
<title>
Return value of aggregate functions in SELECT clause
</title>
<!-- See OPENJPA-1794 for details. -->
<para>
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.
</para>
<para>
To re-enable the prior behavior, you need to set the following persistence property in your persistence.xml or when
creating an EntityManagerFactory.
<programlisting>
&lt;property name="openjpa.Compatibility" value="ReturnNullOnAggregateResult=false"/&gt;
</programlisting>
</para>
</section>
</section>
</section>
</appendix>