From 6d1887379672e6cdaa3bc896114eaebc47b39e30 Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Sun, 5 Jan 2025 07:49:36 +0900 Subject: [PATCH 1/2] Failing test involving Oracle's multiple result sets a.k.a. implicit cursor ? https://stackoverflow.com/q/42091653/1261766 --- pom.xml | 12 +++ .../oracle_implicit_cursor/Author.java | 83 +++++++++++++++++++ .../oracle_implicit_cursor/Book.java | 72 ++++++++++++++++ .../oracle_implicit_cursor/Mapper.java | 28 +++++++ .../OracleImplicitCursorTest.java | 78 +++++++++++++++++ .../testcontainers/OracleTestContainer.java | 57 +++++++++++++ .../oracle_implicit_cursor/CreateDB.sql | 49 +++++++++++ .../oracle_implicit_cursor/Mapper.xml | 67 +++++++++++++++ 8 files changed, 446 insertions(+) create mode 100644 src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java create mode 100644 src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java create mode 100644 src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java create mode 100644 src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java create mode 100644 src/test/java/org/apache/ibatis/testcontainers/OracleTestContainer.java create mode 100644 src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql create mode 100644 src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml diff --git a/pom.xml b/pom.xml index 121a826dc3f..2a7fca4068f 100644 --- a/pom.xml +++ b/pom.xml @@ -204,6 +204,12 @@ 9.1.0 test + + com.oracle.database.jdbc + ojdbc8 + 23.6.0.24.10 + test + org.assertj assertj-core @@ -254,6 +260,12 @@ ${testcontainers.version} test + + org.testcontainers + oracle-free + ${testcontainers.version} + test + com.microsoft.sqlserver diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java new file mode 100644 index 00000000000..ccb73472c5b --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Author.java @@ -0,0 +1,83 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * 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 + * + * https://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.ibatis.submitted.oracle_implicit_cursor; + +import java.util.List; +import java.util.Objects; + +public class Author { + private Integer id; + private String name; + private List books; + + public Author() { + super(); + } + + public Author(Integer id, String name, List books) { + super(); + this.id = id; + this.name = name; + this.books = books; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getBooks() { + return books; + } + + public void setBooks(List books) { + this.books = books; + } + + @Override + public int hashCode() { + return Objects.hash(books, id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Author)) { + return false; + } + Author other = (Author) obj; + return Objects.equals(books, other.books) && Objects.equals(id, other.id) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Author [id=" + id + ", name=" + name + ", books=" + books + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java new file mode 100644 index 00000000000..b031054602a --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Book.java @@ -0,0 +1,72 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * 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 + * + * https://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.ibatis.submitted.oracle_implicit_cursor; + +import java.util.Objects; + +public class Book { + private Integer id; + private String name; + + public Book() { + super(); + } + + public Book(Integer id, String name) { + super(); + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Book)) { + return false; + } + Book other = (Book) obj; + return Objects.equals(id, other.id) && Objects.equals(name, other.name); + } + + @Override + public String toString() { + return "Book [id=" + id + ", name=" + name + "]"; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java new file mode 100644 index 00000000000..db860a95c7d --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.java @@ -0,0 +1,28 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * 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 + * + * https://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.ibatis.submitted.oracle_implicit_cursor; + +import java.util.List; + +public interface Mapper { + + List selectImplicitCursors_Statement(); + + List selectImplicitCursors_Prepared(); + + List selectImplicitCursors_Callable(); + +} diff --git a/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java new file mode 100644 index 00000000000..5459a93033d --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/oracle_implicit_cursor/OracleImplicitCursorTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * 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 + * + * https://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.ibatis.submitted.oracle_implicit_cursor; + +import static org.junit.jupiter.api.Assertions.assertIterableEquals; + +import java.util.List; +import java.util.function.Function; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.testcontainers.OracleTestContainer; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("TestcontainersTests") +class OracleImplicitCursorTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + Configuration configuration = new Configuration(); + Environment environment = new Environment("development", new JdbcTransactionFactory(), + OracleTestContainer.getUnpooledDataSource()); + configuration.setEnvironment(environment); + configuration.addMapper(Mapper.class); + sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); + + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql"); + } + + @Test + void shouldImplicitCursors_Statement() { + doTest(Mapper::selectImplicitCursors_Statement); + } + + @Test + void shouldImplicitCursors_Prepared() { + doTest(Mapper::selectImplicitCursors_Prepared); + } + + @Test + void shouldImplicitCursors_Callable() { + doTest(Mapper::selectImplicitCursors_Callable); + } + + private void doTest(Function> query) { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + List authors = query.apply(mapper); + assertIterableEquals( + List.of(new Author(1, "John", List.of(new Book(1, "C#"), new Book(2, "Python"), new Book(5, "Ruby"))), + new Author(2, "Jane", List.of(new Book(3, "SQL"), new Book(4, "Java")))), + authors); + } + } +} diff --git a/src/test/java/org/apache/ibatis/testcontainers/OracleTestContainer.java b/src/test/java/org/apache/ibatis/testcontainers/OracleTestContainer.java new file mode 100644 index 00000000000..1733f653a72 --- /dev/null +++ b/src/test/java/org/apache/ibatis/testcontainers/OracleTestContainer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * 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 + * + * https://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.ibatis.testcontainers; + +import javax.sql.DataSource; + +import org.apache.ibatis.datasource.pooled.PooledDataSource; +import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.oracle.OracleContainer; + +@Testcontainers +public final class OracleTestContainer { + + private static final String DB_NAME = "mybatis_test"; + private static final String USERNAME = "u"; + private static final String PASSWORD = "p"; + private static final String DRIVER = "oracle.jdbc.driver.OracleDriver"; + + @Container + private static final OracleContainer INSTANCE = initContainer(); + + private static OracleContainer initContainer() { + @SuppressWarnings("resource") + var container = new OracleContainer("gvenzl/oracle-free:slim-faststart").withDatabaseName(DB_NAME) + .withUsername(USERNAME).withPassword(PASSWORD); + container.start(); + return container; + } + + public static DataSource getUnpooledDataSource() { + return new UnpooledDataSource(OracleTestContainer.DRIVER, INSTANCE.getJdbcUrl(), OracleTestContainer.USERNAME, + OracleTestContainer.PASSWORD); + } + + public static PooledDataSource getPooledDataSource() { + return new PooledDataSource(OracleTestContainer.DRIVER, INSTANCE.getJdbcUrl(), OracleTestContainer.USERNAME, + OracleTestContainer.PASSWORD); + } + + private OracleTestContainer() { + } +} diff --git a/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql b/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql new file mode 100644 index 00000000000..5f542eb28d2 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/CreateDB.sql @@ -0,0 +1,49 @@ +-- +-- Copyright 2009-2025 the original author or authors. +-- +-- 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 +-- +-- https://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. +-- + +-- @DELIMITER | +begin + execute immediate 'drop table author'; +exception + when others then + if sqlcode != -942 then + raise; + end if; +end; +begin + execute immediate 'drop table book'; +exception + when others then + if sqlcode != -942 then + raise; + end if; +end; +| +-- @DELIMITER ; + +create table author (id int, name varchar(10)); + +insert into author (id, name) values (1, 'John'); +insert into author (id, name) values (2, 'Jane'); + + +create table book (id int, author_id int, name varchar(10)); + +insert into book (id, author_id, name) values (1, 1, 'C#'); +insert into book (id, author_id, name) values (2, 1, 'Python'); +insert into book (id, author_id, name) values (3, 2, 'SQL'); +insert into book (id, author_id, name) values (4, 2, 'Java'); +insert into book (id, author_id, name) values (5, 1, 'Ruby'); diff --git a/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml new file mode 100644 index 00000000000..f17ef518414 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/oracle_implicit_cursor/Mapper.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + From f72fe1a9c11fd40ee09fdaff8bce81b7050048dd Mon Sep 17 00:00:00 2001 From: Iwao AVE! Date: Sun, 5 Jan 2025 08:22:34 +0900 Subject: [PATCH 2/2] Handle multiple result sets (implicit cursor) with Oracle driver Oracle driver... ... throws `SQLException` with no good reason when calling `Statement#getResultSet()`. `SQLException` is thrown "if a database access error occurs or this method is called on a closed Statement". https://docs.oracle.com/en/java/javase/23/docs/api/java.sql/java/sql/Statement.html#getResultSet() https://stackoverflow.com/q/42091653/1261766 ... always returns `false` from `DatabaseMetaData#supportsMultipleResultSets()` even though it apparently does. --- .../resultset/DefaultResultSetHandler.java | 55 ++++++++++++------- .../DefaultResultSetHandlerTest.java | 5 +- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java index 34f6e8fca2c..803ddfaca96 100644 --- a/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java +++ b/src/main/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandler.java @@ -247,34 +247,49 @@ public Cursor handleCursorResultSets(Statement stmt) throws SQLException } private ResultSetWrapper getFirstResultSet(Statement stmt) throws SQLException { - ResultSet rs = stmt.getResultSet(); - while (rs == null) { - // move forward to get the first resultset in case the driver - // doesn't return the resultset as the first result (HSQLDB) - if (stmt.getMoreResults()) { - rs = stmt.getResultSet(); - } else if (stmt.getUpdateCount() == -1) { - // no more results. Must be no resultset - break; + ResultSet rs = null; + SQLException e1 = null; + + try { + rs = stmt.getResultSet(); + } catch (SQLException e) { + // Oracle throws ORA-17283 for implicit cursor + e1 = e; + } + + try { + while (rs == null) { + // move forward to get the first resultset in case the driver + // doesn't return the resultset as the first result (HSQLDB) + if (stmt.getMoreResults()) { + rs = stmt.getResultSet(); + } else if (stmt.getUpdateCount() == -1) { + // no more results. Must be no resultset + break; + } } + } catch (SQLException e) { + throw e1 != null ? e1 : e; } + return rs != null ? new ResultSetWrapper(rs, configuration) : null; } private ResultSetWrapper getNextResultSet(Statement stmt) { // Making this method tolerant of bad JDBC drivers try { - if (stmt.getConnection().getMetaData().supportsMultipleResultSets()) { - // Crazy Standard JDBC way of determining if there are more results - // DO NOT try to 'improve' the condition even if IDE tells you to! - // It's important that getUpdateCount() is called here. - if (!(!stmt.getMoreResults() && stmt.getUpdateCount() == -1)) { - ResultSet rs = stmt.getResultSet(); - if (rs == null) { - return getNextResultSet(stmt); - } else { - return new ResultSetWrapper(rs, configuration); - } + // We stopped checking DatabaseMetaData#supportsMultipleResultSets() + // because Oracle driver (incorrectly) returns false + + // Crazy Standard JDBC way of determining if there are more results + // DO NOT try to 'improve' the condition even if IDE tells you to! + // It's important that getUpdateCount() is called here. + if (!(!stmt.getMoreResults() && stmt.getUpdateCount() == -1)) { + ResultSet rs = stmt.getResultSet(); + if (rs == null) { + return getNextResultSet(stmt); + } else { + return new ResultSetWrapper(rs, configuration); } } } catch (Exception e) { diff --git a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java index 734cdb21b7d..0d428f25724 100644 --- a/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java +++ b/src/test/java/org/apache/ibatis/executor/resultset/DefaultResultSetHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -92,9 +92,6 @@ void shouldRetainColumnNameCase() throws Exception { when(rsmd.getColumnLabel(1)).thenReturn("CoLuMn1"); when(rsmd.getColumnType(1)).thenReturn(Types.INTEGER); when(rsmd.getColumnClassName(1)).thenReturn(Integer.class.getCanonicalName()); - when(stmt.getConnection()).thenReturn(conn); - when(conn.getMetaData()).thenReturn(dbmd); - when(dbmd.supportsMultipleResultSets()).thenReturn(false); // for simplicity. final List results = fastResultSetHandler.handleResultSets(stmt); assertEquals(1, results.size());