From f25d817b829bca95c3967ce86347d018f7936e46 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Thu, 2 May 2024 20:13:45 +0200 Subject: [PATCH 1/2] Added fetch method, changed parameters annotations Signed-off-by: chandr-andr (Kiselev Aleksandr) --- python/psqlpy/_internal/__init__.pyi | 80 ++++++++++++++++++++++++---- src/driver/connection.rs | 49 +++++++++++++++++ src/driver/connection_pool.rs | 64 ++++++++++++++++++++++ src/driver/transaction.rs | 27 ++++++++++ src/value_converter.rs | 3 +- 5 files changed, 212 insertions(+), 11 deletions(-) diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index af6a8374..e628430d 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -1,6 +1,6 @@ import types from enum import Enum -from typing import Any, Callable, List, Optional, TypeVar +from typing import Any, Callable, List, Optional, Sequence, TypeVar from typing_extensions import Self @@ -337,7 +337,7 @@ class Transaction: async def execute( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> QueryResult: """Execute the query. @@ -375,7 +375,7 @@ class Transaction: async def execute_many( self: Self, querystring: str, - parameters: list[list[Any]] | None = None, + parameters: Sequence[Sequence[Any]] | None = None, prepared: bool = True, ) -> None: ... """Execute query multiple times with different parameters. @@ -410,10 +410,30 @@ class Transaction: await transaction.commit() ``` """ + async def fetch( + self: Self, + querystring: str, + parameters: Sequence[Any] | None = None, + prepared: bool = True, + ) -> QueryResult: + """Fetch the result from database. + + It's the same as `execute` method, we made it because people are used + to `fetch` method name. + + Querystring can contain `$` parameters + for converting them in the driver side. + + ### Parameters: + - `querystring`: querystring to execute. + - `parameters`: list of parameters to pass in the query. + - `prepared`: should the querystring be prepared before the request. + By default any querystring will be prepared. + """ async def fetch_row( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> SingleQueryResult: """Fetch exaclty single row from query. @@ -453,7 +473,7 @@ class Transaction: async def fetch_val( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> Any | None: """Execute the query and return first value of the first row. @@ -661,7 +681,7 @@ class Transaction: def cursor( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, fetch_number: int | None = None, scroll: bool | None = None, prepared: bool = True, @@ -717,7 +737,7 @@ class Connection: async def execute( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> QueryResult: """Execute the query. @@ -784,10 +804,30 @@ class Connection: ) ``` """ + async def fetch( + self: Self, + querystring: str, + parameters: Sequence[Any] | None = None, + prepared: bool = True, + ) -> QueryResult: + """Fetch the result from database. + + It's the same as `execute` method, we made it because people are used + to `fetch` method name. + + Querystring can contain `$` parameters + for converting them in the driver side. + + ### Parameters: + - `querystring`: querystring to execute. + - `parameters`: list of parameters to pass in the query. + - `prepared`: should the querystring be prepared before the request. + By default any querystring will be prepared. + """ async def fetch_row( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> SingleQueryResult: """Fetch exaclty single row from query. @@ -824,7 +864,7 @@ class Connection: async def fetch_val( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> Any: """Execute the query and return first value of the first row. @@ -979,7 +1019,7 @@ class ConnectionPool: async def execute( self: Self, querystring: str, - parameters: list[Any] | None = None, + parameters: Sequence[Any] | None = None, prepared: bool = True, ) -> QueryResult: """Execute the query. @@ -1011,6 +1051,26 @@ class ConnectionPool: # it will be dropped on Rust side. ``` """ + async def fetch( + self: Self, + querystring: str, + parameters: Sequence[Any] | None = None, + prepared: bool = True, + ) -> QueryResult: + """Fetch the result from database. + + It's the same as `execute` method, we made it because people are used + to `fetch` method name. + + Querystring can contain `$` parameters + for converting them in the driver side. + + ### Parameters: + - `querystring`: querystring to execute. + - `parameters`: list of parameters to pass in the query. + - `prepared`: should the querystring be prepared before the request. + By default any querystring will be prepared. + """ async def connection(self: Self) -> Connection: """Create new connection. diff --git a/src/driver/connection.rs b/src/driver/connection.rs index 159e5da6..71e28515 100644 --- a/src/driver/connection.rs +++ b/src/driver/connection.rs @@ -152,6 +152,55 @@ impl Connection { Ok(()) } + /// Fetch result from the database. + /// + /// # Errors + /// + /// May return Err Result if + /// 1) Cannot convert incoming parameters + /// 2) Cannot prepare statement + /// 3) Cannot execute query + pub async fn fetch( + self_: pyo3::Py, + querystring: String, + parameters: Option>, + prepared: Option, + ) -> RustPSQLDriverPyResult { + let db_client = pyo3::Python::with_gil(|gil| self_.borrow(gil).db_client.clone()); + + let mut params: Vec = vec![]; + if let Some(parameters) = parameters { + params = convert_parameters(parameters)?; + } + let prepared = prepared.unwrap_or(true); + + let result = if prepared { + db_client + .query( + &db_client.prepare_cached(&querystring).await?, + ¶ms + .iter() + .map(|param| param as &QueryParameter) + .collect::>() + .into_boxed_slice(), + ) + .await? + } else { + db_client + .query( + &querystring, + ¶ms + .iter() + .map(|param| param as &QueryParameter) + .collect::>() + .into_boxed_slice(), + ) + .await? + }; + + Ok(PSQLDriverPyQueryResult::new(result)) + } + /// Fetch exaclty single row from query. /// /// Method doesn't acquire lock on any structure fields. diff --git a/src/driver/connection_pool.rs b/src/driver/connection_pool.rs index caa36e32..9ac2c0ac 100644 --- a/src/driver/connection_pool.rs +++ b/src/driver/connection_pool.rs @@ -230,6 +230,70 @@ impl ConnectionPool { Ok(PSQLDriverPyQueryResult::new(result)) } + /// Fetch result from the database. + /// + /// It's the same as `execute`, we made it for people who prefer + /// `fetch()`. + /// + /// Prepare statement and cache it, then execute. + /// + /// # Errors + /// May return Err Result if cannot retrieve new connection + /// or prepare statement or execute statement. + pub async fn fetch<'a>( + self_: pyo3::Py, + querystring: String, + prepared: Option, + parameters: Option>, + ) -> RustPSQLDriverPyResult { + let db_pool = pyo3::Python::with_gil(|gil| self_.borrow(gil).0.clone()); + + let db_pool_manager = tokio_runtime() + .spawn(async move { Ok::(db_pool.get().await?) }) + .await??; + let mut params: Vec = vec![]; + if let Some(parameters) = parameters { + params = convert_parameters(parameters)?; + } + let prepared = prepared.unwrap_or(true); + let result = if prepared { + tokio_runtime() + .spawn(async move { + Ok::, RustPSQLDriverError>( + db_pool_manager + .query( + &db_pool_manager.prepare_cached(&querystring).await?, + ¶ms + .iter() + .map(|param| param as &QueryParameter) + .collect::>() + .into_boxed_slice(), + ) + .await?, + ) + }) + .await?? + } else { + tokio_runtime() + .spawn(async move { + Ok::, RustPSQLDriverError>( + db_pool_manager + .query( + &querystring, + ¶ms + .iter() + .map(|param| param as &QueryParameter) + .collect::>() + .into_boxed_slice(), + ) + .await?, + ) + }) + .await?? + }; + Ok(PSQLDriverPyQueryResult::new(result)) + } + /// Return new single connection. /// /// # Errors diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index 8ed00cd7..4ca27ff0 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -221,6 +221,33 @@ impl Transaction { .psqlpy_query(querystring, parameters, prepared) .await } + + /// Fetch result from the database. + /// + /// It converts incoming parameters to rust readable + /// and then execute the query with them. + /// + /// # Errors + /// + /// May return Err Result if: + /// 1) Cannot convert python parameters + /// 2) Cannot execute querystring. + pub async fn fetch( + self_: Py, + querystring: String, + parameters: Option>, + prepared: Option, + ) -> RustPSQLDriverPyResult { + let (is_transaction_ready, db_client) = pyo3::Python::with_gil(|gil| { + let self_ = self_.borrow(gil); + (self_.check_is_transaction_ready(), self_.db_client.clone()) + }); + is_transaction_ready?; + db_client + .psqlpy_query(querystring, parameters, prepared) + .await + } + /// Fetch exaclty single row from query. /// /// Method doesn't acquire lock on any structure fields. diff --git a/src/value_converter.rs b/src/value_converter.rs index 59a3735f..fa7f6ff2 100644 --- a/src/value_converter.rs +++ b/src/value_converter.rs @@ -265,7 +265,8 @@ pub fn convert_parameters(parameters: Py) -> RustPSQLDriverPyResult>>(gil).map_err(|_| { RustPSQLDriverError::PyToRustValueConversionError( - "Cannot convert you parameters argument for an array in Rust, please use List/Set/Tuple".into(), + "Cannot convert you parameters argument into Rust type, please use List/Tuple" + .into(), ) })?; for parameter in params { From 63ce72380923bd3b618b5794da897826b1e59c4d Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Fri, 3 May 2024 17:29:22 +0200 Subject: [PATCH 2/2] Added fetch method, changed parameters annotations. Added tests Signed-off-by: chandr-andr (Kiselev Aleksandr) --- python/tests/test_connection.py | 15 +++++++++++++++ python/tests/test_connection_pool.py | 17 +++++++++++++++++ python/tests/test_transaction.py | 15 +++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/python/tests/test_connection.py b/python/tests/test_connection.py index 5dad00ec..f3a52dc6 100644 --- a/python/tests/test_connection.py +++ b/python/tests/test_connection.py @@ -26,6 +26,21 @@ async def test_connection_execute( assert len(conn_result.result()) == number_database_records +async def test_connection_fetch( + psql_pool: ConnectionPool, + table_name: str, + number_database_records: int, +) -> None: + """Test that single connection can fetch queries.""" + connection = await psql_pool.connection() + + conn_result = await connection.fetch( + querystring=f"SELECT * FROM {table_name}", + ) + assert isinstance(conn_result, QueryResult) + assert len(conn_result.result()) == number_database_records + + async def test_connection_connection( psql_pool: ConnectionPool, ) -> None: diff --git a/python/tests/test_connection_pool.py b/python/tests/test_connection_pool.py index e2962dbf..cb18c20e 100644 --- a/python/tests/test_connection_pool.py +++ b/python/tests/test_connection_pool.py @@ -49,6 +49,23 @@ async def test_pool_execute( assert len(inner_result) == number_database_records +async def test_pool_fetch( + psql_pool: ConnectionPool, + table_name: str, + number_database_records: int, +) -> None: + """Test that ConnectionPool can fetch queries.""" + select_result = await psql_pool.fetch( + f"SELECT * FROM {table_name}", + ) + + assert type(select_result) == QueryResult + + inner_result = select_result.result() + assert isinstance(inner_result, list) + assert len(inner_result) == number_database_records + + async def test_pool_connection( psql_pool: ConnectionPool, ) -> None: diff --git a/python/tests/test_transaction.py b/python/tests/test_transaction.py index a9b61c39..86c74763 100644 --- a/python/tests/test_transaction.py +++ b/python/tests/test_transaction.py @@ -205,6 +205,21 @@ async def test_transaction_cursor( assert isinstance(cursor, Cursor) +async def test_transaction_fetch( + psql_pool: ConnectionPool, + table_name: str, + number_database_records: int, +) -> None: + """Test that single connection can fetch queries.""" + connection = await psql_pool.connection() + + async with connection.transaction() as transaction: + conn_result = await transaction.fetch( + querystring=f"SELECT * FROM {table_name}", + ) + assert len(conn_result.result()) == number_database_records + + @pytest.mark.parametrize( ("insert_values"), [