diff --git a/openjpa-integration/tck/run-tck32.sh b/openjpa-integration/tck/run-tck32.sh index a699cc00f5..598339b5c5 100755 --- a/openjpa-integration/tck/run-tck32.sh +++ b/openjpa-integration/tck/run-tck32.sh @@ -36,7 +36,7 @@ declare -A DOCKER_NAMES=( [mysql]=mysql [postgresql]=postgresql [mssql]=server - [oracle]=oracle-xe + [oracle]=oracle-free ) declare -A VERSIONS=( diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/AbstractVal.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/AbstractVal.java index e836635d7e..b4f301e43a 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/AbstractVal.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/AbstractVal.java @@ -18,6 +18,9 @@ */ package org.apache.openjpa.jdbc.kernel.exps; +import java.sql.Time; + +import org.apache.openjpa.jdbc.meta.JavaSQLTypes; import org.apache.openjpa.jdbc.sql.SQLBuffer; import org.apache.openjpa.jdbc.sql.Select; import org.apache.openjpa.kernel.exps.ExpressionVisitor; @@ -38,6 +41,20 @@ abstract class AbstractVal protected static final String FALSE = "1 <> 1"; private String _alias = null; + /** + * Return the type code to load the value of this expression with: {@link JavaSQLTypes#SQL_DATE} for a + * {@link java.sql.Date} and {@link JavaSQLTypes#TIME} for a {@link Time}, as a driver can return another type + * for the column, such as the Timestamp the Oracle driver returns for a DATE column; else + * {@link JavaSQLTypes#JDBC_DEFAULT}. + */ + protected int getLoadTypeCode() { + Class type = getType(); + if (type == java.sql.Date.class || type == Time.class) { + return JavaSQLTypes.getDateTypeCode(type); + } + return JavaSQLTypes.JDBC_DEFAULT; + } + @Override public boolean isVariable() { return false; diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/CoalesceExpression.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/CoalesceExpression.java index 1d7e74da9f..c6253495b2 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/CoalesceExpression.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/CoalesceExpression.java @@ -20,7 +20,6 @@ import java.sql.SQLException; -import org.apache.openjpa.jdbc.meta.JavaSQLTypes; import org.apache.openjpa.jdbc.sql.Joins; import org.apache.openjpa.jdbc.sql.Raw; import org.apache.openjpa.jdbc.sql.Result; @@ -162,8 +161,7 @@ private SQLBuffer newSQLBuffer(Select sel, ExpContext ctx, ExpState state) { @Override public Object load(ExpContext ctx, ExpState state, Result res) throws SQLException { - return Filters.convert(res.getObject(this, - JavaSQLTypes.JDBC_DEFAULT, null), getType()); + return Filters.convert(res.getObject(this, getLoadTypeCode(), null), getType()); } @Override diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/GeneralCaseExpression.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/GeneralCaseExpression.java index 80fe6e24d5..3561642e50 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/GeneralCaseExpression.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/GeneralCaseExpression.java @@ -21,7 +21,6 @@ import java.sql.SQLException; import java.sql.Types; -import org.apache.openjpa.jdbc.meta.JavaSQLTypes; import org.apache.openjpa.jdbc.sql.DBDictionary; import org.apache.openjpa.jdbc.sql.Joins; import org.apache.openjpa.jdbc.sql.Raw; @@ -203,8 +202,7 @@ private SQLBuffer newSQLBuffer(Select sel, ExpContext ctx, ExpState state) { @Override public Object load(ExpContext ctx, ExpState state, Result res) throws SQLException { - return Filters.convert(res.getObject(this, - JavaSQLTypes.JDBC_DEFAULT, null), getType()); + return Filters.convert(res.getObject(this, getLoadTypeCode(), null), getType()); } @Override diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullIfExpression.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullIfExpression.java index 2368214291..f4570a9114 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullIfExpression.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/NullIfExpression.java @@ -20,7 +20,6 @@ import java.sql.SQLException; -import org.apache.openjpa.jdbc.meta.JavaSQLTypes; import org.apache.openjpa.jdbc.sql.Raw; import org.apache.openjpa.jdbc.sql.Result; import org.apache.openjpa.jdbc.sql.SQLBuffer; @@ -145,8 +144,7 @@ private SQLBuffer newSQLBuffer(Select sel, ExpContext ctx, ExpState state) { @Override public Object load(ExpContext ctx, ExpState state, Result res) throws SQLException { - return Filters.convert(res.getObject(this, - JavaSQLTypes.JDBC_DEFAULT, null), getType()); + return Filters.convert(res.getObject(this, getLoadTypeCode(), null), getType()); } @Override diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/SimpleCaseExpression.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/SimpleCaseExpression.java index fc17f977cf..ccf2cd3cb5 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/SimpleCaseExpression.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/exps/SimpleCaseExpression.java @@ -20,7 +20,6 @@ import java.sql.SQLException; -import org.apache.openjpa.jdbc.meta.JavaSQLTypes; import org.apache.openjpa.jdbc.sql.DBDictionary; import org.apache.openjpa.jdbc.sql.Joins; import org.apache.openjpa.jdbc.sql.Raw; @@ -216,8 +215,7 @@ private SQLBuffer newSQLBuffer(Select sel, ExpContext ctx, ExpState state) { @Override public Object load(ExpContext ctx, ExpState state, Result res) throws SQLException { - return Filters.convert(res.getObject(this, - JavaSQLTypes.JDBC_DEFAULT, null), getType()); + return Filters.convert(res.getObject(this, getLoadTypeCode(), null), getType()); } @Override diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/fkorder/TestDeleteOrderForUnmappedForeignKey.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/fkorder/TestDeleteOrderForUnmappedForeignKey.java index 3ebf6f74a0..e67b2ab5fe 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/fkorder/TestDeleteOrderForUnmappedForeignKey.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/fkorder/TestDeleteOrderForUnmappedForeignKey.java @@ -26,6 +26,7 @@ import org.apache.openjpa.jdbc.conf.JDBCConfiguration; import org.apache.openjpa.jdbc.meta.ClassMapping; import org.apache.openjpa.jdbc.schema.ForeignKey; +import org.apache.openjpa.jdbc.sql.DBDictionary; import org.apache.openjpa.persistence.test.SQLListenerTestCase; /** @@ -151,6 +152,10 @@ public void testCycleOfNonNullableLogicalForeignKeys() { * violate the constraint. */ public void testCycleOfNullableLogicalForeignKeys() { + // the statements are counted, so they are not batched, as on Oracle and DB2 by default + DBDictionary dict = getDbDictionary(emf); + int batchLimit = dict.getBatchLimit(); + dict.setBatchLimit(0); EntityManager em = emf.createEntityManager(); try { em.getTransaction().begin(); @@ -183,6 +188,7 @@ public void testCycleOfNullableLogicalForeignKeys() { assertEquals(sql.toString(), 2, deletes); em.getTransaction().commit(); } finally { + dict.setBatchLimit(batchLimit); if (em.getTransaction().isActive()) { em.getTransaction().rollback(); } diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedA.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedA.java index d13886ece4..f7a5ca5417 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedA.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedA.java @@ -45,8 +45,7 @@ public class BulkDeleteSharedA { @PersistentCollection @ContainerTable(name = "BULK_SHARED_COLL", joinColumns = { - @XJoinColumn(name = "OWNER_ID", referencedColumnName = "ID", - columnDefinition = "BIGINT"), + @XJoinColumn(name = "OWNER_ID", referencedColumnName = "ID"), @XJoinColumn(name = "DISC", referencedColumnName = "'A'", columnDefinition = "VARCHAR(1)") }) @ElementColumn(name = "VAL") diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedB.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedB.java index 201fa833d9..801bb3240e 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedB.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jdbc/query/bulk/BulkDeleteSharedB.java @@ -45,8 +45,7 @@ public class BulkDeleteSharedB { @PersistentCollection @ContainerTable(name = "BULK_SHARED_COLL", joinColumns = { - @XJoinColumn(name = "OWNER_ID", referencedColumnName = "ID", - columnDefinition = "BIGINT"), + @XJoinColumn(name = "OWNER_ID", referencedColumnName = "ID"), @XJoinColumn(name = "DISC", referencedColumnName = "'B'", columnDefinition = "VARCHAR(1)") }) @ElementColumn(name = "VAL") diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestInMemoryJDBCEscapeLiterals.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestInMemoryJDBCEscapeLiterals.java index 64a7e8ea12..43de300973 100644 --- a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestInMemoryJDBCEscapeLiterals.java +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestInMemoryJDBCEscapeLiterals.java @@ -32,6 +32,7 @@ import jakarta.persistence.EntityManager; +import org.apache.openjpa.jdbc.sql.OracleDictionary; import org.apache.openjpa.kernel.QueryImpl; import org.apache.openjpa.persistence.simple.AllFieldTypes; import org.apache.openjpa.persistence.test.SingleEMFTestCase; @@ -193,7 +194,8 @@ public void testCaseAndNullIfInMemory() { }; for (String s : select) { String jpql = "SELECT " + s + " FROM AllFieldTypes e"; - List db = sorted(timestamps(em.createQuery(jpql).getResultList())); + List values = em.createQuery(jpql).getResultList(); + List db = sorted(s.startsWith("NULLIF") && s.contains("{d ") ? dates(values) : timestamps(values)); assertEquals(jpql, 2, db.size()); assertFalse(jpql, db.get(0).equals(db.get(1))); assertEquals(jpql, db, sorted(inMemory(em, all, jpql))); @@ -212,6 +214,28 @@ public void testCaseAndNullIfInMemory() { } } + /** + * Oracle has no type for a date without a time of day: a date field is + * mapped to a DATE column, for which the Oracle driver returns a + * {@link Timestamp}. The type of NULLIF over a date field and an escape is + * not known to OpenJPA, so that Timestamp is passed on. A Timestamp at the + * start of a day is compared as the {@link Date} it stands for. + */ + private List dates(List values) { + if (!(getDBDictionary() instanceof OracleDictionary)) { + return values; + } + List result = new ArrayList<>(); + for (Object value : values) { + if (value instanceof Timestamp ts && ts.toLocalDateTime().toLocalTime().equals(LocalTime.MIDNIGHT)) { + result.add(Date.valueOf(ts.toLocalDateTime().toLocalDate())); + } else { + result.add(value); + } + } + return result; + } + /** * The type of NULLIF over a timestamp field and an escape is not known * to OpenJPA, so the value the driver returns is passed on. MySQL 8.4 diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestTemporalConditionalExpressions.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestTemporalConditionalExpressions.java new file mode 100644 index 0000000000..12bc4f4705 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/jpql/expressions/TestTemporalConditionalExpressions.java @@ -0,0 +1,73 @@ +/* + * 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.persistence.jpql.expressions; + +import java.sql.Date; +import java.sql.Time; +import java.util.List; + +import jakarta.persistence.EntityManager; + +import org.apache.openjpa.persistence.simple.AllFieldTypes; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * COALESCE and CASE over a java.sql.Date or java.sql.Time field return a value of the type of the field, also + * when the driver returns another type for the column, such as the Timestamp the Oracle driver returns for the + * DATE column of such a field. + */ +public class TestTemporalConditionalExpressions extends SingleEMFTestCase { + + private static final Date DATE = Date.valueOf("2021-01-03"); + private static final Time TIME = Time.valueOf("10:20:30"); + + @Override + public void setUp() { + setUp(AllFieldTypes.class, CLEAR_TABLES); + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + AllFieldTypes e = new AllFieldTypes(); + e.setSqlDateField(DATE); + e.setSqlTimeField(TIME); + em.persist(e); + em.getTransaction().commit(); + } + } + + public void testDate() { + assertResult(DATE, "SELECT COALESCE(e.sqlDateField, e.sqlDateField) FROM AllFieldTypes e"); + assertResult(DATE, "SELECT CASE WHEN e.intField = 0 THEN e.sqlDateField ELSE e.sqlDateField END " + + "FROM AllFieldTypes e"); + } + + public void testTime() { + assertResult(TIME, "SELECT COALESCE(e.sqlTimeField, e.sqlTimeField) FROM AllFieldTypes e"); + assertResult(TIME, "SELECT CASE WHEN e.intField = 0 THEN e.sqlTimeField ELSE e.sqlTimeField END " + + "FROM AllFieldTypes e"); + } + + private void assertResult(Object expected, String jpql) { + try (EntityManager em = emf.createEntityManager()) { + List result = em.createQuery(jpql).getResultList(); + assertEquals(jpql, 1, result.size()); + assertEquals(jpql, expected.getClass(), result.get(0).getClass()); + assertEquals(jpql, expected.toString(), result.get(0).toString()); + } + } +} diff --git a/pom.xml b/pom.xml index 406f891776..211f2a5243 100644 --- a/pom.xml +++ b/pom.xml @@ -1484,20 +1484,16 @@ test-oracle-docker @@ -1507,11 +1503,11 @@ - com.oracle.ojdbc - ojdbc8 - 19.3.0.0 + com.oracle.database.jdbc + ojdbc11 + 23.26.4.0.0 - 18.4.0 + 23-slim 1521 oracle.jdbc.driver.OracleDriver @@ -1550,7 +1546,7 @@ true - gvenzl/oracle-xe:${oracle.server.version} + gvenzl/oracle-free:${oracle.server.version} ${openjpa.oracle.password} @@ -1560,12 +1556,11 @@ - ${project.basedir}/src/test/sql/oracle-docker:/docker-entrypoint-initdb.d/ + ${project.basedir}/src/test/sql/oracle-docker:/container-entrypoint-initdb.d/ ${docker.external.oracle.port}:1521 - 5500:5500 DATABASE IS READY TO USE @@ -1583,27 +1578,18 @@ test-oracle @@ -1620,10 +1606,9 @@ - http://not.a.real.repository - com.oracle.ojdbc - ojdbc8 - 19.3.0.0 + com.oracle.database.jdbc + ojdbc11 + 23.26.4.0.0 oracle.jdbc.driver.OracleDriver ${openjpa.oracle.url} diff --git a/scripts/run-build-matrix.sh b/scripts/run-build-matrix.sh index 812179e867..d53c74e7df 100755 --- a/scripts/run-build-matrix.sh +++ b/scripts/run-build-matrix.sh @@ -37,7 +37,7 @@ declare -A DOCKER_NAMES=( [postgresql]=postgresql [mssql]=server [herddb]=herddb - [oracle]=oracle-xe + [oracle]=oracle-free ) declare -A VERSIONS=( [mysql]='8.4.8 9.4.0 9.7' diff --git a/src/test/sql/oracle-docker/01-processes.sh b/src/test/sql/oracle-docker/01-processes.sh new file mode 100755 index 0000000000..1c9f94ee72 --- /dev/null +++ b/src/test/sql/oracle-docker/01-processes.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# 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. +# + +# Init script of the 'test-oracle-docker' Maven profile, run once by the entrypoint of the +# gvenzl/oracle-free image when the database is created, before it reports DATABASE IS READY TO USE. +# The test suite opens more connections than the image allows by default (processes=200) and then +# fails with ORA-12516/ORA-12519, so processes and sessions are raised and the database is restarted. +sqlplus -s / as sysdba <