Skip to content

Commit 9a3a626

Browse files
authored
Merge pull request #8 from qaspen-python/transaction_defferable_2
transaction deferable
2 parents 68eff10 + 75da6f5 commit 9a3a626

File tree

9 files changed

+149
-69
lines changed

9 files changed

+149
-69
lines changed

README.md

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,39 @@
22
[![PyPI](https://img.shields.io/pypi/v/psqlpy?style=for-the-badge)](https://pypi.org/project/psqlpy/)
33
[![PyPI - Downloads](https://img.shields.io/pypi/dm/psqlpy?style=for-the-badge)](https://pypistats.org/packages/psqlpy)
44

5-
65
# PSQLPy - Async PostgreSQL driver for Python written in Rust.
76

87
Driver for PostgreSQL written fully in Rust and exposed to Python.
98
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.
10-
*Normal documentation is in development.*
9+
_Normal documentation is in development._
1110

1211
## Installation
1312

1413
You can install package with `pip` or `poetry`.
1514

1615
poetry:
16+
1717
```bash
1818
> poetry add psqlpy
1919
```
20+
2021
pip:
22+
2123
```bash
2224
> pip install psqlpy
2325
```
2426

2527
Or you can build it by yourself. To do it, install stable rust and [maturin](https://github.com/PyO3/maturin).
28+
2629
```
2730
> maturin develop --release
2831
```
2932

3033
## Usage
34+
3135
Usage is as easy as possible.
3236
Create new instance of PSQLPool, startup it and start querying.
37+
3338
```python
3439
from typing import Any
3540

@@ -57,11 +62,14 @@ async def main() -> None:
5762
# rust does it instead.
5863

5964
```
65+
6066
Please take into account that each new execute gets new connection from connection pool.
6167

6268
### DSN support
69+
6370
You can separate specify `host`, `port`, `username`, etc or specify everything in one `DSN`.
6471
**Please note that if you specify DSN any other argument doesn't take into account.**
72+
6573
```py
6674
from typing import Any
6775

@@ -86,31 +94,33 @@ async def main() -> None:
8694
```
8795

8896
### Control connection recycling
97+
8998
There are 3 available options to control how a connection is recycled - `Fast`, `Verified` and `Clean`.
9099
As connection can be closed in different situations on various sides you can select preferable behavior of how a connection is recycled.
91100

92101
- `Fast`: Only run `is_closed()` when recycling existing connections.
93102
- `Verified`: Run `is_closed()` and execute a test query. This is slower, but guarantees that the database connection is ready to
94-
be used. Normally, `is_closed()` should be enough to filter
95-
out bad connections, but under some circumstances (i.e. hard-closed
96-
network connections) it's possible that `is_closed()`
97-
returns `false` while the connection is dead. You will receive an error
98-
on your first query then.
103+
be used. Normally, `is_closed()` should be enough to filter
104+
out bad connections, but under some circumstances (i.e. hard-closed
105+
network connections) it's possible that `is_closed()`
106+
returns `false` while the connection is dead. You will receive an error
107+
on your first query then.
99108
- `Clean`: Like [`Verified`] query method, but instead use the following sequence of statements which guarantees a pristine connection:
100-
```sql
101-
CLOSE ALL;
102-
SET SESSION AUTHORIZATION DEFAULT;
103-
RESET ALL;
104-
UNLISTEN *;
105-
SELECT pg_advisory_unlock_all();
106-
DISCARD TEMP;
107-
DISCARD SEQUENCES;
108-
```
109-
This is similar to calling `DISCARD ALL`. but doesn't call
110-
`DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not
111-
rendered ineffective.
109+
```sql
110+
CLOSE ALL;
111+
SET SESSION AUTHORIZATION DEFAULT;
112+
RESET ALL;
113+
UNLISTEN *;
114+
SELECT pg_advisory_unlock_all();
115+
DISCARD TEMP;
116+
DISCARD SEQUENCES;
117+
```
118+
This is similar to calling `DISCARD ALL`. but doesn't call
119+
`DEALLOCATE ALL` and `DISCARD PLAN`, so that the statement cache is not
120+
rendered ineffective.
112121

113122
## Query parameters
123+
114124
You can pass parameters into queries.
115125
Parameters can be passed in any `execute` method as the second parameter, it must be a list.
116126
Any placeholder must be marked with `$< num>`.
@@ -123,7 +133,9 @@ Any placeholder must be marked with `$< num>`.
123133
```
124134

125135
## Connection
136+
126137
You can work with connection instead of DatabasePool.
138+
127139
```python
128140
from typing import Any
129141

@@ -154,17 +166,22 @@ async def main() -> None:
154166
```
155167

156168
## Transactions
169+
157170
Of course it's possible to use transactions with this driver.
158171
It's as easy as possible and sometimes it copies common functionality from PsycoPG and AsyncPG.
159172

160173
### Transaction parameters
174+
161175
In process of transaction creation it is possible to specify some arguments to configure transaction.
162176

163177
- `isolation_level`: level of the isolation. By default - `None`.
164178
- `read_variant`: read option. By default - `None`.
179+
- `deferrable`: deferrable option. By default - `None`.
165180

166181
### You can use transactions as async context managers
182+
167183
By default async context manager only begins and commits transaction automatically.
184+
168185
```python
169186
from typing import Any
170187

@@ -188,6 +205,7 @@ async def main() -> None:
188205
```
189206

190207
### Or you can control transaction fully on your own.
208+
191209
```python
192210
from typing import Any
193211

@@ -217,9 +235,11 @@ async def main() -> None:
217235
```
218236

219237
### Transactions can be rolled back
238+
220239
You must understand that rollback can be executed only once per transaction.
221240
After it's execution transaction state changes to `done`.
222241
If you want to use `ROLLBACK TO SAVEPOINT`, see below.
242+
223243
```python
224244
from typing import Any
225245

@@ -245,6 +265,7 @@ async def main() -> None:
245265
```
246266

247267
### Transaction ROLLBACK TO SAVEPOINT
268+
248269
You can rollback your transaction to the specified savepoint, but before it you must create it.
249270

250271
```python
@@ -278,6 +299,7 @@ async def main() -> None:
278299
```
279300

280301
### Transaction RELEASE SAVEPOINT
302+
281303
It's possible to release savepoint
282304

283305
```python
@@ -306,12 +328,15 @@ async def main() -> None:
306328
```
307329

308330
## Cursors
331+
309332
Library supports PostgreSQL cursors.
310333

311334
Cursors can be created only in transaction. In addition, cursor supports async iteration.
312335

313336
### Cursor parameters
337+
314338
In process of cursor creation you can specify some configuration parameters.
339+
315340
- `querystring`: query for the cursor. Required.
316341
- `parameters`: parameters for the query. Not Required.
317342
- `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.
@@ -355,7 +380,9 @@ async def main() -> None:
355380
```
356381

357382
### Cursor operations
383+
358384
Available cursor operations:
385+
359386
- FETCH count - `cursor.fetch(fetch_number=)`
360387
- FETCH NEXT - `cursor.fetch_next()`
361388
- FETCH PRIOR - `cursor.fetch_prior()`
@@ -368,15 +395,16 @@ Available cursor operations:
368395
- FETCH BACKWARD ALL - `cursor.fetch_backward_all()`
369396

370397
## Extra Types
398+
371399
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.
372400

373-
| Extra Type in Python | Type in PostgreSQL | Type in Rust |
374-
| ------------- | ------------- | -------------
375-
| SmallInt | SmallInt | i16 |
376-
| Integer | Integer | i32 |
377-
| BigInt | BigInt | i64 |
378-
| PyUUID | UUID | Uuid |
379-
| PyJSON | JSON, JSONB | Value |
401+
| Extra Type in Python | Type in PostgreSQL | Type in Rust |
402+
| -------------------- | ------------------ | ------------ |
403+
| SmallInt | SmallInt | i16 |
404+
| Integer | Integer | i32 |
405+
| BigInt | BigInt | i64 |
406+
| PyUUID | UUID | Uuid |
407+
| PyJSON | JSON, JSONB | Value |
380408

381409
```python
382410
from typing import Any
@@ -423,15 +451,17 @@ async def main() -> None:
423451
```
424452

425453
## Benchmarks
454+
426455
We have made some benchmark to compare `PSQLPy`, `AsyncPG`, `Psycopg3`.
427456
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.
428457

429458
**So infrastructure consists of:**
430-
1) AioHTTP
431-
2) PostgreSQL driver (`PSQLPy`, `AsyncPG`, `Psycopg3`)
432-
3) PostgreSQL v15. Server is located in other part of the world, because we want to simulate network problems.
433-
4) Grafana (dashboards)
434-
5) InfluxDB
435-
6) JMeter (for load testing)
436-
437-
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.
459+
460+
1. AioHTTP
461+
2. PostgreSQL driver (`PSQLPy`, `AsyncPG`, `Psycopg3`)
462+
3. PostgreSQL v15. Server is located in other part of the world, because we want to simulate network problems.
463+
4. Grafana (dashboards)
464+
5. InfluxDB
465+
6. JMeter (for load testing)
466+
467+
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.

