Skip to content

Commit 627441e

Browse files
Added support for scrollable cursors in thin mode.
1 parent e0efd6f commit 627441e

15 files changed

+468
-42
lines changed

doc/src/api_manual/async_cursor.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,25 @@ AsyncCursor Methods
326326
:meth:`AsyncCursor.callfunc()`, the first parameter in the list refers
327327
to the return value of the PL/SQL function.
328328

329+
.. method:: AsyncCursor.scroll(value=0, mode="relative")
330+
331+
Scrolls the cursor in the result set to a new position according to the
332+
mode.
333+
334+
If mode is *relative* (the default value), the value is taken as an offset
335+
to the current position in the result set. If set to *absolute*, value
336+
states an absolute target position. If set to *first*, the cursor is
337+
positioned at the first row and if set to *last*, the cursor is set to the
338+
last row in the result set.
339+
340+
An error is raised if the mode is *relative* or *absolute* and the scroll
341+
operation would position the cursor outside of the result set.
342+
343+
.. note::
344+
345+
This method is an extension to the DB API definition but it is
346+
mentioned in PEP 249 as an optional extension.
347+
329348
.. method:: AsyncCursor.setoutputsize(size, [column])
330349

331350
This method does nothing and is retained solely for compatibility with the

doc/src/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ oracledb 3.1.0 (TBD)
1717
Thin Mode Changes
1818
+++++++++++++++++
1919

20+
#) Added support for :ref:`scrollable cursors <scrollablecursors>`.
2021
#) Improved support for :ref:`Oracle Advanced Queuing <aqusermanual>`:
2122

2223
- Added support for JSON payloads

doc/src/user_guide/appendix_a.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ see :ref:`driverdiff` and :ref:`compatibility`.
260260
- Yes
261261
- Yes
262262
* - Scrollable cursors (see :ref:`scrollablecursors`)
263-
- No
263+
- Yes
264264
- Yes
265265
- Yes
266266
* - Oracle Database startup and shutdown (see :ref:`startup`)

doc/src/user_guide/sql_execution.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -625,11 +625,6 @@ rows, and to move to a particular row in a query result set. The result set is
625625
cached on the database server until the cursor is closed. In contrast, regular
626626
cursors are restricted to moving forward.
627627

628-
.. note::
629-
630-
Scrollable cursors are only supported in the python-oracledb Thick mode. See
631-
:ref:`enablingthick`.
632-
633628
A scrollable cursor is created by setting the parameter ``scrollable=True``
634629
when creating the cursor. The method :meth:`Cursor.scroll()` is used to move to
635630
different locations in the result set.
@@ -656,6 +651,10 @@ Examples are:
656651
cursor.scroll(-4)
657652
print("SKIP BACK 4 ROWS:", cursor.fetchone())
658653
654+
See `samples/scrollable_cursors.py <https://github.com/oracle/python-oracledb/
655+
blob/main/samples/scrollable_cursors.py>`__ for a runnable example.
656+
657+
659658
.. _fetchobjects:
660659

661660
Fetching Oracle Database Objects and Collections

samples/scrollable_cursors.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# Copyright (c) 2016, 2023, Oracle and/or its affiliates.
2+
# Copyright (c) 2016, 2025, Oracle and/or its affiliates.
33
#
44
# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved.
55
#
@@ -38,8 +38,9 @@
3838
import oracledb
3939
import sample_env
4040

41-
# this script is currently only supported in python-oracledb thick mode
42-
oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client())
41+
# determine whether to use python-oracledb thin mode or thick mode
42+
if not sample_env.get_is_thin():
43+
oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client())
4344

