Skip to content

Commit e5518b7

Browse files
authored
Merge pull request #14 from qaspen-python/transaction_fetch_val
Implement fetch_val method for Transaction
2 parents d18ec8b + dcbefeb commit e5518b7

File tree

4 files changed

+143
-24
lines changed

4 files changed

+143
-24
lines changed

python/psqlpy/_internal/__init__.pyi

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,11 +412,13 @@ class Transaction:
412412
querystring: str,
413413
parameters: list[Any] | None = None,
414414
) -> SingleQueryResult:
415-
"""Execute the query and return first row.
415+
"""Fetch exaclty single row from query.
416416
417+
Query must return exactly one row, otherwise error will be raised.
417418
Querystring can contain `$<number>` parameters
418419
for converting them in the driver side.
419420
421+
420422
### Parameters:
421423
- `querystring`: querystring to execute.
422424
- `parameters`: list of parameters to pass in the query.
@@ -444,6 +446,54 @@ class Transaction:
444446
445447
# Or you can transaction as a async context manager
446448
449+
async def main() -> None:
450+
db_pool = PSQLPool()
451+
await psqlpy.startup()
452+
453+
transaction = await db_pool.transaction()
454+
async with transaction:
455+
query_result: SingleQueryResult = await transaction.execute(
456+
"SELECT username FROM users WHERE id = $1 LIMIT 1",
457+
[100],
458+
)
459+
dict_result: Dict[Any, Any] = query_result.result()
460+
# This way transaction begins and commits by itself.
461+
```
462+
"""
463+
async def fetch_val(
464+
self: Self,
465+
querystring: str,
466+
parameters: list[Any] | None = None,
467+
) -> Any | None:
468+
"""Execute the query and return first value of the first row.
469+
470+
Querystring can contain `$<number>` parameters
471+
for converting them in the driver side.
472+
473+
### Parameters:
474+
- `querystring`: querystring to execute.
475+
- `parameters`: list of parameters to pass in the query.
476+
477+
### Example:
478+
```python
479+
import asyncio
480+
481+
from psqlpy import PSQLPool, QueryResult
482+
483+
484+
async def main() -> None:
485+
db_pool = PSQLPool()
486+
await db_pool.startup()
487+
488+
transaction = await db_pool.transaction()
489+
await transaction.begin()
490+
value: Any | None = await transaction.execute(
491+
"SELECT username FROM users WHERE id = $1",
492+
[100],
493+
)
494+
495+
# Or you can transaction as a async context manager
496+
447497
async def main() -> None:
448498
db_pool = PSQLPool()
449499
await psqlpy.startup()

python/tests/test_transaction.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,48 @@ async def test_transaction_fetch_row(
244244
async with connection.transaction() as transaction:
245245
database_single_query_result: typing.Final = (
246246
await transaction.fetch_row(
247-
f"SELECT * FROM {table_name}",
247+
f"SELECT * FROM {table_name} LIMIT 1",
248248
[],
249249
)
250250
)
251251
result = database_single_query_result.result()
252252
assert isinstance(result, dict)
253+
254+
255+
async def test_transaction_fetch_row_more_than_one_row(
256+
psql_pool: PSQLPool,
257+
table_name: str,
258+
) -> None:
259+
connection = await psql_pool.connection()
260+
async with connection.transaction() as transaction:
261+
with pytest.raises(RustPSQLDriverPyBaseError):
262+
await transaction.fetch_row(
263+
f"SELECT * FROM {table_name}",
264+
[],
265+
)
266+
267+
268+
async def test_transaction_fetch_val(
269+
psql_pool: PSQLPool,
270+
table_name: str,
271+
) -> None:
272+
connection = await psql_pool.connection()
273+
async with connection.transaction() as transaction:
274+
value: typing.Final = await transaction.fetch_val(
275+
f"SELECT COUNT(*) FROM {table_name}",
276+
[],
277+
)
278+
assert isinstance(value, int)
279+
280+
281+
async def test_transaction_fetch_val_more_than_one_row(
282+
psql_pool: PSQLPool,
283+
table_name: str,
284+
) -> None:
285+
connection = await psql_pool.connection()
286+
async with connection.transaction() as transaction:
287+
with pytest.raises(RustPSQLDriverPyBaseError):
288+
await transaction.fetch_row(
289+
f"SELECT * FROM {table_name}",
290+
[],
291+
)

src/driver/transaction.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
common::rustengine_future,
77
exceptions::rust_errors::{RustPSQLDriverError, RustPSQLDriverPyResult},
88
query_result::{PSQLDriverPyQueryResult, PSQLDriverSinglePyQueryResult},
9-
value_converter::{convert_parameters, PythonDTO},
9+
value_converter::{convert_parameters, postgres_to_py, PythonDTO},
1010
};
1111
use deadpool_postgres::Object;
1212
use futures_util::future;
@@ -17,6 +17,7 @@ use pyo3::{
1717
};
1818
use std::{collections::HashSet, sync::Arc, vec};
1919
use tokio_postgres::types::ToSql;
20+
2021
/// Transaction for internal use only.
2122
///
2223
/// It is not exposed to python.
@@ -165,7 +166,7 @@ impl RustTransaction {
165166
Ok(())
166167
}
167168

