Index: src/conf/jdoql.conf =================================================================== --- src/conf/jdoql.conf (revision 1777645) +++ src/conf/jdoql.conf (working copy) @@ -69,6 +69,7 @@ org.apache.jdo.tck.query.jdoql.methods.SupportedListMethods \ org.apache.jdo.tck.query.jdoql.methods.SupportedMapMethods \ org.apache.jdo.tck.query.jdoql.methods.SupportedMathMethods \ +org.apache.jdo.tck.query.jdoql.methods.SupportedOptionalMethods \ org.apache.jdo.tck.query.jdoql.methods.SupportedStringMethods \ org.apache.jdo.tck.query.jdoql.operators.BinaryAddition \ org.apache.jdo.tck.query.jdoql.operators.BinarySubtraction \ Index: src/java/org/apache/jdo/tck/pc/query/OptionalSample.java =================================================================== --- src/java/org/apache/jdo/tck/pc/query/OptionalSample.java (revision 0) +++ src/java/org/apache/jdo/tck/pc/query/OptionalSample.java (working copy) @@ -0,0 +1,128 @@ +/* + * 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.jdo.tck.pc.query; + +import java.util.Date; +import java.util.Optional; + +public class OptionalSample { + long id; + + Optional optionalPC; + + Optional optionalDate; + Optional optionalInteger; + Optional optionalString; + + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public Optional getOptionalPC() { + return optionalPC; + } + + public void setOptionalPC(Optional opt) { + this.optionalPC = opt; + } + + public Optional getOptionalDate() { + return optionalDate; + } + + public void setOptionalDate(Optional opt) { + this.optionalDate = opt; + } + + public Optional getOptionalInteger() { + return optionalInteger; + } + + public void setOptionalInteger(Optional opt) { + this.optionalInteger = opt; + } + + public Optional getOptionalString() { + return optionalString; + } + + public void setOptionalString(Optional opt) { + this.optionalString = opt; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((optionalPC == null) ? 0 : optionalPC.hashCode()); + result = prime * result + ((optionalString == null) ? 0 : optionalString.hashCode()); + result = prime * result + ((optionalInteger == null) ? 0 : optionalInteger.hashCode()); + result = prime * result + ((optionalDate == null) ? 0 : optionalDate.hashCode()); + result = prime * result + (int) (id ^ (id >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OptionalSample other = (OptionalSample) obj; + if (id != other.id) + return false; + + if (optionalPC == null) { + if (other.optionalPC != null) + return false; + } else if (!optionalPC.equals(other.optionalPC)) + return false; + + if (optionalDate == null) { + if (other.optionalDate != null) + return false; + } else if (!optionalDate.equals(other.optionalDate)) + return false; + + if (optionalInteger == null) { + if (other.optionalInteger != null) + return false; + } else if (!optionalInteger.equals(other.optionalInteger)) + return false; + + if (optionalString == null) { + if (other.optionalString != null) + return false; + } else if (!optionalString.equals(other.optionalString)) + return false; + + return true; + } + + @Override + public String toString() { + return "id=" + id + " " + super.toString(); + } +} Index: src/java/org/apache/jdo/tck/query/jdoql/methods/SupportedOptionalMethods.java =================================================================== --- src/java/org/apache/jdo/tck/query/jdoql/methods/SupportedOptionalMethods.java (revision 0) +++ src/java/org/apache/jdo/tck/query/jdoql/methods/SupportedOptionalMethods.java (working copy) @@ -0,0 +1,574 @@ +/* + * 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.jdo.tck.query.jdoql.methods; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Optional; + +import javax.jdo.PersistenceManager; +import javax.jdo.Query; +import javax.jdo.Transaction; + +import org.apache.jdo.tck.JDO_Test; +import org.apache.jdo.tck.pc.query.OptionalSample; +import org.apache.jdo.tck.query.QueryElementHolder; +import org.apache.jdo.tck.query.QueryTest; +import org.apache.jdo.tck.util.BatchTestRunner; + +/** + *Title: Optional Fields. + *
+ *Keywords: query + *
+ *Assertion ID: A14.6.2-9 + *
+ *Assertion Description: + *Queries on fields of type java.util.Optional . + */ +public class SupportedOptionalMethods extends QueryTest { + + /** */ + private static final String ASSERTION_FAILED = + "Assertion A14.6.2-9 (OptionalFields) failed: "; + + private static final int PC_ID = 1; + private static final int PC_EMPTY_ID = 3; + private static final int PC_NULL_ID = 4; + private static final int REFERENCED_PC1_ID = 88; + private static final int REFERENCED_PC2_ID = 99; + private static final String STRING = "Hello JDO"; + private static final Integer INTEGER = 2016; + private static final Date DATE = new Date(1000000000); + private Object oidReferencedPC1; + private Object oidReferencedPC2; + private Object oidPC; + private Object oidEmpty; + private Object oidNull; + + /** + * The main is called when the class + * is directly executed from the command line. + * @param args The arguments passed to the program. + */ + public static void main(String[] args) { + BatchTestRunner.run(SupportedOptionalMethods.class); + } + + /** */ + public void testQueriesWithPresence() { + //Matches 'optionalPC.isPresent() == true' + checkQuery("optionalPC != null", oidPC, oidReferencedPC1); + + //matches !isPresent() but does NOT match Java 'optionalPC==null' + checkQuery("optionalPC == null", oidEmpty, oidNull, oidReferencedPC2); + + //matches isPresent() + checkQuery("!(optionalPC == null)", oidReferencedPC1, oidPC); + + //matches !isPresent() + checkQuery("!(optionalPC != null)", oidReferencedPC2, oidEmpty, oidNull); + + checkQuery("optionalPC.get() != null", oidPC, oidReferencedPC1); + checkQuery("optionalPC.get() == null", oidEmpty, oidNull, oidReferencedPC2); + checkQuery("optionalPC.isPresent()", oidPC, oidReferencedPC1); + checkQuery("!optionalPC.isPresent()", oidReferencedPC2, oidEmpty, oidNull); + + //querying non-PC 'Optional' fields + checkOptionalForPresence("optionalString"); + checkOptionalForPresence("optionalDate"); + checkOptionalForPresence("optionalInteger"); + } + + private void checkOptionalForPresence(String fieldName) { + checkQuery(fieldName + " != null", oidPC); + checkQuery(fieldName + " == null", + oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2); + checkQuery("!(" + fieldName + " == null)", oidPC); + checkQuery("!("+ fieldName + " != null)", + oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2); + + checkQuery(fieldName + ".get() != null", oidPC); + checkQuery(fieldName + ".get() == null", + oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2); + checkQuery(fieldName + ".isPresent()", oidPC); + checkQuery("!"+ fieldName + ".isPresent()", + oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2); + } + + public void testQueriesWithNavigation() { + checkQuery("optionalPC.id == " + REFERENCED_PC1_ID, oidPC); + checkQuery("optionalPC.id != " + REFERENCED_PC1_ID, oidReferencedPC1); + checkQuery("!(optionalPC.id == " + REFERENCED_PC1_ID + ")", oidReferencedPC1); + checkQuery("optionalPC.get().id == " + REFERENCED_PC1_ID, oidPC); + checkQuery("optionalPC.get().id != " + REFERENCED_PC1_ID, oidReferencedPC1); + checkQuery("!(optionalPC.get().id == " + REFERENCED_PC1_ID + ")", oidReferencedPC1); + checkQuery("optionalPC.optionalPC.isPresent()", oidPC); + + //The following reflects the changed behavior in JDO 3.2 in the sense that + //all instances are returned where either 'optionalPC.optionalPC==null' (not present) + //or 'optionalPC==null' (the 'null' evaluates to 'null', which is followed until it is + //evaluated in the 'isPresent()'). In other words, the query also returns all + //objects that match '!(optionalPC.isPresent())'. + checkQuery("!(optionalPC.optionalPC.isPresent())", + oidReferencedPC1, oidReferencedPC2, oidEmpty, oidNull); + + //A query where 'optionalPC!=null' and 'optionalPC.optionalPC==null; + //can be done as follows: + checkQuery("optionalPC.isPresent() && !(optionalPC.optionalPC.isPresent())", + oidReferencedPC1); + + checkQuery("optionalPC.optionalPC.id == " + REFERENCED_PC2_ID, oidPC); + checkQuery("optionalPC.get().optionalPC.get().id == " + REFERENCED_PC2_ID, oidPC); + + //test with && operator + checkQuery("!(optionalPC.isPresent() && optionalPC.id == " + REFERENCED_PC1_ID + ")", + oidReferencedPC1, oidReferencedPC2, oidEmpty, oidNull); + } + + private void checkQuery(String filter, Object ... resultOids) { + QueryElementHolder qeh = new QueryElementHolder( + /*UNIQUE*/ null, + /*RESULT*/ null, + /*INTO*/ null, + /*FROM*/ OptionalSample.class, + /*EXCLUDE*/ null, + /*WHERE*/ filter, + /*VARIABLES*/ null, + /*PARAMETERS*/ null, + /*IMPORTS*/ null, + /*GROUP BY*/ null, + /*ORDER BY*/ null, + /*FROM*/ null, + /*TO*/ null); + + ArrayList expectedResults = new ArrayList<>(); + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + for (int i = 0; i < resultOids.length; i++) { + expectedResults.add(pm.getObjectById(resultOids[i])); + } + } finally { + if (tx.isActive()) { + tx.rollback(); + } + } + + executeAPIQuery(ASSERTION_FAILED, qeh, expectedResults); + executeSingleStringQuery(ASSERTION_FAILED, qeh, expectedResults); + } + + /** + * This methods tests Optional fields and parameters. + */ + public void testParameterOptional() { + OptionalSample osReferencedPC1 = getOptionalSampleById(oidReferencedPC1); + String paramDecl = "java.util.Optional op"; + checkQuery("this.optionalPC == op", paramDecl, + new Object[]{Optional.of(osReferencedPC1)}, + new Object[]{oidPC}); + checkQuery("this.optionalDate == op", paramDecl, + new Object[]{Optional.of(DATE)}, + new Object[]{oidPC}); + checkQuery("this.optionalInteger == op", paramDecl, + new Object[]{Optional.of(INTEGER)}, + new Object[]{oidPC}); + checkQuery("this.optionalString == op", paramDecl, + new Object[]{Optional.of(STRING)}, + new Object[]{oidPC}); + } + + /** + * This methods tests Optional fields and parameters with auto + * de-referencing. + */ + public void testParameterOptionalAutoDeref() { + OptionalSample osReferencedPC1 = getOptionalSampleById(oidReferencedPC1); + checkQuery("this.optionalPC == op", + OptionalSample.class.getName() + " op", + new Object[]{osReferencedPC1}, new Object[]{oidPC}); + checkQuery("this.optionalDate == op", + "java.util.Date op", + new Object[]{DATE}, new Object[]{oidPC}); + checkQuery("this.optionalInteger == op", + "Integer op", + new Object[]{INTEGER}, new Object[]{oidPC}); + checkQuery("this.optionalString == op", + "String op", + new Object[]{STRING}, new Object[]{oidPC}); + } + + private OptionalSample getOptionalSampleById(Object id) { + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + return (OptionalSample) pm.getObjectById(id); + } finally { + if (tx.isActive()) { + tx.rollback(); + } + } + } + + /** + * This methods tests that empty Optional fields and parameters matches with + * Optional.empty(). + */ + public void testParameterOptionalWithEmptyFields() { + String paramDecl = "java.util.Optional op"; + Object[] params = new Object[]{Optional.empty()}; + checkQuery("this.optionalPC == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC2}); + checkQuery("this.optionalDate == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + checkQuery("this.optionalInteger == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + checkQuery("this.optionalString == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + } + + /** + * This methods tests that Optional fields and parameters matches with + * (Optional)null. + */ + public void testParameterOptionalWithNull() { + String paramDecl = "java.util.Optional op"; + Object[] params = new Object[]{null}; + checkQuery("this.optionalPC == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC2}); + checkQuery("this.optionalDate == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + checkQuery("this.optionalInteger == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + checkQuery("this.optionalString == op", paramDecl, params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + } + + /** + * This methods tests that Optional fields and parameters matches with + * (Object)null. + */ + public void testParameterOptionalNull() { + Object[] params = new Object[]{null}; + checkQuery("this.optionalPC == op", OptionalSample.class.getName() + " op", params, + new Object[]{oidEmpty, oidNull, oidReferencedPC2}); + checkQuery("this.optionalDate == op", "java.util.Date op", params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + checkQuery("this.optionalInteger == op", "java.lang.Integer op", params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + checkQuery("this.optionalString == op", "java.lang.String op", params, + new Object[]{oidEmpty, oidNull, oidReferencedPC1, oidReferencedPC2}); + + } + + /** + * This methods tests that Optional fields can be accessed in subqueries. + */ + public void testSubqueries() { + String queryStr1 = + "SELECT FROM " + OptionalSample.class.getName() + " WHERE " + + "(select max(a.id) from " + OptionalSample.class.getName() + " a " + + "where a.optionalPC.isPresent() ) == id"; + Object[] expectedResult1 = new Object[]{oidReferencedPC1}; + checkSingleStringQuery(queryStr1, expectedResult1); + + String queryStr2 = + "SELECT FROM " + OptionalSample.class.getName() + " WHERE " + + "(select max(a.id) from " + OptionalSample.class.getName() + " a " + + "where a.optionalPC.get() != null) == id"; + Object[] expectedResult2 = new Object[]{oidReferencedPC1}; + checkSingleStringQuery(queryStr2, expectedResult2); + } + + /** + * This methods tests that Optional fields can be accessed in subqueries. + */ + public void testOptionalAggregation() { + String clsName = OptionalSample.class.getName(); + + String queryStr1 = "SELECT AVG(optionalInteger) FROM " + clsName; + Query q1 = pm.newQuery(queryStr1); + executeJDOQuery(ASSERTION_FAILED, q1, queryStr1, false, null, (double)INTEGER, true); + + String queryStr2 = "SELECT AVG(optionalInteger.get()) FROM " + clsName; + Query q2 = pm.newQuery(queryStr2); + executeJDOQuery(ASSERTION_FAILED, q2, queryStr2, false, null, (double)INTEGER, true); + + //return the object whose Integer is the same as the AVG of all + //objects that have the Integer present. + String queryStrSub1 = "SELECT FROM " + clsName + " WHERE " + + "(select avg(a.optionalInteger) from " + clsName + " a " + + "where a.optionalInteger.isPresent() ) == optionalInteger"; + Object[] expectedResult1 = new Object[]{oidPC}; + checkSingleStringQuery(queryStrSub1, expectedResult1); + + String queryStrSub2 = "SELECT FROM " + clsName + " WHERE " + + "(select avg(a.optionalInteger.get()) from " + clsName + " a " + + "where a.optionalInteger.isPresent() ) == optionalInteger"; + Object[] expectedResult2 = new Object[]{oidPC}; + checkSingleStringQuery(queryStrSub2, expectedResult2); + + //Find all where the average is the same as the integer value itself. + //This returns ALL objects!!! + String queryStrSub3 = "SELECT FROM " + clsName + " WHERE " + + "(select avg(a.optionalInteger) from " + clsName + " a " + + " ) == optionalInteger"; + Object[] expectedResult3 = new Object[]{oidPC}; + checkSingleStringQuery(queryStrSub3, expectedResult3); + } + + private void checkSingleStringQuery(String singleStringJDOQL, Object ... resultOids) { + ArrayList expectedResults = new ArrayList<>(); + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + for (int i = 0; i < resultOids.length; i++) { + expectedResults.add(pm.getObjectById(resultOids[i])); + } + } finally { + if (tx.isActive()) + tx.rollback(); + } + + Query singleStringQuery = pm.newQuery(singleStringJDOQL); + executeJDOQuery(ASSERTION_FAILED, singleStringQuery, singleStringJDOQL, + false, null, expectedResults, true); + } + + private void checkQuery(String filter, String paramDecl, Object[] params, Object[] result) { + QueryElementHolder qeh = new QueryElementHolder( + /*UNIQUE*/ null, + /*RESULT*/ null, + /*INTO*/ null, + /*FROM*/ OptionalSample.class, + /*EXCLUDE*/ null, + /*WHERE*/ filter, + /*VARIABLES*/ null, + /*PARAMETERS*/ paramDecl, + /*IMPORTS*/ null, + /*GROUP BY*/ null, + /*ORDER BY*/ null, + /*FROM*/ null, + /*TO*/ null); + + ArrayList expectedResults = new ArrayList<>(); + PersistenceManager pm = getPM(); + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + for (int i = 0; i < result.length; i++) { + Object o = result[i]; + if (o instanceof String || o instanceof Date || o instanceof Integer) { + expectedResults.add(o); + } else { + expectedResults.add(pm.getObjectById(result[i])); + } + } + } finally { + if (tx.isActive()) { + tx.rollback(); + } + } + + executeAPIQuery(ASSERTION_FAILED, qeh, params, expectedResults); + executeSingleStringQuery(ASSERTION_FAILED, qeh, params, expectedResults); + } + + + /** + * Result class for queries on OptionalSample. + */ + public static class ResultInfo { + public long id; + public String optionalString; + public ResultInfo() {}; + public ResultInfo(long id, String optionalString) { + this.id = id; + this.optionalString = optionalString; + } + } + + + /** + * Test Optional.orElse() in the SELECT clause of JDOQL queries. + */ + @SuppressWarnings("unchecked") + public void testOrElseInSELECT() { + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + Query q = pm.newQuery("SELECT id, optionalString.orElse('NotPresent') FROM " + + OptionalSample.class.getName()); + q.setResultClass(ResultInfo.class); + Collection c = (Collection) q.execute(); + if (c.size() != 5) { + fail(ASSERTION_FAILED, "Wrong result count: " + c.size()); + } + for (ResultInfo i: c) { + switch ((int)i.id) { + case PC_ID: + if (!STRING.equals(i.optionalString)) { + fail(ASSERTION_FAILED, "Wrong string value: " + i.optionalString); + } + break; + case PC_EMPTY_ID: + case PC_NULL_ID: + case REFERENCED_PC1_ID: + case REFERENCED_PC2_ID: + assertEquals("NotPresent", i.optionalString); + break; + default: fail(ASSERTION_FAILED, "Wrong object id: " + i.id); + } + } + } finally { + if (tx.isActive()) { + tx.rollback(); + } + } + } + + + /** + * This test assert that null-references are converted to Optional.empty() when + * loaded from the database. + */ + public void testPersistenceNotNull() { + OptionalSample osNotNull = getOptionalSampleById(oidNull); + + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + + if (osNotNull.getOptionalDate() == null) { + fail(ASSERTION_FAILED, "Date field was 'null'"); + } + if (osNotNull.getOptionalInteger() == null) { + fail(ASSERTION_FAILED, "Integer field was 'null'"); + } + if (osNotNull.getOptionalPC() == null) { + fail(ASSERTION_FAILED, "optionalPC field was 'null'"); + } + if (osNotNull.getOptionalString() == null) { + fail(ASSERTION_FAILED, "String field was 'null'"); + } + + if (osNotNull.getOptionalDate().isPresent()) { + fail(ASSERTION_FAILED, "Date field was present"); + } + if (osNotNull.getOptionalInteger().isPresent()) { + fail(ASSERTION_FAILED, "Integer field was present"); + } + if (osNotNull.getOptionalPC().isPresent()) { + fail(ASSERTION_FAILED, "optionalPC field was present"); + } + if (osNotNull.getOptionalString().isPresent()) { + fail(ASSERTION_FAILED, "String field was present"); + } + } finally { + if (tx.isActive()) { + tx.rollback(); + } + } + } + + + /** + * @see JDO_Test#localSetUp() + */ + @Override + protected void localSetUp() { + addTearDownClass(OptionalSample.class); + insertOptionalSample(getPM()); + } + + + private void insertOptionalSample(PersistenceManager pm) { + Transaction tx = pm.currentTransaction(); + try { + tx.begin(); + + //Create two objects that are referenced by other objects, this allows + //testing of navigation in queries. + //The referencedPC1 will be referenced by 'osPC', + //The referencedPC2 will be referenced by referencedPC1. + OptionalSample referencedPC1 = new OptionalSample(); + referencedPC1.setId(REFERENCED_PC1_ID); + pm.makePersistent(referencedPC1); + oidReferencedPC1 = pm.getObjectId(referencedPC1); + + OptionalSample referencedPC2 = new OptionalSample(); + referencedPC2.setId(REFERENCED_PC2_ID); + pm.makePersistent(referencedPC2); + oidReferencedPC2 = pm.getObjectId(referencedPC2); + + referencedPC1.setOptionalPC(Optional.of(referencedPC2)); + + OptionalSample osPC = new OptionalSample(); + osPC.setId(PC_ID); + osPC.setOptionalPC(Optional.of(referencedPC1)); + osPC.setOptionalDate(Optional.of(DATE)); + osPC.setOptionalInteger(Optional.of(INTEGER)); + osPC.setOptionalString(Optional.of(STRING)); + pm.makePersistent(osPC); + oidPC = pm.getObjectId(osPC); + + //use empty optionals + OptionalSample osEmpty = new OptionalSample(); + osEmpty.setId(PC_EMPTY_ID); + osEmpty.setOptionalPC(Optional.empty()); + osEmpty.setOptionalDate(Optional.empty()); + osEmpty.setOptionalInteger(Optional.empty()); + osEmpty.setOptionalString(Optional.empty()); + pm.makePersistent(osEmpty); + oidEmpty = pm.getObjectId(osEmpty); + + //use null for optional fields + OptionalSample osNull = new OptionalSample(); + osNull.setId(PC_NULL_ID); + osNull.setOptionalPC(null); + osNull.setOptionalDate(null); + osNull.setOptionalInteger(null); + osNull.setOptionalString(null); + pm.makePersistent(osNull); + oidNull = pm.getObjectId(osNull); + + tx.commit(); + } finally { + if (tx.isActive()) { + tx.rollback(); + } + } + } + + @Override + protected void localTearDown() { + //set all references to null to allow deletion of (otherwise) referenced objects + Transaction tx = getPM().currentTransaction(); + tx.begin(); + Query q = pm.newQuery(OptionalSample.class); + for (OptionalSample os: q.executeList()) { + os.setOptionalPC(Optional.empty()); + } + tx.commit(); + + super.localTearDown(); + } + +} Index: src/jdo/applicationidentity/org/apache/jdo/tck/pc/query/package.jdo =================================================================== --- src/jdo/applicationidentity/org/apache/jdo/tck/pc/query/package.jdo (revision 1777645) +++ src/jdo/applicationidentity/org/apache/jdo/tck/pc/query/package.jdo (working copy) @@ -37,6 +37,16 @@ + + + + + + + + Index: src/jdo/datastoreidentity/org/apache/jdo/tck/pc/query/package.jdo =================================================================== --- src/jdo/datastoreidentity/org/apache/jdo/tck/pc/query/package.jdo (revision 1777645) +++ src/jdo/datastoreidentity/org/apache/jdo/tck/pc/query/package.jdo (working copy) @@ -28,6 +28,15 @@ + + + + + + + Index: src/orm/applicationidentity/org/apache/jdo/tck/pc/query/package-standard.orm =================================================================== --- src/orm/applicationidentity/org/apache/jdo/tck/pc/query/package-standard.orm (revision 1777645) +++ src/orm/applicationidentity/org/apache/jdo/tck/pc/query/package-standard.orm (working copy) @@ -34,6 +34,23 @@ + + + + + + + + + + + + + + + + + Index: src/orm/datastoreidentity/org/apache/jdo/tck/pc/query/package-standard.orm =================================================================== --- src/orm/datastoreidentity/org/apache/jdo/tck/pc/query/package-standard.orm (revision 1777645) +++ src/orm/datastoreidentity/org/apache/jdo/tck/pc/query/package-standard.orm (working copy) @@ -40,6 +40,24 @@ + + + + + + + + + + + + + + + + + + Index: src/sql/derby/applicationidentity/schema.sql =================================================================== --- src/sql/derby/applicationidentity/schema.sql (revision 1777645) +++ src/sql/derby/applicationidentity/schema.sql (working copy) @@ -136,6 +136,7 @@ DROP TABLE JDOQLKeywordsAsFieldNames; DROP TABLE NoExtent; DROP TABLE TimeSample; +DROP TABLE OptionalSample; DROP TABLE MathSample; CREATE TABLE JDOQLKeywordsAsFieldNames ( @@ -154,6 +155,15 @@ CONSTRAINT TIMESAMPLE_PK PRIMARY KEY (ID) ); +CREATE TABLE OptionalSample ( + ID INTEGER NOT NULL, + OPTIONAL_PC INTEGER REFERENCES OptionalSample ON DELETE NO ACTION, + OPTIONAL_DATE DATE, + OPTIONAL_INTEGER INTEGER, + OPTIONAL_STRING VARCHAR(255), + CONSTRAINT OPTIONALSAMPLE_PK PRIMARY KEY (ID) +); + CREATE TABLE MathSample ( ID INTEGER NOT NULL, ANGLE DECIMAL(18,8), Index: src/sql/derby/datastoreidentity/schema.sql =================================================================== --- src/sql/derby/datastoreidentity/schema.sql (revision 1777645) +++ src/sql/derby/datastoreidentity/schema.sql (working copy) @@ -122,6 +122,7 @@ DROP TABLE JDOQLKeywordsAsFieldNames; DROP TABLE NoExtent; DROP TABLE TimeSample; +DROP TABLE OptionalSample; DROP TABLE MathSample; CREATE TABLE JDOQLKeywordsAsFieldNames ( @@ -143,6 +144,16 @@ CONSTRAINT TIMESAMPLE_PK PRIMARY KEY (DATASTORE_IDENTITY) ); +CREATE TABLE OptionalSample ( + DATASTORE_IDENTITY INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY, + ID INTEGER NOT NULL, + OPTIONAL_PC INTEGER REFERENCES OptionalSample ON DELETE NO ACTION, + OPTIONAL_DATE DATE, + OPTIONAL_INTEGER INTEGER, + OPTIONAL_STRING VARCHAR(255), + CONSTRAINT OPTIONALSAMPLE_PK PRIMARY KEY (DATASTORE_IDENTITY) +); + CREATE TABLE MathSample ( DATASTORE_IDENTITY INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY, ID INTEGER NOT NULL,