4445
connection = oracledb.connect(
4546
user=sample_env.get_main_user(),

src/oracledb/cursor.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# Copyright (c) 2021, 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2021, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -846,17 +846,17 @@ def scroll(self, value: int = 0, mode: str = "relative") -> None:
846846
Scroll the cursor in the result set to a new position according to the
847847
mode.
848848
849-
If mode is relative (the default value), the value is taken as an
850-
offset to the current position in the result set. If set to absolute,
851-
value states an absolute target position. If set to first, the cursor
852-
is positioned at the first row and if set to last, the cursor is set
849+
If mode is "relative" (the default value), the value is taken as an
850+
offset to the current position in the result set. If set to "absolute",
851+
value states an absolute target position. If set to "first", the cursor
852+
is positioned at the first row and if set to "last", the cursor is set
853853
to the last row in the result set.
854854
855-
An error is raised if the mode is relative or absolute and the
855+
An error is raised if the mode is "relative" or "absolute" and the
856856
scroll operation would position the cursor outside of the result set.
857857
"""
858858
self._verify_open()
859-
self._impl.scroll(self.connection, value, mode)
859+
self._impl.scroll(self, value, mode)
860860

861861

862862
class AsyncCursor(BaseCursor):
@@ -1081,3 +1081,20 @@ async def parse(self, statement: str) -> None:
10811081
self._verify_open()
10821082
self._prepare(statement)
10831083
await self._impl.parse(self)
1084+
1085+
async def scroll(self, value: int = 0, mode: str = "relative") -> None:
1086+
"""
1087+
Scroll the cursor in the result set to a new position according to the
1088+
mode.
1089+
1090+
If mode is "relative" (the default value), the value is taken as an
1091+
offset to the current position in the result set. If set to "absolute",
1092+
value states an absolute target position. If set to "first", the cursor
1093+
is positioned at the first row and if set to "last", the cursor is set
1094+
to the last row in the result set.
1095+
1096+
An error is raised if the mode is "relative" or "absolute" and the
1097+
scroll operation would position the cursor outside of the result set.
1098+
"""
1099+
self._verify_open()
1100+
await self._impl.scroll(self, value, mode)

src/oracledb/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def _raise_not_supported(feature: str) -> None:
282282
ERR_ARROW_C_API_ERROR = 2060
283283
ERR_PARAMS_HOOK_HANDLER_FAILED = 2061
284284
ERR_PAYLOAD_CANNOT_BE_ENQUEUED = 2062
285+
ERR_SCROLL_OUT_OF_RESULT_SET = 2063
285286

286287
# error numbers that result in NotSupportedError
287288
ERR_TIME_NOT_SUPPORTED = 3000
@@ -428,6 +429,7 @@ def _raise_not_supported(feature: str) -> None:
428429
ERR_DPI_ERROR_XREF = {
429430
1010: ERR_NOT_CONNECTED,
430431
1024: (ERR_INVALID_COLL_INDEX_GET, r"at index (?P<index>\d+) does"),
432+
1027: ERR_SCROLL_OUT_OF_RESULT_SET,
431433
1043: ERR_INVALID_NUMBER,
432434
1044: ERR_ORACLE_NUMBER_NO_REPR,
433435
1063: ERR_EXECUTE_MODE_ONLY_FOR_DML,
@@ -772,6 +774,9 @@ def _raise_not_supported(feature: str) -> None:
772774
ERR_PYTHON_VALUE_NOT_SUPPORTED: (
773775
'Python value of type "{type_name}" is not supported'
774776
),
777+
ERR_SCROLL_OUT_OF_RESULT_SET: (
778+
"scroll operation would go out of the result set"
779+
),
775780
ERR_SELF_BIND_NOT_SUPPORTED: "binding to self is not supported",
776781
ERR_CONNECTION_CLOSED: "the database or network closed the connection",
777782
ERR_SERVER_VERSION_NOT_SUPPORTED: (

src/oracledb/impl/base/cursor.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ cdef class BaseCursorImpl:
664664
"""
665665
self._prepare(statement, tag, cache_statement)
666666

667-
def scroll(self, conn, value, mode):
667+
def scroll(self, cursor, value, mode):
668668
"""
669669
Scrolls a scrollable cursor.
670670
"""

src/oracledb/impl/thick/cursor.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ cdef class ThickCursorImpl(BaseCursorImpl):
510510
if num_query_cols > 0:
511511
self._perform_define(cursor, num_query_cols)
512512

513-
def scroll(self, object conn, int32_t offset, object mode):
513+
def scroll(self, object cursor, int32_t offset, object mode):
514514
cdef:
515515
uint32_t temp_buffer_row_index = 0, num_rows_in_buffer = 0
516516
bint more_rows_to_fetch = False

src/oracledb/impl/thin/constants.pxi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ cdef enum:
239239
cdef enum:
240240
TNS_EXEC_FLAGS_DML_ROWCOUNTS = 0x4000
241241
TNS_EXEC_FLAGS_IMPLICIT_RESULTSET = 0x8000
242+
TNS_EXEC_FLAGS_SCROLLABLE = 0x02
243+
244+
# fetch orientations
245+
cdef enum:
246+
TNS_FETCH_ORIENTATION_ABSOLUTE = 0x20
247+
TNS_FETCH_ORIENTATION_CURRENT = 0x01
248+
TNS_FETCH_ORIENTATION_FIRST = 0x04
249+
TNS_FETCH_ORIENTATION_LAST = 0x08
250+
TNS_FETCH_ORIENTATION_NEXT = 0x02
251+
TNS_FETCH_ORIENTATION_PRIOR = 0x10
252+
TNS_FETCH_ORIENTATION_RELATIVE = 0x40
242253

243254
# server side piggyback op codes
244255
cdef enum:

src/oracledb/impl/thin/cursor.pyx

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ cdef class BaseThinCursorImpl(BaseCursorImpl):
3737
list _batcherrors
3838
list _dmlrowcounts
3939
list _implicit_resultsets
40+
uint64_t _buffer_min_row
41+
uint64_t _buffer_max_row
4042
uint32_t _num_columns
4143
uint32_t _last_row_index
4244
Rowid _lastrowid
@@ -61,6 +63,64 @@ cdef class BaseThinCursorImpl(BaseCursorImpl):
6163
message.cursor_impl = self
6264
return message
6365

66+
cdef ExecuteMessage _create_execute_message(self, object cursor):
67+
"""
68+
Creates and returns the message used to execute a statement once.
69+
"""
70+
cdef ExecuteMessage message
71+
message = self._create_message(ExecuteMessage, cursor)
72+
message.num_execs = 1
73+
if self.scrollable:
74+
message.fetch_orientation = TNS_FETCH_ORIENTATION_CURRENT
75+
message.fetch_pos = 1
76+
return message
77+
78+
cdef ExecuteMessage _create_scroll_message(self, object cursor,
79+
object mode, int32_t offset):
80+
"""
81+
Creates a message object that is used to send a scroll request to the
82+
database and receive back its response.
83+
"""
84+
cdef:
85+
ExecuteMessage message
86+
uint32_t orientation
87+
uint64_t desired_row
88+
89+
# check mode and calculate desired row
90+
if mode == "relative":
91+
if <int64_t> (self.rowcount + offset) < 1:
92+
errors._raise_err(errors.ERR_SCROLL_OUT_OF_RESULT_SET)
93+
orientation = TNS_FETCH_ORIENTATION_RELATIVE
94+
desired_row = self.rowcount + offset
95+
elif mode == "absolute":
96+
orientation = TNS_FETCH_ORIENTATION_ABSOLUTE
97+
desired_row = <uint64_t> offset
98+
elif mode == "first":
99+
orientation = TNS_FETCH_ORIENTATION_FIRST
100+
desired_row = 1
101+
elif mode == "last":
102+
orientation = TNS_FETCH_ORIENTATION_LAST
103+
else:
104+
errors._raise_err(errors.ERR_WRONG_SCROLL_MODE)
105+
106+
# determine if the server needs to be contacted at all
107+
# for "last", the server is always contacted
108+
if orientation != TNS_FETCH_ORIENTATION_LAST \
109+
and desired_row >= self._buffer_min_row \
110+
and desired_row < self._buffer_max_row:
111+
self._buffer_index = \
112+
<uint32_t> (desired_row - self._buffer_min_row)
113+
self._buffer_rowcount = self._buffer_max_row - desired_row
114+
self.rowcount = desired_row - 1
115+
return None
116+
117+
# build message
118+
message = self._create_message(ExecuteMessage, cursor)
119+
message.scroll_operation = self._more_rows_to_fetch
120+
message.fetch_orientation = orientation
121+
message.fetch_pos = <uint32_t> desired_row
122+
return message
123+
64124
cdef BaseVarImpl _create_var_impl(self, object conn):
65125
cdef ThinVarImpl var_impl
66126
var_impl = ThinVarImpl.__new__(ThinVarImpl)
@@ -125,6 +185,28 @@ cdef class BaseThinCursorImpl(BaseCursorImpl):
125185
errors._raise_err(errors.ERR_MISSING_BIND_VALUE,
126186
name=bind_info._bind_name)
127187

188+
cdef int _post_process_scroll(self, ExecuteMessage message) except -1:
189+
"""
190+
Called after a scroll operation has completed successfully. The row
191+
count and buffer row counts and indices are updated as required.
192+
"""
193+
if self._buffer_rowcount == 0:
194+
if message.fetch_orientation not in (
195+
TNS_FETCH_ORIENTATION_FIRST,
196+
TNS_FETCH_ORIENTATION_LAST
197+
):
198+
errors._raise_err(errors.ERR_SCROLL_OUT_OF_RESULT_SET)
199+
self.rowcount = 0
200+
self._more_rows_to_fetch = False
201+
self._buffer_index = 0
202+
self._buffer_min_row = 0
203+
self._buffer_max_row = 0
204+
else:
205+
self.rowcount = message.error_info.rowcount - self._buffer_rowcount
206+
self._buffer_min_row = self.rowcount + 1
207+
self._buffer_max_row = self._buffer_min_row + self._buffer_rowcount
208+
self._buffer_index = 0
209+
128210
cdef int _set_fetch_array_size(self, uint32_t value):
129211
"""
130212
Internal method for setting the fetch array size. This also ensures
@@ -182,15 +264,16 @@ cdef class ThinCursorImpl(BaseThinCursorImpl):
182264
else:
183265
message = self._create_message(FetchMessage, cursor)
184266
protocol._process_single_message(message)
267+
self._buffer_min_row = self.rowcount + 1
268+
self._buffer_max_row = self._buffer_min_row + self._buffer_rowcount
185269

186270
def execute(self, cursor):
187271
cdef:
188272
Protocol protocol = <Protocol> self._conn_impl._protocol
189273
object conn = cursor.connection
190274
MessageWithData message
191275
self._preprocess_execute(conn)
192-
message = self._create_message(ExecuteMessage, cursor)
193-
message.num_execs = 1
276+
message = self._create_execute_message(cursor)
194277
protocol._process_single_message(message)
195278
self.warning = message.warning
196279
if self._statement._is_query:
@@ -242,6 +325,15 @@ cdef class ThinCursorImpl(BaseThinCursorImpl):
242325
message.parse_only = True
243326
protocol._process_single_message(message)
244327

328+
def scroll(self, object cursor, int32_t offset, object mode):
329+
cdef:
330+
Protocol protocol = <Protocol> self._conn_impl._protocol
331+
ExecuteMessage message
332+
message = self._create_scroll_message(cursor, mode, offset)
333+
if message is not None:
334+
protocol._process_single_message(message)
335+
self._post_process_scroll(message)
336+
245337

246338
cdef class AsyncThinCursorImpl(BaseThinCursorImpl):
247339

@@ -268,6 +360,7 @@ cdef class AsyncThinCursorImpl(BaseThinCursorImpl):
268360
else:
269361
message = self._create_message(FetchMessage, cursor)
270362
await self._conn_impl._protocol._process_single_message(message)
363+
self._buffer_min_row = self.rowcount + 1
271364

272365
async def _preprocess_execute_async(self, object conn):
273366
"""
@@ -293,8 +386,7 @@ cdef class AsyncThinCursorImpl(BaseThinCursorImpl):
293386
MessageWithData message
294387
protocol = <BaseAsyncProtocol> self._conn_impl._protocol
295388
await self._preprocess_execute_async(conn)
296-
message = self._create_message(ExecuteMessage, cursor)
297-
message.num_execs = 1
389+
message = self._create_execute_message(cursor)
298390
await protocol._process_single_message(message)
299391
self.warning = message.warning
300392
if self._statement._is_query:
@@ -378,3 +470,13 @@ cdef class AsyncThinCursorImpl(BaseThinCursorImpl):
378470
message = self._create_message(ExecuteMessage, cursor)
379471
message.parse_only = True
380472
await protocol._process_single_message(message)
473+
474+
async def scroll(self, object cursor, int32_t offset, object mode):
475+
cdef:
476+
BaseAsyncProtocol protocol
477+
MessageWithData message
478+
protocol = <BaseAsyncProtocol> self._conn_impl._protocol
479+
message = self._create_scroll_message(cursor, mode, offset)
480+
if message is not None:
481+
await protocol._process_single_message(message)
482+
self._post_process_scroll(message)

0 commit comments

Comments
 (0)