Skip to content

Commit 2bbe1fc

Browse files
committed
Added close method for cursor and scroll option for cursor
1 parent 44f5481 commit 2bbe1fc

File tree

8 files changed

+94
-11
lines changed

8 files changed

+94
-11
lines changed

README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Driver for PostgreSQL written fully in Rust and exposed to Python.
44
*Normal documentation is in development.*
55

6-
## Installation
6+
# Installation
77

88
You can install package with `pip` or `poetry`.
99

@@ -53,7 +53,7 @@ async def main() -> None:
5353
```
5454
Please take into account that each new execute gets new connection from connection pool.
5555

56-
## Query parameters
56+
# Query parameters
5757
You can pass parameters into queries.
5858
Parameters can be passed in any `execute` method as the second parameter, it must be a list.
5959
Any placeholder must be marked with `$< num>`.
@@ -65,7 +65,7 @@ Any placeholder must be marked with `$< num>`.
6565
)
6666
```
6767

68-
## Connection
68+
# Connection
6969
You can work with connection instead of DatabasePool.
7070
```python
7171
from typing import Any
@@ -96,7 +96,7 @@ async def main() -> None:
9696
# rust does it instead.
9797
```
9898

99-
## Transactions
99+
# Transactions
100100
Of course it's possible to use transactions with this driver.
101101
It's as easy as possible and sometimes it copies common functionality from PsycoPG and AsyncPG.
102102

@@ -254,11 +254,18 @@ async def main() -> None:
254254
await transaction.commit()
255255
```
256256

257-
## Cursors
257+
# Cursors
258258
Library supports PostgreSQL cursors.
259259

260260
Cursors can be created only in transaction. In addition, cursor supports async iteration.
261261

262+
### Cursor parameters
263+
In process of cursor creation you can specify some configuration parameters.
264+
- `querystring`: query for the cursor. Required.
265+
- `parameters`: parameters for the query. Not Required.
266+
- `fetch_number`: number of records per fetch if cursor is used as an async iterator. If you are using `.fetch()` method you can pass different fetch number. Not required. Default - 10.
267+
- `scroll`: set `SCROLL` if True or `NO SCROLL` if False. Not required. By default - `None`.
268+
262269
```python
263270
from typing import Any
264271

@@ -283,9 +290,16 @@ async def main() -> None:
283290
fetch_number=100,
284291
)
285292

293+
# You can manually fetch results from cursor
294+
results: list[dict[str, Any]] = await cursor.fetch(fetch_number=8)
295+
296+
# Or you can use it as an async iterator.
286297
async for fetched_result in cursor:
287298
print(fetched_result.result())
288299