168-
/// Fetch single row from query.
169+
/// Fetch exaclty single row from query.
169170
///
170171
/// Method doesn't acquire lock on any structure fields.
171172
/// It prepares and caches querystring in the inner Object object.
@@ -178,6 +179,7 @@ impl RustTransaction {
178179
/// 2) Transaction is done already
179180
/// 3) Can not create/retrieve prepared statement
180181
/// 4) Can not execute statement
182+
/// 5) Query returns more than one row
181183
pub async fn inner_fetch_row(
182184
&self,
183185
querystring: String,
@@ -210,7 +212,7 @@ impl RustTransaction {
210212
let statement = db_client_guard.prepare_cached(&querystring).await?;
211213

212214
let result = db_client_guard
213-
.query(&statement, &vec_parameters.into_boxed_slice())
215+
.query_one(&statement, &vec_parameters.into_boxed_slice())
214216
.await?;
215217

216218
Ok(PSQLDriverSinglePyQueryResult::new(result))
@@ -755,6 +757,7 @@ impl Transaction {
755757
/// May return Err Result if:
756758
/// 1) Cannot convert python parameters
757759
/// 2) Cannot execute querystring.
760+
/// 3) Query returns more than one row.
758761
pub fn fetch_row<'a>(
759762
&'a self,
760763
py: Python<'a>,
@@ -773,6 +776,42 @@ impl Transaction {
773776
})
774777
}
775778

779+
/// Execute querystring with parameters and return first value in the first row.
780+
///
781+
/// It converts incoming parameters to rust readable,
782+
/// executes query with them and returns first row of response.
783+
///
784+
/// # Errors
785+
///
786+
/// May return Err Result if:
787+
/// 1) Cannot convert python parameters
788+
/// 2) Cannot execute querystring.
789+
/// 3) Query returns more than one row
790+
pub fn fetch_val<'a>(
791+
&'a self,
792+
py: Python<'a>,
793+
querystring: String,
794+
parameters: Option<&'a PyList>,
795+
) -> RustPSQLDriverPyResult<&PyAny> {
796+
let transaction_arc = self.transaction.clone();
797+
let mut params: Vec<PythonDTO> = vec![];
798+
if let Some(parameters) = parameters {
799+
params = convert_parameters(parameters)?;
800+
}
801+
802+
rustengine_future(py, async move {
803+
let transaction_guard = transaction_arc.read().await;
804+
let first_row = transaction_guard
805+
.inner_fetch_row(querystring, params)
806+
.await?
807+
.get_inner();
808+
Python::with_gil(|py| match first_row.columns().first() {
809+
Some(first_column) => postgres_to_py(py, &first_row, first_column, 0),
810+
None => Ok(py.None()),
811+
})
812+
})
813+
}
814+
776815
/// Execute querystrings with parameters and return all results.
777816
///
778817
/// Create pipeline of queries.

src/query_result.rs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
use pyo3::{pyclass, pymethods, types::PyDict, Py, PyAny, Python, ToPyObject};
22
use tokio_postgres::Row;
33

4-
use crate::{
5-
exceptions::rust_errors::{RustPSQLDriverError, RustPSQLDriverPyResult},
6-
value_converter::postgres_to_py,
7-
};
4+
use crate::{exceptions::rust_errors::RustPSQLDriverPyResult, value_converter::postgres_to_py};
85

96
/// Convert postgres `Row` into Python Dict.
107
///
@@ -82,16 +79,20 @@ impl PSQLDriverPyQueryResult {
8279
#[pyclass(name = "SingleQueryResult")]
8380
#[allow(clippy::module_name_repetitions)]
8481
pub struct PSQLDriverSinglePyQueryResult {
85-
inner: Vec<Row>,
82+
inner: Row,
8683
}
8784

8885
impl PSQLDriverSinglePyQueryResult {
8986
#[must_use]
90-
pub fn new(database_row: Vec<Row>) -> Self {
87+
pub fn new(database_row: Row) -> Self {
9188
PSQLDriverSinglePyQueryResult {
9289
inner: database_row,
9390
}
9491
}
92+
93+
pub fn get_inner(self) -> Row {
94+
self.inner
95+
}
9596
}
9697

9798
#[pymethods]
@@ -106,12 +107,7 @@ impl PSQLDriverSinglePyQueryResult {
106107
/// postgres type to python, can not set new key-value pair
107108
/// in python dict or there are no result.
108109
pub fn result(&self, py: Python<'_>) -> RustPSQLDriverPyResult<Py<PyAny>> {
109-
if let Some(row) = self.inner.first() {
110-
return Ok(row_to_dict(py, row)?.to_object(py));
111-
}
112-
Err(RustPSQLDriverError::RustToPyValueConversionError(
113-
"There are not results from the query, can't return first row.".into(),
114-
))
110+
Ok(row_to_dict(py, &self.inner)?.to_object(py))
115111
}
116112

117113
/// Convert result from database to any class passed from Python.
@@ -126,12 +122,7 @@ impl PSQLDriverSinglePyQueryResult {
126122
py: Python<'a>,
127123
as_class: &'a PyAny,
128124
) -> RustPSQLDriverPyResult<&'a PyAny> {
129-
if let Some(row) = self.inner.first() {
130-
let pydict: &PyDict = row_to_dict(py, row)?;
131-
return Ok(as_class.call((), Some(pydict))?);
132-
}
133-
Err(RustPSQLDriverError::RustToPyValueConversionError(
134-
"There are not results from the query, can't convert first row.".into(),
135-
))
125+
let pydict: &PyDict = row_to_dict(py, &self.inner)?;
126+
Ok(as_class.call((), Some(pydict))?)
136127
}
137128
}

0 commit comments

Comments
 (0)