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