python/psqlpy/_internal/__init__.pyi

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import types
22
from enum import Enum
3-
from typing import Any, Dict, List, Optional
3+
from typing import Any, Optional
44

55
from typing_extensions import Self
66

77
class QueryResult:
88
"""Result."""
99

10-
def result(self: Self) -> List[Dict[Any, Any]]:
10+
def result(self: Self) -> list[dict[Any, Any]]:
1111
"""Return result from database as a list of dicts."""
1212

1313
class IsolationLevel(Enum):
@@ -221,7 +221,7 @@ class Transaction:
221221
async def execute(
222222
self: Self,
223223
querystring: str,
224-
parameters: List[Any] | None = None,
224+
parameters: list[Any] | None = None,
225225
) -> QueryResult:
226226
"""Execute the query.
227227
@@ -377,7 +377,7 @@ class Transaction:
377377
async def cursor(
378378
self: Self,
379379
querystring: str,
380-
parameters: List[Any] | None = None,
380+
parameters: list[Any] | None = None,
381381
fetch_number: int | None = None,
382382
scroll: bool | None = None,
383383
) -> Cursor:
@@ -429,7 +429,7 @@ class Connection:
429429
async def execute(
430430
self: Self,
431431
querystring: str,
432-
parameters: List[Any] | None = None,
432+
parameters: list[Any] | None = None,
433433
) -> QueryResult:
434434
"""Execute the query.
435435
@@ -466,6 +466,7 @@ class Connection:
466466
self,
467467
isolation_level: IsolationLevel | None = None,
468468
read_variant: ReadVariant | None = None,
469+
deferrable: bool | None = None,
469470
) -> Transaction:
470471
"""Create new transaction.
471472
@@ -522,7 +523,7 @@ class PSQLPool:
522523
async def execute(
523524
self: Self,
524525
querystring: str,
525-
parameters: List[Any] | None = None,
526+
parameters: list[Any] | None = None,
526527
) -> QueryResult:
527528
"""Execute the query.
528529