300+
# If you want to close cursor, please do it manually.
301+
await cursor.close()
302+
289303
await transaction.commit()
290304
```
291305

python/psqlpy/_internal/__init__.pyi

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ class Cursor:
4949
result as `QueryResult`.
5050
"""
5151

52+
async def close(self: Self) -> None:
53+
"""Close the cursor.
54+
55+
Execute CLOSE command for the cursor.
56+
"""
57+
5258
def __aiter__(self: Self) -> Self:
5359
...
5460

@@ -247,6 +253,7 @@ class Transaction:
247253
querystring: str,
248254
parameters: List[Any] | None = None,
249255
fetch_number: int | None = None,
256+
scroll: bool | None = None,
250257
) -> Cursor:
251258
"""Create new cursor object.
252259
@@ -256,6 +263,7 @@ class Transaction:
256263
- `querystring`: querystring to execute.
257264
- `parameters`: list of parameters to pass in the query.
258265
- `fetch_number`: how many rows need to fetch.
266+
- `scroll`: SCROLL or NO SCROLL cursor.
259267
260268
### Returns:
261269
new initialized cursor.

python/psqlpy/_internal/exceptions.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ class DBPoolConfigurationError(RustPSQLDriverPyBaseError):
4040

4141
class UUIDValueConvertError(RustPSQLDriverPyBaseError):
4242
"""Error if it's impossible to convert py string UUID into rust UUID."""
43+
44+
45+
class CursorError(RustPSQLDriverPyBaseError):
46+
"""Error if something goes wrong with the cursor."""

python/psqlpy/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
DBTransactionError,
77
DBPoolConfigurationError,
88
UUIDValueConvertError,
9+
CursorError,
910
)
1011

1112
__all__ = [
@@ -16,4 +17,5 @@
1617
"DBTransactionError",
1718
"DBPoolConfigurationError",
1819
"UUIDValueConvertError",
20+
"CursorError",
1921
]

src/driver/cursor.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub struct Cursor {
1212
db_client: Arc<tokio::sync::RwLock<Object>>,
1313
cursor_name: String,
1414
fetch_number: usize,
15+
closed: Arc<tokio::sync::RwLock<bool>>,
1516
}
1617

1718
impl Cursor {
@@ -24,6 +25,7 @@ impl Cursor {
2425
db_client,
2526
cursor_name,
2627
fetch_number,
28+
closed: Arc::new(tokio::sync::RwLock::new(false)),
2729
};
2830
}
2931
}
@@ -82,4 +84,39 @@ impl Cursor {
8284

8385
Ok(Some(future?.into()))
8486
}
87+
88+
/// Close cursor.
89+
///
90+
/// # Errors
91+
/// May return Err Result if cannot execute CLOSE command
92+
pub fn close<'a>(&'a self, py: Python<'a>) -> RustPSQLDriverPyResult<&PyAny> {
93+
let db_client_arc = self.db_client.clone();
94+
let cursor_name = self.cursor_name.clone();
95+
let closed = self.closed.clone();
96+
97+
rustengine_future(py, async move {
98+
let is_closed = {
99+
let closed_read = closed.write().await;
100+
*closed_read
101+
};
102+
if is_closed {
103+
return Err(
104+
crate::exceptions::rust_errors::RustPSQLDriverError::DBCursorError(
105+
"Cursor is already closed".into(),
106+
),
107+
);
108+
}
109+
110+
let db_client_guard = db_client_arc.read().await;
111+
112+
db_client_guard
113+
.batch_execute(format!("CLOSE {cursor_name}").as_str())
114+
.await?;
115+
116+
let mut closed_write = closed.write().await;
117+
*closed_write = true;
118+
119+
Ok(())
120+
})
121+
}
85122
}

src/driver/transaction.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ impl RustTransaction {
427427
querystring: String,
428428
parameters: Vec<PythonDTO>,
429429
fetch_number: usize,
430+
scroll: Option<bool>,
430431
) -> RustPSQLDriverPyResult<Cursor> {
431432
let db_client_arc = self.db_client.clone();
432433
let db_client_arc2 = self.db_client.clone();
@@ -437,12 +438,22 @@ impl RustTransaction {
437438
vec_parameters.push(param);
438439
}
439440

441+
let mut cursor_init_query = "DECLARE".to_string();
442+
cursor_init_query.push_str(format!(" cur{}", self.cursor_num).as_str());
443+
444+
if let Some(scroll) = scroll {
445+
if scroll == true {
446+
cursor_init_query.push_str(" SCROLL");
447+
} else {
448+
cursor_init_query.push_str(" NO SCROLL");
449+
}
450+
}
451+
452+
cursor_init_query.push_str(format!(" CURSOR FOR {querystring}").as_str());
453+
440454
let cursor_name = format!("cur{}", self.cursor_num);
441455
db_client_guard
442-
.execute(
443-
&format!("DECLARE {} CURSOR FOR {querystring}", cursor_name),
444-
&vec_parameters.into_boxed_slice(),
445-
)
456+
.execute(&cursor_init_query, &vec_parameters.into_boxed_slice())
446457
.await?;
447458

448459
self.cursor_num = self.cursor_num + 1;
@@ -682,6 +693,7 @@ impl Transaction {
682693
querystring: String,
683694
parameters: Option<&'a PyAny>,
684695
fetch_number: Option<usize>,
696+
scroll: Option<bool>,
685697
) -> RustPSQLDriverPyResult<&PyAny> {
686698
let transaction_arc = self.transaction.clone();
687699
let mut params: Vec<PythonDTO> = vec![];
@@ -692,7 +704,7 @@ impl Transaction {
692704
rustengine_future(py, async move {
693705
let mut transaction_guard = transaction_arc.write().await;
694706
Ok(transaction_guard
695-
.inner_cursor(querystring, params, fetch_number.unwrap_or(10))
707+
.inner_cursor(querystring, params, fetch_number.unwrap_or(10), scroll)
696708
.await?)
697709
})
698710
}

src/exceptions/python_errors.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ create_exception!(
3333
RustPSQLDriverPyBaseError
3434
);
3535

36+
create_exception!(psqlpy.exceptions, CursorError, RustPSQLDriverPyBaseError);
37+
3638
pub fn python_exceptions_module(py: Python<'_>, pymod: &PyModule) -> PyResult<()> {
3739
pymod.add(
3840
"RustPSQLDriverPyBaseError",
@@ -56,5 +58,6 @@ pub fn python_exceptions_module(py: Python<'_>, pymod: &PyModule) -> PyResult<()
5658
"UUIDValueConvertError",
5759
py.get_type::<UUIDValueConvertError>(),
5860
)?;
61+
pymod.add("CursorError", py.get_type::<CursorError>())?;
5962
Ok(())
6063
}

src/exceptions/rust_errors.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::exceptions::python_errors::{
55
RustPSQLDriverPyBaseError, RustToPyValueMappingError,
66
};
77

8-
use super::python_errors::UUIDValueConvertError;
8+
use super::python_errors::{CursorError, UUIDValueConvertError};
99

1010
pub type RustPSQLDriverPyResult<T> = Result<T, RustPSQLDriverError>;
1111

@@ -21,6 +21,8 @@ pub enum RustPSQLDriverError {
2121
DataBaseTransactionError(String),
2222
#[error("Configuration database pool error: {0}")]
2323
DataBasePoolConfigurationError(String),
24+
#[error("Cursor error: {0}")]
25+
DBCursorError(String),
2426

2527
#[error("Python exception: {0}.")]
2628
PyError(#[from] pyo3::PyErr),
@@ -62,6 +64,7 @@ impl From<RustPSQLDriverError> for pyo3::PyErr {
6264
DBPoolConfigurationError::new_err((error_desc,))
6365
}
6466
RustPSQLDriverError::UUIDConvertError(_) => UUIDValueConvertError::new_err(error_desc),
67+
RustPSQLDriverError::DBCursorError(_) => CursorError::new_err(error_desc),
6568
}
6669
}
6770
}

0 commit comments

Comments
 (0)