diff --git a/Cargo.toml b/Cargo.toml index 68160a6a..e6dc160d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,14 @@ crate-type = ["cdylib"] [dependencies] deadpool-postgres = { git = "https://github.com/chandr-andr/deadpool.git", branch = "psqlpy" } -pyo3 = { version = "0.23.4", features = ["chrono", "experimental-async", "rust_decimal", "py-clone", "macros", "multiple-pymethods"] } +pyo3 = { version = "0.23.4", features = [ + "chrono", + "experimental-async", + "rust_decimal", + "py-clone", + "macros", + "multiple-pymethods", +] } pyo3-async-runtimes = { git = "https://github.com/chandr-andr/pyo3-async-runtimes.git", branch = "psqlpy", features = [ "tokio-runtime", ] } diff --git a/docs/.vuepress/sidebar.ts b/docs/.vuepress/sidebar.ts index 9fc80456..7a03377d 100644 --- a/docs/.vuepress/sidebar.ts +++ b/docs/.vuepress/sidebar.ts @@ -46,6 +46,15 @@ export default sidebar({ "advanced_type_usage", ] }, + { + text: "Row Factories Usage", + prefix: "row_factories/", + collapsible: true, + children: [ + "row_factories", + "predefined_row_factories", + ] + }, { text: "Frameworks Usage", prefix: "frameworks/", @@ -58,15 +67,6 @@ export default sidebar({ "robyn", ] }, - { - text: "Row Factories Usage", - prefix: "row_factories/", - collapsible: true, - children: [ - "row_factories", - "predefined_row_factories", - ] - }, ], }, { diff --git a/docs/README.md b/docs/README.md index d2dece04..00c1b642 100644 --- a/docs/README.md +++ b/docs/README.md @@ -28,10 +28,9 @@ highlights: details: PSQLPy is under active development. --- ## What is PSQLPy -`PSQLPy` is a new Python driver for `PostgreSQL` fully written in Rust. It was inspired by `Psycopg3` and `AsyncPG`. +`PSQLPy` is a Python driver for `PostgreSQL` fully written in Rust. It was inspired by `Psycopg3` and `AsyncPG`. This project has two main goals: -Make a interaction with the database as fast as possible and now `PSQLPy` shows itself to be times faster than the above drivers. -Don't make useless abstractions and make it like a mirror to `PostgreSQL`. +We found that communication with the database can be faster and safer, so we tried to implement a new PostgreSQL driver in Rust for Python. It has all necessary components to create high-load and fault tolerance applications. @@ -59,4 +58,5 @@ pip install git+https://github.com/psqlpy-python/psqlpy ## Join community! You can get support from the creators and users of `PSQLPy` in some social media: -- [Telegram](https://t.me/+f3Y8mYKgXxhmYThi) \ No newline at end of file +- [Telegram](https://t.me/+f3Y8mYKgXxhmYThi) +- [Discord](https://discord.gg/ugNhzmhZ) diff --git a/docs/components/connection.md b/docs/components/connection.md index bd73d933..180777d2 100644 --- a/docs/components/connection.md +++ b/docs/components/connection.md @@ -24,11 +24,10 @@ async def main() -> None: ```python from psqlpy import connect -db_connection: Final = connect( - dsn="postgres://postgres:postgres@localhost:5432/postgres", -) - async def main() -> None: + db_connection: Final = await connect( + dsn="postgres://postgres:postgres@localhost:5432/postgres", + ) await db_connection.execute(...) ``` diff --git a/docs/components/cursor.md b/docs/components/cursor.md index ca931a58..f8c2fbd3 100644 --- a/docs/components/cursor.md +++ b/docs/components/cursor.md @@ -3,44 +3,93 @@ title: Cursor --- `Cursor` objects represents server-side `Cursor` in the `PostgreSQL`. [PostgreSQL docs](https://www.postgresql.org/docs/current/plpgsql-cursors.html). +::: important +Cursor always lives inside a transaction. If you don't begin transaction explicitly, it will be opened anyway. +::: ## Cursor Parameters - `querystring`: specify query for cursor. - `parameters`: parameters for the querystring. Default `None` -- `fetch_number`: default fetch number. It is used in `fetch()` method and in async iterator. Default 10 -- `scroll`: is cursor scrollable or not. Default as in `PostgreSQL`. +- `fetch_number`: default fetch number. It is used in `fetch()` method and in async iterator. -## Cursor as async iterator +## Usage -The most common situation is using `Cursor` as async iterator. +Cursor can be used in different ways. +::: tabs +@tab Pre-Initialization ```python from psqlpy import ConnectionPool, QueryResult +async def main() -> None: + db_pool = ConnectionPool() + connection = await db_pool.connection() + + cursor = connection.cursor( + querystring="SELECT * FROM users WHERE id > $1", + parameters=[100], + fetch_number=10, + ) + await cursor.start() +``` + +@tab Post-Initialization +```python +from psqlpy import ConnectionPool, QueryResult async def main() -> None: db_pool = ConnectionPool() + connection = await db_pool.connection() + + cursor = connection.cursor() + await cursor.execute( + querystring="SELECT * FROM users WHERE id > $1", + parameters=[100], + ) + result: QueryResult = await cursor.fetchone() +``` +@tab Async Context Manager +```python +from psqlpy import ConnectionPool, QueryResult +async def main() -> None: + db_pool = ConnectionPool() connection = await db_pool.connection() - transaction = await connection.transaction() - - # Here we fetch 5 results in each iteration. - async with cursor in transaction.cursor( - querystring="SELECT * FROM users WHERE username = $1", - parameters=["Some_Username"], - fetch_number=5, - ): - async for fetched_result in cursor: - dict_result: List[Dict[Any, Any]] = fetched_result.result() - ... # do something with this result. + + async with connection.cursor( + querystring="SELECT * FROM users WHERE id > $1", + parameters=[100], + array_size=10, + ) as cursor: + result: QueryResult = await cursor.fetchone() ``` -## Cursor methods +@tab Async Iterator +```python +from psqlpy import ConnectionPool, QueryResult -There are a lot of methods to work with cursor. +async def main() -> None: + db_pool = ConnectionPool() + connection = await db_pool.connection() + + cursor = connection.cursor( + querystring="SELECT * FROM users WHERE id > $1", + parameters=[100], + fetch_number=10, + ) + await cursor.start() + async for result in cursor: + print(result) +``` +::: + +## Cursor attributes +- `array_size`: get and set attribute. Used in async iterator and `fetch_many` method. + +## Cursor methods ### Start Declare (create) cursor. @@ -58,14 +107,53 @@ async def main() -> None: await cursor.close() ``` -### Fetch +### Execute -You can fetch next `N` records from the cursor. -It's possible to specify `N` fetch record with parameter `fetch_number`, otherwise will be used `fetch_number` from the `Cursor` initialization. +Initialize cursor and make it ready for fetching. + +::: important +If you initialized cursor with `start` method or via context manager, you don't have to use this method. +::: + +#### Parameters: +- `querystring`: specify query for cursor. +- `parameters`: parameters for the querystring. Default `None` ```python async def main() -> None: - result: QueryResult = await cursor.fetch( - fetch_number=100, + await cursor.execute( + querystring="SELECT * FROM users WHERE id > $1", + parameters=[100], ) + result: QueryResult = await cursor.fetchone() +``` + +### Fetchone + +Fetch one result from the cursor. + +```python +async def main() -> None: + result: QueryResult = await cursor.fetchone() +``` + +### Fetchmany + +Fetch N results from the cursor. Default is `array_size`. + +#### Parameters: +- `size`: number of records to fetch. + +```python +async def main() -> None: + result: QueryResult = await cursor.fetchmany(size=10) +``` + +### Fetchall + +Fetch all results from the cursor. + +```python +async def main() -> None: + result: QueryResult = await cursor.fetchall() ``` diff --git a/docs/components/transaction.md b/docs/components/transaction.md index 1bed17e4..8f18a097 100644 --- a/docs/components/transaction.md +++ b/docs/components/transaction.md @@ -407,8 +407,6 @@ async def main() -> None: - `querystring`: Statement string. - `parameters`: List of list of parameters for the statement string. - `fetch_number`: rewrite default fetch_number. Default is 10. -- `scroll`: make cursor scrollable or not. Default is like in `PostgreSQL`. -- `prepared`: prepare querystring or not. From `Transaction` you can create new `Cursor` object which represents cursor in the `PostgreSQL`. [PostgreSQL Docs](https://www.postgresql.org/docs/current/plpgsql-cursors.html) diff --git a/docs/introduction/introduction.md b/docs/introduction/introduction.md index 545eb700..27f142d9 100644 --- a/docs/introduction/introduction.md +++ b/docs/introduction/introduction.md @@ -5,15 +5,14 @@ title: What is PSQLPy? `PSQLPy` is a new Python driver for PostgreSQL fully written in Rust. It was inspired by `Psycopg3` and `AsyncPG`. With `PSQLPy` you can: -- Make an interaction with the PostgeSQL in your application much faster (2-3 times). +- Make an interaction with the PostgeSQL in your application faster. - Be sure that there won't be any unexpected errors. -- Don't usually go to the documentation to search every question - we have awesome docstrings for every component. +- Don't usually go to the documentation to search every question - we have docstrings for every component. - Use `MyPy` (or any other Python type checker) with confidence that exactly the types specified in the typing will be returned. - Concentrate on writing your code, not understanding new abstractions in this library, we only have classes which represents PostgreSQL object (transaction, cursor, etc). ::: info -It is extremely important to understand that the library will provide a noticeable acceleration in working with the database only if your queries are optimized. -Otherwise, there will be acceleration, but not so significant +The library will provide a noticeable acceleration in working with the database only if your queries are optimized. ::: ## Important notes @@ -22,4 +21,5 @@ But in some situations this behavior can break you application. As an example, i ## Join community! You can get support from the creators of `PSQLPy` in some social media: -- [Telegram](https://t.me/+f3Y8mYKgXxhmYThi) \ No newline at end of file +- [Telegram](https://t.me/+f3Y8mYKgXxhmYThi) +- [Discord](https://discord.gg/ugNhzmhZ) diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py index fdd53ba7..bd423a9b 100644 --- a/python/tests/test_cursor.py +++ b/python/tests/test_cursor.py @@ -65,6 +65,22 @@ async def test_cursor_as_async_context_manager( assert len(results.result()) == number_database_records +async def test_cursor_as_async_iterator( + psql_pool: ConnectionPool, + table_name: str, + number_database_records: int, +) -> None: + connection = await psql_pool.connection() + all_results = [] + async with connection.cursor( + querystring=f"SELECT * FROM {table_name}", + ) as cursor: + async for results in cursor: + all_results.extend(results.result()) + + assert len(all_results) == number_database_records + + async def test_cursor_send_underlying_connection_to_pool( psql_pool: ConnectionPool, table_name: str, diff --git a/src/connection/traits.rs b/src/connection/traits.rs index 5d8d49ae..ccf8f467 100644 --- a/src/connection/traits.rs +++ b/src/connection/traits.rs @@ -101,39 +101,3 @@ pub trait CloseTransaction: StartTransaction { fn rollback(&mut self) -> impl std::future::Future>; } - -// pub trait Cursor { -// fn build_cursor_start_qs( -// &self, -// cursor_name: &str, -// scroll: &Option, -// querystring: &str, -// ) -> String { -// let mut cursor_init_query = format!("DECLARE {cursor_name}"); -// if let Some(scroll) = scroll { -// if *scroll { -// cursor_init_query.push_str(" SCROLL"); -// } else { -// cursor_init_query.push_str(" NO SCROLL"); -// } -// } - -// cursor_init_query.push_str(format!(" CURSOR FOR {querystring}").as_str()); - -// cursor_init_query -// } - -// fn start_cursor( -// &mut self, -// cursor_name: &str, -// scroll: &Option, -// querystring: String, -// prepared: &Option, -// parameters: Option>, -// ) -> impl std::future::Future>; - -// fn close_cursor( -// &mut self, -// cursor_name: &str, -// ) -> impl std::future::Future>; -// } diff --git a/src/driver/common.rs b/src/driver/common.rs index bab84049..d2f6aec8 100644 --- a/src/driver/common.rs +++ b/src/driver/common.rs @@ -132,18 +132,18 @@ macro_rules! impl_cursor_method { ($name:ident) => { #[pymethods] impl $name { - #[pyo3(signature = (querystring=None, parameters=None, fetch_number=None))] + #[pyo3(signature = (querystring=None, parameters=None, array_size=None))] pub fn cursor( &self, querystring: Option, parameters: Option>, - fetch_number: Option, + array_size: Option, ) -> PSQLPyResult { Ok(Cursor::new( self.conn.clone(), querystring, parameters, - fetch_number, + array_size, self.pg_config.clone(), None, )) diff --git a/src/driver/connection.rs b/src/driver/connection.rs index e9972e58..0d562b00 100644 --- a/src/driver/connection.rs +++ b/src/driver/connection.rs @@ -1,5 +1,5 @@ use deadpool_postgres::Pool; -use pyo3::{ffi::PyObject, pyclass, pyfunction, pymethods, Py, PyAny, PyErr}; +use pyo3::{pyclass, pyfunction, pymethods, Py, PyAny, PyErr}; use std::sync::Arc; use tokio::sync::RwLock; use tokio_postgres::Config; diff --git a/src/driver/cursor.rs b/src/driver/cursor.rs index ab40526a..248bd08f 100644 --- a/src/driver/cursor.rs +++ b/src/driver/cursor.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use pyo3::{ - exceptions::PyStopAsyncIteration, pyclass, pymethods, types::PyNone, Py, PyAny, PyErr, - PyObject, Python, + exceptions::PyStopAsyncIteration, pyclass, pymethods, Py, PyAny, PyErr, PyObject, Python, }; use tokio::sync::RwLock; use tokio_postgres::{Config, Portal as tp_Portal};