python/tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def psql_pool(
6464
postgres_password: str,
6565
postgres_port: int,
6666
postgres_dbname: str,
67-
) -> AsyncGenerator[PSQLPool, None]:
67+
) -> PSQLPool:
6868
pg_pool = PSQLPool(
6969
username=postgres_user,
7070
password=postgres_password,
@@ -73,7 +73,7 @@ async def psql_pool(
7373
db_name=postgres_dbname,
7474
)
7575
await pg_pool.startup()
76-
yield pg_pool
76+
return pg_pool
7777

7878

7979
@pytest.fixture(autouse=True)

python/tests/test_connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from psqlpy import PSQLPool, QueryResult, Transaction
44

55

6-
@pytest.mark.anyio
6+
@pytest.mark.anyio()
77
async def test_connection_execute(
88
psql_pool: PSQLPool,
99
table_name: str,
@@ -19,7 +19,7 @@ async def test_connection_execute(
1919
assert len(conn_result.result()) == number_database_records
2020

2121

22-
@pytest.mark.anyio
22+
@pytest.mark.anyio()
2323
async def test_connection_transaction(
2424
psql_pool: PSQLPool,
2525
) -> None:

python/tests/test_connection_pool.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from psqlpy import Connection, ConnRecyclingMethod, PSQLPool, QueryResult
44

55

6-
@pytest.mark.anyio
6+
@pytest.mark.anyio()
77
async def test_pool_dsn_startup() -> None:
88
"""Test that connection pool can startup with dsn."""
99
pg_pool = PSQLPool(
@@ -14,7 +14,7 @@ async def test_pool_dsn_startup() -> None:
1414
await pg_pool.execute("SELECT 1")
1515

1616

17-
@pytest.mark.anyio
17+
@pytest.mark.anyio()
1818
async def test_pool_execute(
1919
psql_pool: PSQLPool,
2020
table_name: str,
@@ -32,7 +32,7 @@ async def test_pool_execute(
3232
assert len(inner_result) == number_database_records
3333

3434

35-
@pytest.mark.anyio
35+
@pytest.mark.anyio()
3636
async def test_pool_connection(
3737
psql_pool: PSQLPool,
3838
) -> None:
@@ -41,7 +41,7 @@ async def test_pool_connection(
4141
assert isinstance(connection, Connection)
4242

4343

44-
@pytest.mark.anyio
44+
@pytest.mark.anyio()
4545
@pytest.mark.parametrize(
4646
"conn_recycling_method",
4747
[

0 commit comments

Comments
 (0)