From 53b4e79b3447ede004a4d3e1149d9efc5adc9e4a Mon Sep 17 00:00:00 2001 From: reqww Date: Wed, 28 Feb 2024 12:17:34 +0400 Subject: [PATCH 01/30] transaction deferable --- src/driver/transaction.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index af4667e5..6b9826ca 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -6,6 +6,7 @@ use tokio_postgres::types::ToSql; use crate::{ common::rustengine_future, exceptions::rust_errors::{RustPSQLDriverError, RustPSQLDriverPyResult}, + extra_types::Integer, query_result::PSQLDriverPyQueryResult, value_converter::{convert_parameters, PythonDTO}, }; @@ -28,6 +29,7 @@ pub struct RustTransaction { isolation_level: Option, read_variant: Option, cursor_num: usize, + deferable: Option, } impl RustTransaction { @@ -38,6 +40,7 @@ impl RustTransaction { rollback_savepoint: Arc>>, isolation_level: Option, read_variant: Option, + deferable: Option, cursor_num: usize, ) -> Self { Self { @@ -48,6 +51,7 @@ impl RustTransaction { isolation_level, read_variant, cursor_num, + deferable, } } @@ -102,8 +106,9 @@ impl RustTransaction { Ok(PSQLDriverPyQueryResult::new(result)) } - /// Start transaction with isolation level if specified - /// + /// Start transaction + /// Set up isolation level if specified + /// Set up deferable if specified /// # Errors /// May return Err Result if cannot execute querystring. pub async fn start_transaction(&self) -> RustPSQLDriverPyResult<()> { @@ -118,6 +123,10 @@ impl RustTransaction { querystring.push_str(format!(" {}", &read_var.to_str_option()).as_str()); } + if let Some(_) = self.deferable { + querystring.push_str("SET CONSTRAINTS ALL DEFERRED"); + } + let db_client_arc = self.db_client.clone(); let db_client_guard = db_client_arc.read().await; From 2b0f317eb98ba12fed1ec92b5ef3c42c25f8711b Mon Sep 17 00:00:00 2001 From: reqww Date: Wed, 28 Feb 2024 12:18:19 +0400 Subject: [PATCH 02/30] transaction deferable --- src/driver/transaction.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index 6b9826ca..758fde27 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -6,7 +6,6 @@ use tokio_postgres::types::ToSql; use crate::{ common::rustengine_future, exceptions::rust_errors::{RustPSQLDriverError, RustPSQLDriverPyResult}, - extra_types::Integer, query_result::PSQLDriverPyQueryResult, value_converter::{convert_parameters, PythonDTO}, }; From 955934666204a6a47151f7aabc95e957fa4cd3f9 Mon Sep 17 00:00:00 2001 From: reqww Date: Wed, 28 Feb 2024 12:21:06 +0400 Subject: [PATCH 03/30] transaction deferable --- src/driver/connection.rs | 2 ++ src/driver/transaction.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/driver/connection.rs b/src/driver/connection.rs index 9172b31c..34a01090 100644 --- a/src/driver/connection.rs +++ b/src/driver/connection.rs @@ -66,6 +66,7 @@ impl Connection { &self, isolation_level: Option, read_variant: Option, + deferable: Option, ) -> Transaction { let inner_transaction = RustTransaction::new( self.db_client.clone(), @@ -74,6 +75,7 @@ impl Connection { Arc::new(tokio::sync::RwLock::new(HashSet::new())), isolation_level, read_variant, + deferable, Default::default(), ); diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index 758fde27..15a86d4f 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -27,8 +27,8 @@ pub struct RustTransaction { isolation_level: Option, read_variant: Option, - cursor_num: usize, deferable: Option, + cursor_num: usize, } impl RustTransaction { From b5c9c4a048bff1ff1bd410efeb0bf8e2ca4f58f9 Mon Sep 17 00:00:00 2001 From: reqww Date: Thu, 29 Feb 2024 15:27:41 +0400 Subject: [PATCH 04/30] transaction deferable --- python/psqlpy/_internal/__init__.pyi | 13 +++++++------ src/driver/transaction.rs | 3 +-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index 4180133a..e74fad2b 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -1,12 +1,12 @@ from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Optional from typing_extensions import Self class QueryResult: """Result.""" - def result(self: Self) -> List[Dict[Any, Any]]: + def result(self: Self) -> list[dict[Any, Any]]: """Return result from database as a list of dicts.""" class IsolationLevel(Enum): @@ -170,7 +170,7 @@ class Transaction: async def execute( self: Self, querystring: str, - parameters: List[Any] | None = None, + parameters: list[Any] | None = None, ) -> QueryResult: """Execute the query. @@ -326,7 +326,7 @@ class Transaction: async def cursor( self: Self, querystring: str, - parameters: List[Any] | None = None, + parameters: list[Any] | None = None, fetch_number: int | None = None, scroll: bool | None = None, ) -> Cursor: @@ -378,7 +378,7 @@ class Connection: async def execute( self: Self, querystring: str, - parameters: List[Any] | None = None, + parameters: list[Any] | None = None, ) -> QueryResult: """Execute the query. @@ -414,6 +414,7 @@ class Connection: def transaction( self, isolation_level: IsolationLevel | None = None, + deferable: bool | None = None, read_variant: ReadVariant | None = None, ) -> Transaction: """Create new transaction. @@ -466,7 +467,7 @@ class PSQLPool: async def execute( self: Self, querystring: str, - parameters: List[Any] | None = None, + parameters: list[Any] | None = None, ) -> QueryResult: """Execute the query. diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index 15a86d4f..c9e4d829 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -121,8 +121,7 @@ impl RustTransaction { if let Some(read_var) = self.read_variant { querystring.push_str(format!(" {}", &read_var.to_str_option()).as_str()); } - - if let Some(_) = self.deferable { + if self.deferable.is_some() { querystring.push_str("SET CONSTRAINTS ALL DEFERRED"); } From 10ea9c66405c44889b5f31b198718d9a160bc607 Mon Sep 17 00:00:00 2001 From: reqww Date: Thu, 29 Feb 2024 15:28:09 +0400 Subject: [PATCH 05/30] transaction deferable --- python/psqlpy/_internal/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index e74fad2b..46882eb3 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -414,8 +414,8 @@ class Connection: def transaction( self, isolation_level: IsolationLevel | None = None, - deferable: bool | None = None, read_variant: ReadVariant | None = None, + deferable: bool | None = None, ) -> Transaction: """Create new transaction. From 2fc6959d7bad7c6728f99937759db26a4c907b2e Mon Sep 17 00:00:00 2001 From: reqww Date: Sun, 3 Mar 2024 10:25:30 +0400 Subject: [PATCH 06/30] readme --- README.md | 44 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b43344e9..a7242938 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,35 @@ Driver for PostgreSQL written fully in Rust and exposed to Python. The project is under active development and _**we cannot confirm that it's ready for production**_. Anyway, We will be grateful for the bugs found and open issues. Stay tuned. -*Normal documentation is in development.* +_Normal documentation is in development._ ## Installation You can install package with `pip` or `poetry`. poetry: + ```bash > poetry add psqlpy ``` + pip: + ```bash > pip install psqlpy ``` Or you can build it by yourself. To do it, install stable rust and [maturin](/~https://github.com/PyO3/maturin). + ``` > maturin develop --release ``` ## Usage + Usage is as easy as possible. Create new instance of PSQLPool, startup it and start querying. + ```python from typing import Any @@ -52,9 +58,11 @@ async def main() -> None: # rust does it instead. ``` + Please take into account that each new execute gets new connection from connection pool. ## Query parameters + You can pass parameters into queries. Parameters can be passed in any `execute` method as the second parameter, it must be a list. Any placeholder must be marked with `$< num>`. @@ -67,7 +75,9 @@ Any placeholder must be marked with `$< num>`. ``` ## Connection + You can work with connection instead of DatabasePool. + ```python from typing import Any @@ -98,17 +108,22 @@ async def main() -> None: ``` ## Transactions + Of course it's possible to use transactions with this driver. It's as easy as possible and sometimes it copies common functionality from PsycoPG and AsyncPG. ### Transaction parameters + In process of transaction creation it is possible to specify some arguments to configure transaction. - `isolation_level`: level of the isolation. By default - `None`. - `read_variant`: read option. By default - `None`. +- `deferable`: deferable option. By default - `None`. ### You can use transactions as async context managers + By default async context manager only begins and commits transaction automatically. + ```python from typing import Any @@ -132,6 +147,7 @@ async def main() -> None: ``` ### Or you can control transaction fully on your own. + ```python from typing import Any @@ -163,9 +179,11 @@ async def main() -> None: ``` ### Transactions can be rolled back + You must understand that rollback can be executed only once per transaction. After it's execution transaction state changes to `done`. If you want to use `ROLLBACK TO SAVEPOINT`, see below. + ```python from typing import Any @@ -191,6 +209,7 @@ async def main() -> None: ``` ### Transaction ROLLBACK TO SAVEPOINT + You can rollback your transaction to the specified savepoint, but before it you must create it. ```python @@ -224,6 +243,7 @@ async def main() -> None: ``` ### Transaction RELEASE SAVEPOINT + It's possible to release savepoint ```python @@ -252,12 +272,15 @@ async def main() -> None: ``` ## Cursors + Library supports PostgreSQL cursors. Cursors can be created only in transaction. In addition, cursor supports async iteration. ### Cursor parameters + In process of cursor creation you can specify some configuration parameters. + - `querystring`: query for the cursor. Required. - `parameters`: parameters for the query. Not Required. - `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. @@ -301,7 +324,9 @@ async def main() -> None: ``` ### Cursor operations + Available cursor operations: + - FETCH count - `cursor.fetch(fetch_number=)` - FETCH NEXT - `cursor.fetch_next()` - FETCH PRIOR - `cursor.fetch_prior()` @@ -314,15 +339,16 @@ Available cursor operations: - FETCH BACKWARD ALL - `cursor.fetch_backward_all()` ## Extra Types + Sometimes it's impossible to identify which type user tries to pass as a argument. But Rust is a strongly typed programming language so we have to help. -| Extra Type in Python | Type in PostgreSQL | Type in Rust | -| ------------- | ------------- | ------------- -| SmallInt | SmallInt | i16 | -| Integer | Integer | i32 | -| BigInt | BigInt | i64 | -| PyUUID | UUID | Uuid | -| PyJSON | JSON, JSONB | Value | +| Extra Type in Python | Type in PostgreSQL | Type in Rust | +| -------------------- | ------------------ | ------------ | +| SmallInt | SmallInt | i16 | +| Integer | Integer | i32 | +| BigInt | BigInt | i64 | +| PyUUID | UUID | Uuid | +| PyJSON | JSON, JSONB | Value | ```python from typing import Any @@ -366,4 +392,4 @@ async def main() -> None: # You don't need to close Database Pool by yourself, # rust does it instead. -``` \ No newline at end of file +``` From 3ad9f35c6ebd395a6b0eee220e37a0a7782ac5bd Mon Sep 17 00:00:00 2001 From: reqww Date: Sun, 3 Mar 2024 10:29:42 +0400 Subject: [PATCH 07/30] readme --- src/driver/transaction.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index c9e4d829..10b72804 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -49,8 +49,8 @@ impl RustTransaction { rollback_savepoint, isolation_level, read_variant, - cursor_num, deferable, + cursor_num, } } From ad6f701e3f3a27147f248c2d461f8a27cbfa9311 Mon Sep 17 00:00:00 2001 From: reqww Date: Sun, 3 Mar 2024 10:33:12 +0400 Subject: [PATCH 08/30] fix clippy --- src/driver/transaction.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index 10b72804..f832026d 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -32,6 +32,7 @@ pub struct RustTransaction { } impl RustTransaction { + #[allow(clippy::too_many_arguments)] pub fn new( db_client: Arc>, is_started: Arc>, From f4a7112fc1fb151e7fc6f2a220294949d183bc40 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Wed, 21 Feb 2024 21:31:06 +0100 Subject: [PATCH 09/30] Added test actions --- .github/workflows/{CI.yml => release.yml} | 5 -- .github/workflows/test.yaml | 95 +++++++++++++++++++++++ python/psqlpy/__init__.py | 10 ++- python/tests/conftest.py | 88 ++++++++++++++++++++- python/tests/test_connection_pool.py | 30 +++++++ tox.ini | 28 +++++++ 6 files changed, 249 insertions(+), 7 deletions(-) rename .github/workflows/{CI.yml => release.yml} (97%) create mode 100644 .github/workflows/test.yaml create mode 100644 python/tests/test_connection_pool.py create mode 100644 tox.ini diff --git a/.github/workflows/CI.yml b/.github/workflows/release.yml similarity index 97% rename from .github/workflows/CI.yml rename to .github/workflows/release.yml index 06c41c59..f3f0e5ff 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/release.yml @@ -2,13 +2,8 @@ name: CI on: push: - branches: - - main - - master tags: - '*' - pull_request: - workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..85d559a2 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,95 @@ +name: 'Testing package' + +on: + pull_request: + +jobs: + py-lint: + strategy: + matrix: + cmd: + - black + - isort + - ruff + - mypy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.12" + - name: Run lint check + uses: pre-commit/action@v3.0.0 + with: + extra_args: -a ${{ matrix.cmd }} + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + override: true + - name: Check code format + run: cargo fmt -- --check --config use_try_shorthand=true,imports_granularity=Crate + + clippy: + permissions: + checks: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: -p psqlpy --all-features -- -W clippy::all -W clippy::pedantic -D warnings + pytest: + name: ${{matrix.job.os}}-${{matrix.py_version}} + services: + scylla: + image: scylladb/scylla:5.2 + options: >- + --health-cmd="cqlsh -e 'select * from system.local' " + --health-interval=5s + --health-timeout=5s + --health-retries=60 + ports: + - 9042:9042 + strategy: + matrix: + py_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + job: + - os: ubuntu-latest + ssl_cmd: sudo apt-get update && sudo apt-get install libssl-dev openssl + runs-on: ${{matrix.job.os}} + steps: + - uses: actions/checkout@v1 + - uses: ikalnytskyi/action-setup-postgres@v4 + with: + username: postgres + password: postgres + database: psqlpy_test + id: postgres + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - name: Setup OpenSSL + run: ${{matrix.job.ssl_cmd}} + - name: Setup python for test ${{ matrix.py_version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.py_version }} + - name: Install tox + run: pip install "tox-gh>=1.2,<2" + - name: Run pytest + run: tox -v + diff --git a/python/psqlpy/__init__.py b/python/psqlpy/__init__.py index dd6b7af8..77f26fb2 100644 --- a/python/psqlpy/__init__.py +++ b/python/psqlpy/__init__.py @@ -1,4 +1,11 @@ -from ._internal import IsolationLevel, PSQLPool, QueryResult, ReadVariant, Transaction +from ._internal import ( + Connection, + IsolationLevel, + PSQLPool, + QueryResult, + ReadVariant, + Transaction, +) __all__ = [ "PSQLPool", @@ -6,4 +13,5 @@ "Transaction", "IsolationLevel", "ReadVariant", + "Connection", ] diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 038776af..8d81582d 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,7 +1,13 @@ +import os +import random +from typing import AsyncGenerator + import pytest +from psqlpy import PSQLPool + -@pytest.fixture(scope="session") +@pytest.fixture() def anyio_backend() -> str: """ Anyio backend. @@ -10,3 +16,83 @@ def anyio_backend() -> str: :return: backend name. """ return "asyncio" + + +def random_string(length: int = 10) -> str: + return "".join(random.choice("AbCdEfG") for _ in range(length)) + + +@pytest.fixture() +def postgres_host() -> str: + return os.environ.get("POSTGRES_HOST", "localhost") + + +@pytest.fixture() +def postgres_user() -> str: + return os.environ.get("POSTGRES_USER", "postgres") + + +@pytest.fixture() +def postgres_password() -> str: + return os.environ.get("POSTGRES_PASSWORD", "postgres") + + +@pytest.fixture() +def postgres_port() -> int: + return int(os.environ.get("POSTGRES_PORT", 5432)) + + +@pytest.fixture() +def postgres_dbname() -> str: + return os.environ.get("POSTGRES_DBNAME", "psqlpy_test") + + +@pytest.fixture() +def table_name() -> str: + return random_string() + + +@pytest.fixture() +def number_database_records() -> int: + return random.randint(10, 35) + + +@pytest.fixture() +async def psql_pool( + postgres_host: str, + postgres_user: str, + postgres_password: str, + postgres_port: int, + postgres_dbname: str, +) -> AsyncGenerator[PSQLPool, None]: + pg_pool = PSQLPool( + username=postgres_user, + password=postgres_password, + host=postgres_host, + port=postgres_port, + db_name=postgres_dbname, + ) + await pg_pool.startup() + yield pg_pool + + +@pytest.fixture(autouse=True) +async def create_deafult_data_for_tests( + psql_pool: PSQLPool, + table_name: str, + number_database_records: int, +) -> AsyncGenerator[None, None]: + await psql_pool.execute( + f"CREATE TABLE {table_name} (id SERIAL, name VARCHAR(255))", + ) + + for table_id in range(1, number_database_records + 1): + new_name = random_string() + await psql_pool.execute( + querystring=f"INSERT INTO {table_name} VALUES ($1, $2)", + parameters=[table_id, new_name], + ) + yield + await psql_pool.execute( + f"DROP TABLE {table_name}", + ) diff --git a/python/tests/test_connection_pool.py b/python/tests/test_connection_pool.py new file mode 100644 index 00000000..729b7ef2 --- /dev/null +++ b/python/tests/test_connection_pool.py @@ -0,0 +1,30 @@ +import pytest + +from psqlpy import Connection, PSQLPool, QueryResult + + +@pytest.mark.anyio +async def test_pool_execute( + psql_pool: PSQLPool, + table_name: str, + number_database_records: int, +) -> None: + """Test that PSQLPool can execute queries.""" + select_result = await psql_pool.execute( + 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 + + +@pytest.mark.anyio +async def test_pool_connection( + psql_pool: PSQLPool, +) -> None: + """Test that PSQLPool can return single connection from the pool.""" + connection = await psql_pool.connection() + assert isinstance(connection, Connection) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..1bed175f --- /dev/null +++ b/tox.ini @@ -0,0 +1,28 @@ +[tox] +isolated_build = true +env_list = + py312 + py311 + py310 + py39 + py38 + +[gh] +python = + 3.12 = py312 + 3.11 = py311 + 3.10 = py310 + 3.9 = py39 + 3.8 = py38 + +[testenv] +skip_install = true +deps = + pytest>=7,<8 + anyio>=3,<4 + maturin>=1,<2 +allowlist_externals = maturin +commands_pre = + maturin develop +commands = + pytest -vv From cc7a8e90e4ed8d365c520d1b91a4cb2fc625e211 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Wed, 21 Feb 2024 21:35:12 +0100 Subject: [PATCH 10/30] Removed unnecessary code from actions --- .github/workflows/test.yaml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 85d559a2..917b2aa6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -52,16 +52,6 @@ jobs: args: -p psqlpy --all-features -- -W clippy::all -W clippy::pedantic -D warnings pytest: name: ${{matrix.job.os}}-${{matrix.py_version}} - services: - scylla: - image: scylladb/scylla:5.2 - options: >- - --health-cmd="cqlsh -e 'select * from system.local' " - --health-interval=5s - --health-timeout=5s - --health-retries=60 - ports: - - 9042:9042 strategy: matrix: py_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] From e140dffc5c2be98226836591777e5b822c27a282 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Wed, 21 Feb 2024 23:44:25 +0100 Subject: [PATCH 11/30] Set line length to 79 --- pyproject.toml | 2 +- python/psqlpy/_internal/__init__.pyi | 3 ++- python/tests/test_connection.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 python/tests/test_connection.py diff --git a/pyproject.toml b/pyproject.toml index 87995d40..b9da62a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,7 @@ ignore = [ ] exclude = [".venv/"] mccabe = { max-complexity = 10 } -line-length = 88 +line-length = 79 [tool.ruff.per-file-ignores] "python/psqlpy/*" = ["PYI021"] diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index 46882eb3..28a0647e 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -493,7 +493,8 @@ class PSQLPool: [100], ) dict_result: List[Dict[Any, Any]] = query_result.result() - # you don't need to close the pool, it will be dropped on Rust side. + # you don't need to close the pool, + # it will be dropped on Rust side. ``` """ async def connection(self: Self) -> Connection: diff --git a/python/tests/test_connection.py b/python/tests/test_connection.py new file mode 100644 index 00000000..068ad25e --- /dev/null +++ b/python/tests/test_connection.py @@ -0,0 +1,19 @@ +import pytest + +from psqlpy import PSQLPool, QueryResult + + +@pytest.mark.anyio +async def test_connection_execute( + psql_pool: PSQLPool, + table_name: str, + number_database_records: int, +) -> None: + """Test that single connection can execute queries.""" + connection = await psql_pool.connection() + + conn_result = await connection.execute( + querystring=f"SELECT * FROM {table_name}", + ) + assert isinstance(conn_result, QueryResult) + assert len(conn_result.result()) == number_database_records From 6d5e1c9cb6ffee3482941310d9d089f53178198d Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Fri, 23 Feb 2024 04:27:45 +0400 Subject: [PATCH 12/30] Added more tests --- python/tests/test_connection.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/python/tests/test_connection.py b/python/tests/test_connection.py index 068ad25e..01084da8 100644 --- a/python/tests/test_connection.py +++ b/python/tests/test_connection.py @@ -1,6 +1,6 @@ import pytest -from psqlpy import PSQLPool, QueryResult +from psqlpy import PSQLPool, QueryResult, Transaction @pytest.mark.anyio @@ -17,3 +17,14 @@ async def test_connection_execute( ) assert isinstance(conn_result, QueryResult) assert len(conn_result.result()) == number_database_records + + +@pytest.mark.anyio +async def test_connection_transaction( + psql_pool: PSQLPool, +) -> None: + """Test that connection can create transactions.""" + connection = await psql_pool.connection() + transaction = connection.transaction() + + assert isinstance(transaction, Transaction) From 675ae9d3a22d7e66bc10ef43b7b32072b2e73fb2 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Sun, 25 Feb 2024 19:08:20 +0400 Subject: [PATCH 13/30] Continue adding tests --- python/psqlpy/exceptions.py | 1 + python/tests/test_transaction.py | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 python/tests/test_transaction.py diff --git a/python/psqlpy/exceptions.py b/python/psqlpy/exceptions.py index 786e57dd..74c6a355 100644 --- a/python/psqlpy/exceptions.py +++ b/python/psqlpy/exceptions.py @@ -18,4 +18,5 @@ "DBPoolConfigurationError", "UUIDValueConvertError", "CursorError", + "DBTransactionError", ] diff --git a/python/tests/test_transaction.py b/python/tests/test_transaction.py new file mode 100644 index 00000000..94d4307f --- /dev/null +++ b/python/tests/test_transaction.py @@ -0,0 +1,95 @@ +import pytest + +from psqlpy import PSQLPool +from psqlpy.exceptions import DBTransactionError + + +@pytest.mark.anyio +async def test_transaction_begin( + psql_pool: PSQLPool, + table_name: str, + number_database_records: int, +) -> None: + """Test that transaction must be started with `begin()` method.""" + connection = await psql_pool.connection() + transaction = connection.transaction() + + with pytest.raises(expected_exception=DBTransactionError): + await transaction.execute( + f"SELECT * FROM {table_name}", + ) + + await transaction.begin() + + result = await transaction.execute( + f"SELECT * FROM {table_name}", + ) + + assert len(result.result()) == number_database_records + + +@pytest.mark.anyio +async def test_transaction_commit( + psql_pool: PSQLPool, + table_name: str, +) -> None: + """Test that transaction commit command.""" + connection = await psql_pool.connection() + transaction = connection.transaction() + await transaction.begin() + + test_name: str = "test_name" + await transaction.execute( + f"INSERT INTO {table_name} VALUES ($1, $2)", + parameters=[100, test_name], + ) + + # Make request from other connection, it mustn't know + # about new INSERT data before commit. + result = await psql_pool.execute( + f"SELECT * FROM {table_name} WHERE name = $1", + parameters=[test_name], + ) + assert not result.result() + + await transaction.commit() + + result = await psql_pool.execute( + f"SELECT * FROM {table_name} WHERE name = $1", + parameters=[test_name], + ) + + assert len(result.result()) + + +@pytest.mark.anyio +async def test_transaction_savepoint( + psql_pool: PSQLPool, + table_name: str, +) -> None: + """Test that it's possible to rollback to savepoint.""" + connection = await psql_pool.connection() + transaction = connection.transaction() + await transaction.begin() + + test_name = "test_name" + savepoint_name = "sp1" + await transaction.savepoint(savepoint_name=savepoint_name) + await transaction.execute( + f"INSERT INTO {table_name} VALUES ($1, $2)", + parameters=[100, test_name], + ) + result = await transaction.execute( + f"SELECT * FROM {table_name} WHERE name = $1", + parameters=[test_name], + ) + assert result.result() + + await transaction.rollback_to(savepoint_name=savepoint_name) + result = await psql_pool.execute( + f"SELECT * FROM {table_name} WHERE name = $1", + parameters=[test_name], + ) + assert not len(result.result()) + + await transaction.commit() From f02cd98d069039b7d810d35826be6e3ec9ed6246 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Wed, 28 Feb 2024 12:23:18 +0400 Subject: [PATCH 14/30] Continue adding tests --- python/psqlpy/__init__.py | 2 + python/psqlpy/_internal/__init__.pyi | 8 +++ python/tests/test_transaction.py | 76 +++++++++++++++++++++++++++- src/driver/transaction.rs | 8 ++- 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/python/psqlpy/__init__.py b/python/psqlpy/__init__.py index 77f26fb2..737eeb49 100644 --- a/python/psqlpy/__init__.py +++ b/python/psqlpy/__init__.py @@ -1,5 +1,6 @@ from ._internal import ( Connection, + Cursor, IsolationLevel, PSQLPool, QueryResult, @@ -14,4 +15,5 @@ "IsolationLevel", "ReadVariant", "Connection", + "Cursor", ] diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index 28a0647e..3507129c 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -1,3 +1,4 @@ +import types from enum import Enum from typing import Any, Optional @@ -153,6 +154,13 @@ class Transaction: `.transaction()`. """ + async def __aenter__(self: Self) -> Self: ... + async def __aexit__( + self: Self, + exception_type: type[BaseException] | None, + exception: BaseException | None, + traceback: types.TracebackType | None, + ) -> None: ... async def begin(self: Self) -> None: """Start the transaction. diff --git a/python/tests/test_transaction.py b/python/tests/test_transaction.py index 94d4307f..9bc1890d 100644 --- a/python/tests/test_transaction.py +++ b/python/tests/test_transaction.py @@ -1,6 +1,6 @@ import pytest -from psqlpy import PSQLPool +from psqlpy import Cursor, PSQLPool from psqlpy.exceptions import DBTransactionError @@ -93,3 +93,77 @@ async def test_transaction_savepoint( assert not len(result.result()) await transaction.commit() + + +@pytest.mark.anyio +async def test_transaction_rollback( + psql_pool: PSQLPool, + table_name: str, +) -> None: + """Test that ROLLBACK works correctly.""" + connection = await psql_pool.connection() + transaction = connection.transaction() + await transaction.begin() + + test_name = "test_name" + await transaction.execute( + f"INSERT INTO {table_name} VALUES ($1, $2)", + parameters=[100, test_name], + ) + + result = await transaction.execute( + f"SELECT * FROM {table_name} WHERE name = $1", + parameters=[test_name], + ) + assert result.result() + + await transaction.rollback() + + with pytest.raises(expected_exception=DBTransactionError): + await transaction.execute( + f"SELECT * FROM {table_name} WHERE name = $1", + parameters=[test_name], + ) + + result_from_conn = await psql_pool.execute( + f"INSERT INTO {table_name} VALUES ($1, $2)", + parameters=[100, test_name], + ) + + assert not (result_from_conn.result()) + + +@pytest.mark.anyio +async def test_transaction_release_savepoint( + psql_pool: PSQLPool, +) -> None: + """Test that it is possible to acquire and release savepoint.""" + connection = await psql_pool.connection() + transaction = connection.transaction() + await transaction.begin() + + sp_name_1 = "sp1" + sp_name_2 = "sp2" + + await transaction.savepoint(sp_name_1) + + with pytest.raises(expected_exception=DBTransactionError): + await transaction.savepoint(sp_name_1) + + await transaction.savepoint(sp_name_2) + + await transaction.release_savepoint(sp_name_1) + await transaction.savepoint(sp_name_1) + + +@pytest.mark.anyio +async def test_transaction_cursor( + psql_pool: PSQLPool, + table_name: str, +) -> None: + """Test that transaction can create cursor.""" + connection = await psql_pool.connection() + async with connection.transaction() as transaction: + cursor = await transaction.cursor(f"SELECT * FROM {table_name}") + + assert isinstance(cursor, Cursor) diff --git a/src/driver/transaction.rs b/src/driver/transaction.rs index f832026d..ee402467 100644 --- a/src/driver/transaction.rs +++ b/src/driver/transaction.rs @@ -404,11 +404,9 @@ impl RustTransaction { )); }; - let rollback_savepoint_arc = self.rollback_savepoint.clone(); - let is_rollback_exists = { - let rollback_savepoint_guard = rollback_savepoint_arc.read().await; - rollback_savepoint_guard.contains(&rollback_name) - }; + let mut rollback_savepoint_guard = self.rollback_savepoint.write().await; + let is_rollback_exists = rollback_savepoint_guard.remove(&rollback_name); + if !is_rollback_exists { return Err(RustPSQLDriverError::DataBaseTransactionError( "Don't have rollback with this name".into(), From 67e5a4744826efed0dc87bba550909c782bf5ba7 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Wed, 28 Feb 2024 13:31:23 +0400 Subject: [PATCH 15/30] Continue adding tests --- python/tests/test_cursor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 python/tests/test_cursor.py diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py new file mode 100644 index 00000000..57491804 --- /dev/null +++ b/python/tests/test_cursor.py @@ -0,0 +1,16 @@ +import pytest + +from psqlpy import PSQLPool + + +@pytest.mark.anyio +async def test_cursor_fetch( + psql_pool: PSQLPool, + table_name: str, + number_database_records: int, +) -> None: + connection = await psql_pool.connection() + transaction = connection.transaction() + await transaction.cursor( + querystring=f"SELECT * FROM {table_name}", + ) From 68ae8e2b79488533cb72692da7cc67c77334e4c0 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Wed, 28 Feb 2024 14:00:02 +0400 Subject: [PATCH 16/30] Continue adding tests --- python/tests/test_cursor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py index 57491804..88addc76 100644 --- a/python/tests/test_cursor.py +++ b/python/tests/test_cursor.py @@ -11,6 +11,9 @@ async def test_cursor_fetch( ) -> None: connection = await psql_pool.connection() transaction = connection.transaction() + await transaction.begin() await transaction.cursor( querystring=f"SELECT * FROM {table_name}", ) + + await transaction.commit() From 09ff57e76d1b94b9e7065e0eb2ddc999eb518528 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Thu, 29 Feb 2024 20:11:24 +0400 Subject: [PATCH 17/30] Continue adding tests --- python/tests/conftest.py | 17 +++++++++- python/tests/test_cursor.py | 63 +++++++++++++++++++++++++++++++------ 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 8d81582d..16236d51 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -4,7 +4,7 @@ import pytest -from psqlpy import PSQLPool +from psqlpy import Cursor, PSQLPool @pytest.fixture() @@ -96,3 +96,18 @@ async def create_deafult_data_for_tests( await psql_pool.execute( f"DROP TABLE {table_name}", ) + + +@pytest.fixture() +async def test_cursor( + psql_pool: PSQLPool, + table_name: str, +) -> AsyncGenerator[Cursor, None]: + connection = await psql_pool.connection() + transaction = connection.transaction() + await transaction.begin() + cursor = await transaction.cursor( + querystring=f"SELECT * FROM {table_name}", + ) + yield cursor + await transaction.commit() diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py index 88addc76..949a0a65 100644 --- a/python/tests/test_cursor.py +++ b/python/tests/test_cursor.py @@ -1,19 +1,64 @@ import pytest -from psqlpy import PSQLPool +from psqlpy import Cursor @pytest.mark.anyio async def test_cursor_fetch( - psql_pool: PSQLPool, - table_name: str, number_database_records: int, + test_cursor: Cursor, ) -> None: - connection = await psql_pool.connection() - transaction = connection.transaction() - await transaction.begin() - await transaction.cursor( - querystring=f"SELECT * FROM {table_name}", + """Test cursor fetch with custom number of fetch.""" + result = await test_cursor.fetch(fetch_number=number_database_records // 2) + assert len(result.result()) == number_database_records // 2 + + +@pytest.mark.anyio +async def test_cursor_fetch_next( + test_cursor: Cursor, +) -> None: + """Test cursor fetch next.""" + result = await test_cursor.fetch_next() + assert len(result.result()) == 1 + + +@pytest.mark.anyio +async def test_cursor_fetch_prior( + test_cursor: Cursor, +) -> None: + """Test cursor fetch prior.""" + result = await test_cursor.fetch_prior() + assert len(result.result()) == 0 + + await test_cursor.fetch(fetch_number=2) + result = await test_cursor.fetch_prior() + assert len(result.result()) == 1 + + +@pytest.mark.anyio +async def test_cursor_fetch_first( + test_cursor: Cursor, +) -> None: + """Test cursor fetch first.""" + fetch_first = await test_cursor.fetch(fetch_number=1) + + await test_cursor.fetch(fetch_number=3) + + first = await test_cursor.fetch_first() + + assert fetch_first.result() == first.result() + + +@pytest.mark.anyio +async def test_cursor_fetch_last( + test_cursor: Cursor, + number_database_records: int, +) -> None: + """Test cursor fetch last.""" + all_res = await test_cursor.fetch( + fetch_number=number_database_records, ) - await transaction.commit() + last_res = await test_cursor.fetch_last() + + assert all_res.result()[-1] == last_res.result()[0] From c236a64a9b10493475cc5f925352391f6718d043 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Thu, 29 Feb 2024 21:34:04 +0400 Subject: [PATCH 18/30] Continue adding tests --- python/tests/test_cursor.py | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py index 949a0a65..3b66aeb4 100644 --- a/python/tests/test_cursor.py +++ b/python/tests/test_cursor.py @@ -62,3 +62,46 @@ async def test_cursor_fetch_last( last_res = await test_cursor.fetch_last() assert all_res.result()[-1] == last_res.result()[0] + + +@pytest.mark.anyio +async def test_cursor_fetch_absolute( + test_cursor: Cursor, + number_database_records: int, +) -> None: + """Test cursor fetch Absolute.""" + all_res = await test_cursor.fetch( + fetch_number=number_database_records, + ) + + first_record = await test_cursor.fetch_absolute( + absolute_number=1, + ) + last_record = await test_cursor.fetch_absolute( + absolute_number=-1, + ) + + assert all_res.result()[0] == first_record.result()[0] + assert all_res.result()[-1] == last_record.result()[0] + + +@pytest.mark.anyio +async def test_cursor_fetch_relative( + test_cursor: Cursor, + number_database_records: int, +) -> None: + """Test cursor fetch Relative.""" + first_absolute = await test_cursor.fetch_relative( + relative_number=1, + ) + + assert first_absolute.result() + + await test_cursor.fetch( + fetch_number=number_database_records, + ) + records = await test_cursor.fetch_relative( + relative_number=1, + ) + + assert not (records.result()) From 2409705ddf901ae9d2cdcd20844a5b8740feaf9f Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Sun, 3 Mar 2024 10:08:38 +0100 Subject: [PATCH 19/30] Continue adding tests --- pyproject.toml | 3 +++ python/tests/test_cursor.py | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index b9da62a4..b08db174 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,9 @@ documentation = "/~https://github.com/qaspen-python/psqlpy/blob/main/README.md" profile = "black" multi_line_output = 3 +[tool.black] +line-length = 79 + [tool.mypy] strict = true mypy_path = "python" diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py index 3b66aeb4..29442da8 100644 --- a/python/tests/test_cursor.py +++ b/python/tests/test_cursor.py @@ -105,3 +105,53 @@ async def test_cursor_fetch_relative( ) assert not (records.result()) + + +@pytest.mark.anyio +async def test_cursor_fetch_forward_all( + test_cursor: Cursor, + number_database_records: int, +) -> None: + """Test that cursor execute FETCH FORWARD ALL correctly.""" + default_fetch_number = 2 + await test_cursor.fetch(fetch_number=default_fetch_number) + + rest_results = await test_cursor.fetch_forward_all() + + assert ( + len(rest_results.result()) + == number_database_records - default_fetch_number + ) + + +@pytest.mark.anyio +async def test_cursor_fetch_backward( + test_cursor: Cursor, +) -> None: + """Test cursor backward fetch.""" + must_be_empty = await test_cursor.fetch_backward(backward_count=10) + assert not (must_be_empty.result()) + + default_fetch_number = 5 + await test_cursor.fetch(fetch_number=default_fetch_number) + + expected_number_of_results = 3 + must_not_be_empty = await test_cursor.fetch_backward( + backward_count=expected_number_of_results, + ) + assert len(must_not_be_empty.result()) == expected_number_of_results + + +@pytest.mark.anyio +async def test_cursor_fetch_backward_all( + test_cursor: Cursor, +) -> None: + """Test cursor `fetch_backward_all`.""" + must_be_empty = await test_cursor.fetch_backward_all() + assert not (must_be_empty.result()) + + default_fetch_number = 5 + await test_cursor.fetch(fetch_number=default_fetch_number) + + must_not_be_empty = await test_cursor.fetch_backward_all() + assert len(must_not_be_empty.result()) == default_fetch_number - 1 From 2b0f626d14e7ff04a30ced582fbf58c93c40db47 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Sun, 3 Mar 2024 17:18:44 +0100 Subject: [PATCH 20/30] Added dsn support --- README.md | 26 ++++++++++++++++++ python/psqlpy/_internal/__init__.pyi | 3 +++ python/tests/test_connection_pool.py | 11 ++++++++ src/driver/connection_pool.rs | 40 +++++++++++++++++----------- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a7242938..cfa14d38 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,32 @@ async def main() -> None: Please take into account that each new execute gets new connection from connection pool. +### DSN support +You can separate specify `host`, `port`, `username`, etc or specify everything in one `DSN`. +**Please note that if you specify DSN any other argument doesn't take into account.** +```py +from typing import Any + +from psqlpy import PSQLPool + + +db_pool = PSQLPool( + dsn="postgres://postgres:postgres@localhost:5432/postgres", + max_db_pool_size=2, +) + +async def main() -> None: + await db_pool.startup() + + res: list[dict[str, Any]] = await db_pool.execute( + "SELECT * FROM users", + ) + + print(res) + # You don't need to close Database Pool by yourself, + # rust does it instead. +``` + ## Query parameters You can pass parameters into queries. diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index 3507129c..498d0b46 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -440,6 +440,7 @@ class PSQLPool: def __init__( self: Self, + dsn: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None, host: Optional[str] = None, @@ -460,6 +461,8 @@ class PSQLPool: - Create new instance of `Transaction` ### Parameters: + - `dsn`: full dsn connection string. + `postgres://postgres:postgres@localhost:5432/postgres?target_session_attrs=read-write` - `username`: username of the user in postgres - `password`: password of the user in postgres - `host`: host of postgres diff --git a/python/tests/test_connection_pool.py b/python/tests/test_connection_pool.py index 729b7ef2..5ea6ffdb 100644 --- a/python/tests/test_connection_pool.py +++ b/python/tests/test_connection_pool.py @@ -3,6 +3,17 @@ from psqlpy import Connection, PSQLPool, QueryResult +@pytest.mark.anyio +async def test_pool_dsn_startup() -> None: + """Test that connection pool can startup with dsn.""" + pg_pool = PSQLPool( + dsn="postgres://postgres:postgres@localhost:5432/psqlpy_test", + ) + await pg_pool.startup() + + await pg_pool.execute("SELECT 1") + + @pytest.mark.anyio async def test_pool_execute( psql_pool: PSQLPool, diff --git a/src/driver/connection_pool.rs b/src/driver/connection_pool.rs index b7f6e2c1..b5b481c5 100644 --- a/src/driver/connection_pool.rs +++ b/src/driver/connection_pool.rs @@ -1,6 +1,6 @@ use deadpool_postgres::{Manager, ManagerConfig, Pool, RecyclingMethod}; use pyo3::{pyclass, pymethods, PyAny, Python}; -use std::{sync::Arc, vec}; +use std::{str::FromStr, sync::Arc, vec}; use tokio_postgres::{types::ToSql, NoTls}; use crate::{ @@ -16,6 +16,7 @@ use super::connection::Connection; /// /// It is not exposed to python. pub struct RustPSQLPool { + dsn: Option, username: Option, password: Option, host: Option, @@ -29,6 +30,7 @@ impl RustPSQLPool { /// Create new `RustPSQLPool`. #[must_use] pub fn new( + dsn: Option, username: Option, password: Option, host: Option, @@ -37,6 +39,7 @@ impl RustPSQLPool { max_db_pool_size: Option, ) -> Self { RustPSQLPool { + dsn, username, password, host, @@ -115,6 +118,7 @@ impl RustPSQLPool { /// `max_db_pool_size` is less than 2 or it's impossible to build db pool. pub async fn inner_startup(&self) -> RustPSQLDriverPyResult<()> { let db_pool_arc = self.db_pool.clone(); + let dsn = self.dsn.clone(); let password = self.password.clone(); let username = self.username.clone(); let db_host = self.host.clone(); @@ -137,22 +141,26 @@ impl RustPSQLPool { } } - let mut pg_config = tokio_postgres::Config::new(); - - if let (Some(password), Some(username)) = (password, username) { - pg_config.password(&password); - pg_config.user(&username); - } - if let Some(db_host) = db_host { - pg_config.host(&db_host); - } + let mut pg_config: tokio_postgres::Config; + if let Some(dsn_string) = dsn { + pg_config = tokio_postgres::Config::from_str(&dsn_string)?; + } else { + pg_config = tokio_postgres::Config::new(); + if let (Some(password), Some(username)) = (password, username) { + pg_config.password(&password); + pg_config.user(&username); + } + if let Some(db_host) = db_host { + pg_config.host(&db_host); + } - if let Some(db_port) = db_port { - pg_config.port(db_port); - } + if let Some(db_port) = db_port { + pg_config.port(db_port); + } - if let Some(db_name) = db_name { - pg_config.dbname(&db_name); + if let Some(db_name) = db_name { + pg_config.dbname(&db_name); + } } let mgr_config = ManagerConfig { @@ -180,6 +188,7 @@ impl PSQLPool { #[new] #[must_use] pub fn new( + dsn: Option, username: Option, password: Option, host: Option, @@ -189,6 +198,7 @@ impl PSQLPool { ) -> Self { PSQLPool { rust_psql_pool: Arc::new(tokio::sync::RwLock::new(RustPSQLPool { + dsn, username, password, host, From c1b84b880156567932ba8622e3902955630bde3e Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Sun, 3 Mar 2024 18:27:57 +0100 Subject: [PATCH 21/30] Bumped version to 0.2.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c497e82..d9a20d12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,7 +665,7 @@ dependencies = [ [[package]] name = "psqlpy" -version = "0.2.0" +version = "0.2.1" dependencies = [ "byteorder", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 9c48f106..3bd53b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psqlpy" -version = "0.2.0" +version = "0.2.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From f46298be295975af2d8cc74875e3932519aaf301 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Mon, 4 Mar 2024 17:48:23 +0100 Subject: [PATCH 22/30] Added connection recycling settings --- README.md | 25 ++++++++++++++++ python/psqlpy/__init__.py | 2 ++ python/psqlpy/_internal/__init__.pyi | 45 ++++++++++++++++++++++++++++ python/tests/test_connection_pool.py | 24 ++++++++++++++- src/driver/common_options.rs | 21 +++++++++++++ src/driver/connection_pool.rs | 23 +++++++++++--- src/driver/mod.rs | 1 + src/lib.rs | 1 + 8 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 src/driver/common_options.rs diff --git a/README.md b/README.md index cfa14d38..39376b9d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,31 @@ async def main() -> None: # rust does it instead. ``` +### Control connection recycling +There are 3 available options to control how a connection is recycled - `Fast`, `Verified` and `Clean`. +As connection can be closed in different situations on various sides you can select preferable behavior of how a connection is recycled. + +- `Fast`: Only run `is_closed()` when recycling existing connections. +- `Verified`: Run `is_closed()` and execute a test query. This is slower, but guarantees that the database connection is ready to + be used. Normally, `is_closed()` should be enough to filter + out bad connections, but under some circumstances (i.e. hard-closed + network connections) it's possible that `is_closed()` + returns `false` while the connection is dead. You will receive an error + on your first query then. +- `Clean`: Like [`Verified`] query method, but instead use the following sequence of statements which guarantees a pristine connection: + ```sql + CLOSE ALL; + SET SESSION AUTHORIZATION DEFAULT; + RESET ALL; + UNLISTEN *; + SELECT pg_advisory_unlock_all(); + DISCARD TEMP; + DISCARD SEQUENCES; + ``` + This is similar to calling `DISCARD ALL`. but doesn't call + `DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not + rendered ineffective. + ## Query parameters You can pass parameters into queries. diff --git a/python/psqlpy/__init__.py b/python/psqlpy/__init__.py index 737eeb49..42477791 100644 --- a/python/psqlpy/__init__.py +++ b/python/psqlpy/__init__.py @@ -1,5 +1,6 @@ from ._internal import ( Connection, + ConnRecyclingMethod, Cursor, IsolationLevel, PSQLPool, @@ -16,4 +17,5 @@ "ReadVariant", "Connection", "Cursor", + "ConnRecyclingMethod", ] diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index 498d0b46..a873459a 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -24,6 +24,49 @@ class ReadVariant(Enum): ReadOnly = 1 ReadWrite = 2 +class ConnRecyclingMethod(Enum): + """Possible methods of how a connection is recycled. + + The default is [`Fast`] which does not check the connection health or + perform any clean-up queries. + + # Description: + ## Fast: + Only run [`is_closed()`] when recycling existing connections. + + Unless you have special needs this is a safe choice. + + ## Verified: + Run [`is_closed()`] and execute a test query. + + This is slower, but guarantees that the database connection is ready to + be used. Normally, [`is_closed()`] should be enough to filter + out bad connections, but under some circumstances (i.e. hard-closed + network connections) it's possible that [`is_closed()`] + returns `false` while the connection is dead. You will receive an error + on your first query then. + + ## Clean: + Like [`Verified`] query method, but instead use the following sequence + of statements which guarantees a pristine connection: + ```sql + CLOSE ALL; + SET SESSION AUTHORIZATION DEFAULT; + RESET ALL; + UNLISTEN *; + SELECT pg_advisory_unlock_all(); + DISCARD TEMP; + DISCARD SEQUENCES; + ``` + This is similar to calling `DISCARD ALL`. but doesn't call + `DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not + rendered ineffective. + """ + + Fast = 1 + Verified = 2 + Clean = 3 + class Cursor: """Represent opened cursor in a transaction. @@ -447,6 +490,7 @@ class PSQLPool: port: Optional[int] = None, db_name: Optional[str] = None, max_db_pool_size: Optional[str] = None, + conn_recycling_method: Optional[ConnRecyclingMethod] = None, ) -> None: """Create new PostgreSQL connection pool. @@ -469,6 +513,7 @@ class PSQLPool: - `port`: port of postgres - `db_name`: name of the database in postgres - `max_db_pool_size`: maximum size of the connection pool + - `conn_recycling_method`: how a connection is recycled. """ async def startup(self: Self) -> None: """Startup the connection pool. diff --git a/python/tests/test_connection_pool.py b/python/tests/test_connection_pool.py index 5ea6ffdb..70613899 100644 --- a/python/tests/test_connection_pool.py +++ b/python/tests/test_connection_pool.py @@ -1,6 +1,6 @@ import pytest -from psqlpy import Connection, PSQLPool, QueryResult +from psqlpy import Connection, ConnRecyclingMethod, PSQLPool, QueryResult @pytest.mark.anyio @@ -39,3 +39,25 @@ async def test_pool_connection( """Test that PSQLPool can return single connection from the pool.""" connection = await psql_pool.connection() assert isinstance(connection, Connection) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "conn_recycling_method", + [ + ConnRecyclingMethod.Fast, + ConnRecyclingMethod.Verified, + ConnRecyclingMethod.Clean, + ], +) +async def test_pool_conn_recycling_method( + conn_recycling_method: ConnRecyclingMethod, +) -> None: + pg_pool = PSQLPool( + dsn="postgres://postgres:postgres@localhost:5432/psqlpy_test", + conn_recycling_method=conn_recycling_method, + ) + + await pg_pool.startup() + + await pg_pool.execute("SELECT 1") diff --git a/src/driver/common_options.rs b/src/driver/common_options.rs new file mode 100644 index 00000000..cd8cb362 --- /dev/null +++ b/src/driver/common_options.rs @@ -0,0 +1,21 @@ +use deadpool_postgres::RecyclingMethod; +use pyo3::pyclass; + +#[pyclass] +#[derive(Clone, Copy)] +pub enum ConnRecyclingMethod { + Fast, + Verified, + Clean, +} + +impl ConnRecyclingMethod { + #[must_use] + pub fn to_internal(&self) -> RecyclingMethod { + match self { + ConnRecyclingMethod::Fast => RecyclingMethod::Fast, + ConnRecyclingMethod::Verified => RecyclingMethod::Verified, + ConnRecyclingMethod::Clean => RecyclingMethod::Clean, + } + } +} diff --git a/src/driver/connection_pool.rs b/src/driver/connection_pool.rs index b5b481c5..cba6fac6 100644 --- a/src/driver/connection_pool.rs +++ b/src/driver/connection_pool.rs @@ -10,7 +10,7 @@ use crate::{ value_converter::{convert_parameters, PythonDTO}, }; -use super::connection::Connection; +use super::{common_options::ConnRecyclingMethod, connection::Connection}; /// `PSQLPool` for internal use only. /// @@ -23,12 +23,14 @@ pub struct RustPSQLPool { port: Option, db_name: Option, max_db_pool_size: Option, + conn_recycling_method: Option, db_pool: Arc>>, } impl RustPSQLPool { /// Create new `RustPSQLPool`. #[must_use] + #[allow(clippy::too_many_arguments)] pub fn new( dsn: Option, username: Option, @@ -37,6 +39,7 @@ impl RustPSQLPool { port: Option, db_name: Option, max_db_pool_size: Option, + conn_recycling_method: Option, ) -> Self { RustPSQLPool { dsn, @@ -46,6 +49,7 @@ impl RustPSQLPool { port, db_name, max_db_pool_size, + conn_recycling_method, db_pool: Arc::new(tokio::sync::RwLock::new(None)), } } @@ -124,6 +128,7 @@ impl RustPSQLPool { let db_host = self.host.clone(); let db_port = self.port; let db_name = self.db_name.clone(); + let conn_recycling_method = self.conn_recycling_method; let max_db_pool_size = self.max_db_pool_size; let mut db_pool_guard = db_pool_arc.write().await; @@ -163,9 +168,16 @@ impl RustPSQLPool { } } - let mgr_config = ManagerConfig { - recycling_method: RecyclingMethod::Fast, - }; + let mgr_config: ManagerConfig; + if let Some(conn_recycling_method) = conn_recycling_method { + mgr_config = ManagerConfig { + recycling_method: conn_recycling_method.to_internal(), + } + } else { + mgr_config = ManagerConfig { + recycling_method: RecyclingMethod::Fast, + }; + } let mgr = Manager::from_config(pg_config, NoTls, mgr_config); let mut db_pool_builder = Pool::builder(mgr); @@ -186,6 +198,7 @@ pub struct PSQLPool { #[pymethods] impl PSQLPool { #[new] + #[allow(clippy::too_many_arguments)] #[must_use] pub fn new( dsn: Option, @@ -195,6 +208,7 @@ impl PSQLPool { port: Option, db_name: Option, max_db_pool_size: Option, + conn_recycling_method: Option, ) -> Self { PSQLPool { rust_psql_pool: Arc::new(tokio::sync::RwLock::new(RustPSQLPool { @@ -205,6 +219,7 @@ impl PSQLPool { port, db_name, max_db_pool_size, + conn_recycling_method, db_pool: Arc::new(tokio::sync::RwLock::new(None)), })), } diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 6b0b1a7f..aec33d5b 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -1,3 +1,4 @@ +pub mod common_options; pub mod connection; pub mod connection_pool; pub mod cursor; diff --git a/src/lib.rs b/src/lib.rs index 7008e375..5db06244 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ fn psqlpy(py: Python<'_>, pymod: &PyModule) -> PyResult<()> { pymod.add_class::()?; pymod.add_class::()?; pymod.add_class::()?; + pymod.add_class::()?; pymod.add_class::()?; add_module(py, pymod, "extra_types", extra_types_module)?; add_module(py, pymod, "exceptions", python_exceptions_module)?; From eed31d67996fc96a6f7dc012bae58b27f61c15a9 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Mon, 4 Mar 2024 17:59:30 +0100 Subject: [PATCH 23/30] Bumped version to 0.2.2 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9a20d12..46d35a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,7 +665,7 @@ dependencies = [ [[package]] name = "psqlpy" -version = "0.2.1" +version = "0.2.2" dependencies = [ "byteorder", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 3bd53b43..b97c39d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psqlpy" -version = "0.2.1" +version = "0.2.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 5e630f45d4a6a5e18258df19881d425a07845c44 Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Mon, 4 Mar 2024 18:56:13 +0100 Subject: [PATCH 24/30] Made README more pretty --- README.md | 5 +++++ pyproject.toml | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 39376b9d..80db2b97 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/psqlpy?style=for-the-badge)](https://pypi.org/project/psqlpy/) +[![PyPI](https://img.shields.io/pypi/v/psqlpy?style=for-the-badge)](https://pypi.org/project/psqlpy/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/psqlpy?style=for-the-badge)](https://pypistats.org/packages/psqlpy) + + # PSQLPy - Async PostgreSQL driver for Python written in Rust. Driver for PostgreSQL written fully in Rust and exposed to Python. diff --git a/pyproject.toml b/pyproject.toml index b08db174..720aeea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,20 @@ classifiers = [ "Topic :: Database", "Development Status :: 4 - Beta", "Programming Language :: Rust", + "Programming Language :: Python", "Operating System :: MacOS", "Operating System :: Microsoft", "Operating System :: POSIX :: Linux", "Intended Audience :: Developers", "Topic :: Database :: Front-Ends", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] authors = [{ email = "askiselev00@gmail.com" }, { name = "Kiselev Aleksandr" }] maintainers = [{ name = "Kiselev Aleksandr", email = "askiselev00@gmail.com" }] @@ -118,6 +127,3 @@ line-length = 79 [tool.ruff.pydocstyle] convention = "pep257" ignore-decorators = ["typing.overload"] - -# [tool.ruff.pylint] -# allow-magic-value-types = ["int", "str", "float", "tuple"] From 2ce000499c544931465994af8aa8e1ec884e115d Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Mon, 4 Mar 2024 18:56:54 +0100 Subject: [PATCH 25/30] Bumped version to 0.2.3 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46d35a0f..0fc55462 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,7 +665,7 @@ dependencies = [ [[package]] name = "psqlpy" -version = "0.2.2" +version = "0.2.3" dependencies = [ "byteorder", "bytes", diff --git a/Cargo.toml b/Cargo.toml index b97c39d6..412cd236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psqlpy" -version = "0.2.2" +version = "0.2.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From b038affb296108f8ec54b6d035df0478aacc3c9e Mon Sep 17 00:00:00 2001 From: "chandr-andr (Kiselev Aleksandr)" Date: Mon, 4 Mar 2024 19:52:41 +0100 Subject: [PATCH 26/30] Added small section about benchmarks --- README.md | 54 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 80db2b97..5f518d1f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![PyPI](https://img.shields.io/pypi/v/psqlpy?style=for-the-badge)](https://pypi.org/project/psqlpy/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/psqlpy?style=for-the-badge)](https://pypistats.org/packages/psqlpy) - # PSQLPy - Async PostgreSQL driver for Python written in Rust. Driver for PostgreSQL written fully in Rust and exposed to Python. @@ -67,8 +66,10 @@ async def main() -> None: Please take into account that each new execute gets new connection from connection pool. ### DSN support + You can separate specify `host`, `port`, `username`, etc or specify everything in one `DSN`. **Please note that if you specify DSN any other argument doesn't take into account.** + ```py from typing import Any @@ -93,29 +94,30 @@ async def main() -> None: ``` ### Control connection recycling + There are 3 available options to control how a connection is recycled - `Fast`, `Verified` and `Clean`. As connection can be closed in different situations on various sides you can select preferable behavior of how a connection is recycled. - `Fast`: Only run `is_closed()` when recycling existing connections. - `Verified`: Run `is_closed()` and execute a test query. This is slower, but guarantees that the database connection is ready to - be used. Normally, `is_closed()` should be enough to filter - out bad connections, but under some circumstances (i.e. hard-closed - network connections) it's possible that `is_closed()` - returns `false` while the connection is dead. You will receive an error - on your first query then. + be used. Normally, `is_closed()` should be enough to filter + out bad connections, but under some circumstances (i.e. hard-closed + network connections) it's possible that `is_closed()` + returns `false` while the connection is dead. You will receive an error + on your first query then. - `Clean`: Like [`Verified`] query method, but instead use the following sequence of statements which guarantees a pristine connection: - ```sql - CLOSE ALL; - SET SESSION AUTHORIZATION DEFAULT; - RESET ALL; - UNLISTEN *; - SELECT pg_advisory_unlock_all(); - DISCARD TEMP; - DISCARD SEQUENCES; - ``` - This is similar to calling `DISCARD ALL`. but doesn't call - `DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not - rendered ineffective. + ```sql + CLOSE ALL; + SET SESSION AUTHORIZATION DEFAULT; + RESET ALL; + UNLISTEN *; + SELECT pg_advisory_unlock_all(); + DISCARD TEMP; + DISCARD SEQUENCES; + ``` + This is similar to calling `DISCARD ALL`. but doesn't call + `DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not + rendered ineffective. ## Query parameters @@ -449,3 +451,19 @@ async def main() -> None: # rust does it instead. ``` + +## Benchmarks + +We have made some benchmark to compare `PSQLPy`, `AsyncPG`, `Psycopg3`. +Main idea is do not compare clear drivers because there are a few situations in which you need to use only driver without any other dependencies. + +**So infrastructure consists of:** + +1. AioHTTP +2. PostgreSQL driver (`PSQLPy`, `AsyncPG`, `Psycopg3`) +3. PostgreSQL v15. Server is located in other part of the world, because we want to simulate network problems. +4. Grafana (dashboards) +5. InfluxDB +6. JMeter (for load testing) + +The results are very promising! `PSQLPy` is faster than `AsyncPG` at best by 2 times, at worst by 45%. `PsycoPG` is 3.5 times slower than `PSQLPy` in the worst case, 60% in the best case. From ba41005c2d294b3cd0d56b037435a2b29bd7530c Mon Sep 17 00:00:00 2001 From: reqww Date: Thu, 29 Feb 2024 15:27:41 +0400 Subject: [PATCH 27/30] transaction deferable --- python/psqlpy/_internal/__init__.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index a873459a..12154dca 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -465,6 +465,7 @@ class Connection: def transaction( self, isolation_level: IsolationLevel | None = None, + deferable: bool | None = None, read_variant: ReadVariant | None = None, deferable: bool | None = None, ) -> Transaction: From 835111485b8e3554980e515b03978dfc601b77fb Mon Sep 17 00:00:00 2001 From: reqww Date: Tue, 5 Mar 2024 14:30:59 +0300 Subject: [PATCH 28/30] add test --- pyproject.toml | 5 +++ python/psqlpy/_internal/__init__.pyi | 5 ++- python/tests/conftest.py | 6 ++-- python/tests/test_connection.py | 4 +-- python/tests/test_connection_pool.py | 8 ++--- python/tests/test_cursor.py | 20 +++++------ python/tests/test_transaction.py | 54 ++++++++++++++++++++++++---- 7 files changed, 75 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 720aeea8..e870342f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,3 +127,8 @@ line-length = 79 [tool.ruff.pydocstyle] convention = "pep257" ignore-decorators = ["typing.overload"] + +[tool.ruff.isort] +lines-after-imports = 2 +no-lines-before = ["standard-library", "local-folder"] +known-first-party = ["psqlpy"] diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index 12154dca..cff87ed6 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -197,6 +197,10 @@ class Transaction: `.transaction()`. """ + isolation_level: IsolationLevel | None = None + deferable: bool | None = None + read_variant: ReadVariant | None = None + async def __aenter__(self: Self) -> Self: ... async def __aexit__( self: Self, @@ -467,7 +471,6 @@ class Connection: isolation_level: IsolationLevel | None = None, deferable: bool | None = None, read_variant: ReadVariant | None = None, - deferable: bool | None = None, ) -> Transaction: """Create new transaction. diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 16236d51..d8cbb4e3 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -64,7 +64,7 @@ async def psql_pool( postgres_password: str, postgres_port: int, postgres_dbname: str, -) -> AsyncGenerator[PSQLPool, None]: +) -> PSQLPool: pg_pool = PSQLPool( username=postgres_user, password=postgres_password, @@ -73,11 +73,11 @@ async def psql_pool( db_name=postgres_dbname, ) await pg_pool.startup() - yield pg_pool + return pg_pool @pytest.fixture(autouse=True) -async def create_deafult_data_for_tests( +async def _create_deafult_data_for_tests( psql_pool: PSQLPool, table_name: str, number_database_records: int, diff --git a/python/tests/test_connection.py b/python/tests/test_connection.py index 01084da8..5b54d5bd 100644 --- a/python/tests/test_connection.py +++ b/python/tests/test_connection.py @@ -3,7 +3,7 @@ from psqlpy import PSQLPool, QueryResult, Transaction -@pytest.mark.anyio +@pytest.mark.anyio() async def test_connection_execute( psql_pool: PSQLPool, table_name: str, @@ -19,7 +19,7 @@ async def test_connection_execute( assert len(conn_result.result()) == number_database_records -@pytest.mark.anyio +@pytest.mark.anyio() async def test_connection_transaction( psql_pool: PSQLPool, ) -> None: diff --git a/python/tests/test_connection_pool.py b/python/tests/test_connection_pool.py index 70613899..d1f58261 100644 --- a/python/tests/test_connection_pool.py +++ b/python/tests/test_connection_pool.py @@ -3,7 +3,7 @@ from psqlpy import Connection, ConnRecyclingMethod, PSQLPool, QueryResult -@pytest.mark.anyio +@pytest.mark.anyio() async def test_pool_dsn_startup() -> None: """Test that connection pool can startup with dsn.""" pg_pool = PSQLPool( @@ -14,7 +14,7 @@ async def test_pool_dsn_startup() -> None: await pg_pool.execute("SELECT 1") -@pytest.mark.anyio +@pytest.mark.anyio() async def test_pool_execute( psql_pool: PSQLPool, table_name: str, @@ -32,7 +32,7 @@ async def test_pool_execute( assert len(inner_result) == number_database_records -@pytest.mark.anyio +@pytest.mark.anyio() async def test_pool_connection( psql_pool: PSQLPool, ) -> None: @@ -41,7 +41,7 @@ async def test_pool_connection( assert isinstance(connection, Connection) -@pytest.mark.anyio +@pytest.mark.anyio() @pytest.mark.parametrize( "conn_recycling_method", [ diff --git a/python/tests/test_cursor.py b/python/tests/test_cursor.py index 29442da8..ea85621e 100644 --- a/python/tests/test_cursor.py +++ b/python/tests/test_cursor.py @@ -3,7 +3,7 @@ from psqlpy import Cursor -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch( number_database_records: int, test_cursor: Cursor, @@ -13,7 +13,7 @@ async def test_cursor_fetch( assert len(result.result()) == number_database_records // 2 -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_next( test_cursor: Cursor, ) -> None: @@ -22,7 +22,7 @@ async def test_cursor_fetch_next( assert len(result.result()) == 1 -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_prior( test_cursor: Cursor, ) -> None: @@ -35,7 +35,7 @@ async def test_cursor_fetch_prior( assert len(result.result()) == 1 -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_first( test_cursor: Cursor, ) -> None: @@ -49,7 +49,7 @@ async def test_cursor_fetch_first( assert fetch_first.result() == first.result() -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_last( test_cursor: Cursor, number_database_records: int, @@ -64,7 +64,7 @@ async def test_cursor_fetch_last( assert all_res.result()[-1] == last_res.result()[0] -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_absolute( test_cursor: Cursor, number_database_records: int, @@ -85,7 +85,7 @@ async def test_cursor_fetch_absolute( assert all_res.result()[-1] == last_record.result()[0] -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_relative( test_cursor: Cursor, number_database_records: int, @@ -107,7 +107,7 @@ async def test_cursor_fetch_relative( assert not (records.result()) -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_forward_all( test_cursor: Cursor, number_database_records: int, @@ -124,7 +124,7 @@ async def test_cursor_fetch_forward_all( ) -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_backward( test_cursor: Cursor, ) -> None: @@ -142,7 +142,7 @@ async def test_cursor_fetch_backward( assert len(must_not_be_empty.result()) == expected_number_of_results -@pytest.mark.anyio +@pytest.mark.anyio() async def test_cursor_fetch_backward_all( test_cursor: Cursor, ) -> None: diff --git a/python/tests/test_transaction.py b/python/tests/test_transaction.py index 9bc1890d..eaa42f88 100644 --- a/python/tests/test_transaction.py +++ b/python/tests/test_transaction.py @@ -1,10 +1,50 @@ +from __future__ import annotations +import typing + import pytest -from psqlpy import Cursor, PSQLPool +from psqlpy import Cursor, IsolationLevel, PSQLPool, ReadVariant from psqlpy.exceptions import DBTransactionError -@pytest.mark.anyio +@pytest.mark.anyio() +async def test_transaction_init_parameters(psql_pool: PSQLPool) -> None: + connection = await psql_pool.connection() + transaction = connection.transaction() + + test_init_parameters: typing.Final[list[dict[str, typing.Any]]] = [ + {"isolation_level": None, "deferable": None, "read_variant": None}, + { + "isolation_level": IsolationLevel.ReadCommitted, + "deferable": True, + "read_variant": ReadVariant.ReadOnly, + }, + { + "isolation_level": IsolationLevel.ReadUncommitted, + "deferable": False, + "read_variant": ReadVariant.ReadWrite, + }, + { + "isolation_level": IsolationLevel.RepeatableRead, + "deferable": True, + "read_variant": ReadVariant.ReadOnly, + }, + { + "isolation_level": IsolationLevel.Serializable, + "deferable": False, + "read_variant": ReadVariant.ReadWrite, + }, + ] + + for init_parameters in test_init_parameters: + connection.transaction( + isolation_level=init_parameters.get("isolation_level"), + deferable=init_parameters.get("deferable"), + read_variant=init_parameters.get("read_variant"), + ) + + +@pytest.mark.anyio() async def test_transaction_begin( psql_pool: PSQLPool, table_name: str, @@ -28,7 +68,7 @@ async def test_transaction_begin( assert len(result.result()) == number_database_records -@pytest.mark.anyio +@pytest.mark.anyio() async def test_transaction_commit( psql_pool: PSQLPool, table_name: str, @@ -62,7 +102,7 @@ async def test_transaction_commit( assert len(result.result()) -@pytest.mark.anyio +@pytest.mark.anyio() async def test_transaction_savepoint( psql_pool: PSQLPool, table_name: str, @@ -95,7 +135,7 @@ async def test_transaction_savepoint( await transaction.commit() -@pytest.mark.anyio +@pytest.mark.anyio() async def test_transaction_rollback( psql_pool: PSQLPool, table_name: str, @@ -133,7 +173,7 @@ async def test_transaction_rollback( assert not (result_from_conn.result()) -@pytest.mark.anyio +@pytest.mark.anyio() async def test_transaction_release_savepoint( psql_pool: PSQLPool, ) -> None: @@ -156,7 +196,7 @@ async def test_transaction_release_savepoint( await transaction.savepoint(sp_name_1) -@pytest.mark.anyio +@pytest.mark.anyio() async def test_transaction_cursor( psql_pool: PSQLPool, table_name: str, From af7bed4c1c22f549221a3910b712058cd5876a9b Mon Sep 17 00:00:00 2001 From: reqww Date: Tue, 5 Mar 2024 14:32:45 +0300 Subject: [PATCH 29/30] add test --- python/tests/test_transaction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/tests/test_transaction.py b/python/tests/test_transaction.py index eaa42f88..5e592964 100644 --- a/python/tests/test_transaction.py +++ b/python/tests/test_transaction.py @@ -10,7 +10,6 @@ @pytest.mark.anyio() async def test_transaction_init_parameters(psql_pool: PSQLPool) -> None: connection = await psql_pool.connection() - transaction = connection.transaction() test_init_parameters: typing.Final[list[dict[str, typing.Any]]] = [ {"isolation_level": None, "deferable": None, "read_variant": None}, From 1004db7e902319a208aff0603dc5c93c107b3f59 Mon Sep 17 00:00:00 2001 From: reqww Date: Tue, 5 Mar 2024 14:33:02 +0300 Subject: [PATCH 30/30] add test --- python/psqlpy/_internal/__init__.pyi | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/psqlpy/_internal/__init__.pyi b/python/psqlpy/_internal/__init__.pyi index cff87ed6..667bc0f9 100644 --- a/python/psqlpy/_internal/__init__.pyi +++ b/python/psqlpy/_internal/__init__.pyi @@ -197,10 +197,6 @@ class Transaction: `.transaction()`. """ - isolation_level: IsolationLevel | None = None - deferable: bool | None = None - read_variant: ReadVariant | None = None - async def __aenter__(self: Self) -> Self: ... async def __aexit__( self: Self,