Skip to content

Commit 9dfde90

Browse files
authored
Merge pull request #6 from h0rn3t/uuid-fix
add result_processor to handle UUID conversion; enhance tests for UUI…
2 parents 7d6aa2c + e8f1972 commit 9dfde90

11 files changed

Lines changed: 152 additions & 82 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- 5432:5432
2828
strategy:
2929
matrix:
30-
python: ["3.8", "3.10", "3.11", "3.13.3"]
30+
python: ["3.14", "3.10", "3.11", "3.13.3"]
3131
steps:
3232
- name: Checkout repository
3333
uses: actions/checkout@v4

performance_comparison.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77

88
import asyncio
99
import time
10-
import typing as t
1110
from statistics import mean, median, stdev
12-
from typing import Dict, List
1311

1412
from sqlalchemy import (
1513
Integer,
@@ -36,7 +34,7 @@ class TestModel(Base):
3634

3735
id: Mapped[int] = mapped_column(Integer, primary_key=True)
3836
name: Mapped[str] = mapped_column(String(100), nullable=False)
39-
description: Mapped[t.Optional[str]] = mapped_column(Text)
37+
description: Mapped[str | None] = mapped_column(Text)
4038
value: Mapped[int] = mapped_column(Integer, default=0)
4139

4240

@@ -45,13 +43,13 @@ class BenchmarkResult:
4543

4644
def __init__(self, name: str):
4745
self.name = name
48-
self.times: List[float] = []
46+
self.times: list[float] = []
4947

5048
def add_time(self, duration: float) -> None:
5149
"""Add a timing measurement."""
5250
self.times.append(duration)
5351

54-
def get_stats(self) -> Dict[str, float]:
52+
def get_stats(self) -> dict[str, float]:
5553
"""Calculate statistics for the benchmark."""
5654
if not self.times:
5755
return {"mean": 0, "median": 0, "stdev": 0, "min": 0, "max": 0}
@@ -298,7 +296,7 @@ async def benchmark_transaction(
298296

299297
async def run_benchmarks(
300298
url: str, dialect_name: str
301-
) -> Dict[str, BenchmarkResult]:
299+
) -> dict[str, BenchmarkResult]:
302300
"""Run all benchmarks for a specific dialect."""
303301
print(f"\n{'=' * 60}")
304302
print(f"Running benchmarks for {dialect_name}")
@@ -338,8 +336,8 @@ async def run_benchmarks(
338336

339337

340338
def print_comparison(
341-
psqlpy_results: Dict[str, BenchmarkResult],
342-
asyncpg_results: Dict[str, BenchmarkResult],
339+
psqlpy_results: dict[str, BenchmarkResult],
340+
asyncpg_results: dict[str, BenchmarkResult],
343341
) -> None:
344342
"""Print comparison of results."""
345343
print(f"\n{'=' * 60}")

psqlpy_sqlalchemy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
PsqlpyDialect = PSQLPyAsyncDialect
44

5-
__version__ = "0.1.0a12"
5+
__version__ = "0.1.1b1"
66
__all__ = ["PsqlpyDialect", "PSQLPyAsyncDialect"]

psqlpy_sqlalchemy/connection.py

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import time
55
import typing as t
66
from collections import deque
7-
from typing import Any, Optional, Tuple, Union
7+
from typing import Any
88

99
import psqlpy
1010
from psqlpy import row_factories
@@ -55,7 +55,7 @@ class AsyncAdapt_psqlpy_cursor(AsyncAdapt_dbapi_cursor):
5555

5656
_adapt_connection: "AsyncAdapt_psqlpy_connection"
5757
_connection: psqlpy.Connection # type: ignore[assignment]
58-
_cursor: t.Optional[t.Any] # type: ignore[assignment]
58+
_cursor: t.Any | None # type: ignore[assignment]
5959
_awaitable_cursor_close: bool = False
6060

6161
def __init__(
@@ -66,17 +66,15 @@ def __init__(
6666
self.await_ = adapt_connection.await_
6767
self._rows: deque[t.Any] = deque()
6868
self._cursor = None
69-
self._description: t.Optional[t.List[t.Tuple[t.Any, ...]]] = None
69+
self._description: list[tuple[t.Any, ...]] | None = None
7070
self._arraysize = 1
7171
self._rowcount = -1
7272
self._invalidate_schema_cache_asof = 0
7373

7474
async def _prepare_execute(
7575
self,
7676
querystring: str,
77-
parameters: t.Union[
78-
t.Sequence[t.Any], t.Mapping[str, Any], None
79-
] = None,
77+
parameters: t.Sequence[t.Any] | t.Mapping[str, Any] | None = None,
8078
) -> None:
8179
"""Execute a prepared statement.
8280
@@ -175,10 +173,8 @@ async def _prepare_execute(
175173

176174
def _process_parameters(
177175
self,
178-
parameters: t.Union[
179-
t.Sequence[t.Any], t.Mapping[str, Any], None
180-
] = None,
181-
) -> t.Union[t.Sequence[t.Any], t.Mapping[str, Any], None]:
176+
parameters: t.Sequence[t.Any] | t.Mapping[str, Any] | None = None,
177+
) -> t.Sequence[t.Any] | t.Mapping[str, Any] | None:
182178
"""Process parameters for type conversion.
183179
184180
Converts UUID objects to bytes format required by psqlpy.
@@ -209,7 +205,7 @@ def process_value(value: Any) -> Any:
209205
return {
210206
key: process_value(value) for key, value in parameters.items()
211207
}
212-
if isinstance(parameters, (list, tuple)):
208+
if isinstance(parameters, list | tuple):
213209
return type(parameters)(
214210
process_value(value) for value in parameters
215211
)
@@ -218,10 +214,8 @@ def process_value(value: Any) -> Any:
218214
def _convert_named_params_with_casting(
219215
self,
220216
querystring: str,
221-
parameters: t.Union[
222-
t.Sequence[t.Any], t.Mapping[str, Any], None
223-
] = None,
224-
) -> t.Tuple[str, t.Union[t.Sequence[t.Any], t.Mapping[str, Any], None]]:
217+
parameters: t.Sequence[t.Any] | t.Mapping[str, Any] | None = None,
218+
) -> tuple[str, t.Sequence[t.Any] | t.Mapping[str, Any] | None]:
225219
"""Convert named parameters with PostgreSQL casting syntax to positional parameters.
226220
227221
Transforms queries like:
@@ -326,7 +320,7 @@ def _convert_named_params_with_casting(
326320
return converted_query, converted_params
327321

328322
@property
329-
def description(self) -> "Optional[_DBAPICursorDescription]":
323+
def description(self) -> "_DBAPICursorDescription | None":
330324
return self._description
331325

332326
@property
@@ -387,7 +381,7 @@ async def _executemany(
387381

388382
# Process all parameters first
389383
if seq_of_parameters and all(
390-
isinstance(p, (list, tuple)) for p in seq_of_parameters
384+
isinstance(p, list | tuple) for p in seq_of_parameters
391385
):
392386
converted_seq = [list(p) for p in seq_of_parameters]
393387
else:
@@ -398,7 +392,7 @@ async def _executemany(
398392
converted_seq.append([])
399393
elif isinstance(processed, dict):
400394
converted_seq.append(list(processed.values()))
401-
elif isinstance(processed, (list, tuple)):
395+
elif isinstance(processed, list | tuple):
402396
converted_seq.append(list(processed))
403397
else:
404398
converted_seq.append([processed])
@@ -458,7 +452,7 @@ async def _executemany(
458452
if adapt_connection._transaction is not None:
459453
try:
460454
# Build queries list for pipeline: [(query, params), ...]
461-
queries: t.List[t.Tuple[str, t.Optional[t.List[t.Any]]]] = [
455+
queries: list[tuple[str, list[t.Any] | None]] = [
462456
(operation, params) for params in converted_seq
463457
]
464458
await adapt_connection._transaction.pipeline(
@@ -479,9 +473,7 @@ async def _executemany(
479473
def execute(
480474
self,
481475
operation: t.Any,
482-
parameters: t.Union[
483-
t.Sequence[t.Any], t.Mapping[str, Any], None
484-
] = None,
476+
parameters: t.Sequence[t.Any] | t.Mapping[str, Any] | None = None,
485477
) -> None:
486478
self.await_(self._prepare_execute(operation, parameters))
487479

@@ -500,7 +492,7 @@ class AsyncAdapt_psqlpy_ss_cursor(
500492
):
501493
"""Server-side cursor implementation for psqlpy."""
502494

503-
_cursor: t.Optional[psqlpy.Cursor] # type: ignore[assignment]
495+
_cursor: psqlpy.Cursor | None # type: ignore[assignment]
504496

505497
def __init__(
506498
self, adapt_connection: "AsyncAdapt_psqlpy_connection"
@@ -514,7 +506,7 @@ def __init__(
514506
def _convert_result(
515507
self,
516508
result: psqlpy.QueryResult,
517-
) -> Tuple[Tuple[Any, ...], ...]:
509+
) -> tuple[tuple[Any, ...], ...]:
518510
"""Convert psqlpy QueryResult to tuple of tuples."""
519511
if result is None:
520512
return ()
@@ -540,7 +532,7 @@ def close(self) -> None:
540532
self._cursor = None
541533
self._closed = True
542534

543-
def fetchone(self) -> Optional[Tuple[Any, ...]]:
535+
def fetchone(self) -> tuple[Any, ...] | None:
544536
"""Fetch the next row from the cursor."""
545537
if self._closed or self._cursor is None:
546538
return None
@@ -552,7 +544,7 @@ def fetchone(self) -> Optional[Tuple[Any, ...]]:
552544
except Exception:
553545
return None
554546

555-
def fetchmany(self, size: Optional[int] = None) -> t.List[Tuple[Any, ...]]:
547+
def fetchmany(self, size: int | None = None) -> list[tuple[Any, ...]]:
556548
"""Fetch the next set of rows from the cursor."""
557549
if self._closed or self._cursor is None:
558550
return []
@@ -565,7 +557,7 @@ def fetchmany(self, size: Optional[int] = None) -> t.List[Tuple[Any, ...]]:
565557
except Exception:
566558
return []
567559

568-
def fetchall(self) -> t.List[Tuple[Any, ...]]:
560+
def fetchall(self) -> list[tuple[Any, ...]]:
569561
"""Fetch all remaining rows from the cursor."""
570562
if self._closed or self._cursor is None:
571563
return []
@@ -576,7 +568,7 @@ def fetchall(self) -> t.List[Tuple[Any, ...]]:
576568
except Exception:
577569
return []
578570

579-
def __iter__(self) -> t.Iterator[Tuple[Any, ...]]:
571+
def __iter__(self) -> t.Iterator[tuple[Any, ...]]:
580572
if self._closed or self._cursor is None:
581573
return
582574

@@ -596,7 +588,7 @@ class AsyncAdapt_psqlpy_connection(AsyncAdapt_dbapi_connection):
596588
_ss_cursor_cls = AsyncAdapt_psqlpy_ss_cursor # type: ignore[assignment]
597589

598590
_connection: psqlpy.Connection # type: ignore[assignment]
599-
_transaction: t.Optional[psqlpy.Transaction]
591+
_transaction: psqlpy.Transaction | None
600592

601593
__slots__ = (
602594
"_invalidate_schema_cache_asof",
@@ -637,7 +629,7 @@ def __init__(
637629
# LRU cache for prepared statements. Defaults to 100 statements per
638630
# connection. The cache is on a per-connection basis, stored within
639631
# connections pooled by the connection pool.
640-
self._prepared_statement_cache: t.Optional[util.LRUCache[t.Any, t.Any]]
632+
self._prepared_statement_cache: util.LRUCache[t.Any, t.Any] | None
641633
if prepared_statement_cache_size > 0:
642634
self._prepared_statement_cache = util.LRUCache(
643635
prepared_statement_cache_size
@@ -649,7 +641,7 @@ def __init__(
649641
self._prepared_statement_name_func = self._default_name_func
650642

651643
# Legacy query cache (kept for compatibility)
652-
self._query_cache: t.Dict[str, t.Any] = {}
644+
self._query_cache: dict[str, t.Any] = {}
653645
self._cache_max_size = prepared_statement_cache_size
654646

655647
async def _check_type_cache_invalidation(
@@ -739,7 +731,7 @@ def ping(self, reconnect: t.Any = None) -> t.Any:
739731
self._connection_valid = False
740732
return False
741733

742-
def _get_cached_query(self, query_key: str) -> t.Optional[t.Any]:
734+
def _get_cached_query(self, query_key: str) -> t.Any | None:
743735
"""Get a cached prepared statement if available."""
744736
return self._query_cache.get(query_key)
745737

@@ -761,7 +753,7 @@ def close(self) -> None:
761753

762754
def cursor(
763755
self, server_side: bool = False
764-
) -> Union[AsyncAdapt_psqlpy_cursor, AsyncAdapt_psqlpy_ss_cursor]:
756+
) -> AsyncAdapt_psqlpy_cursor | AsyncAdapt_psqlpy_ss_cursor:
765757
if server_side:
766758
return self._ss_cursor_cls(self)
767759
return self._cursor_cls(self)

psqlpy_sqlalchemy/dbapi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def TimestampFromTicks(self, ticks: float) -> t.Any:
155155

156156
return datetime.datetime.fromtimestamp(ticks)
157157

158-
def Binary(self, string: t.Union[str, bytes]) -> bytes:
158+
def Binary(self, string: str | bytes) -> bytes:
159159
"""Construct a binary value"""
160160
if isinstance(string, str):
161161
return string.encode("utf-8")

psqlpy_sqlalchemy/dialect.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import typing as t
22
import uuid
3+
from collections.abc import MutableMapping, Sequence
34
from types import ModuleType
4-
from typing import Any, Dict, MutableMapping, Sequence, Tuple
5+
from typing import Any
56

67
import psqlpy
78
from sqlalchemy import URL, util
@@ -28,8 +29,8 @@ class CompatibleNullPool(NullPool):
2829
def __init__(
2930
self,
3031
creator: t.Any,
31-
pool_size: t.Optional[int] = None,
32-
max_overflow: t.Optional[int] = None,
32+
pool_size: int | None = None,
33+
max_overflow: int | None = None,
3334
**kw: t.Any,
3435
) -> None:
3536
# Filter out pool sizing arguments that NullPool doesn't accept
@@ -231,10 +232,10 @@ class _PGUUID(UUID[t.Any]):
231232

232233
def bind_processor(
233234
self, dialect: t.Any
234-
) -> t.Optional[t.Callable[[t.Any], t.Any]]:
235+
) -> t.Callable[[t.Any], t.Any] | None:
235236
"""Process UUID parameters for psqlpy compatibility."""
236237

237-
def process(value: t.Any) -> t.Optional[bytes]:
238+
def process(value: t.Any) -> bytes | None:
238239
if value is None:
239240
return None
240241
if isinstance(value, uuid.UUID):
@@ -256,6 +257,33 @@ def process(value: t.Any) -> t.Optional[bytes]:
256257

257258
return process
258259

260+
def result_processor(
261+
self, dialect: t.Any, coltype: t.Any
262+
) -> t.Callable[[t.Any], t.Any] | None:
263+
"""Process UUID results from psqlpy.
264+
265+
Converts string UUID values returned by psqlpy to Python uuid.UUID objects
266+
when as_uuid=True (which is the default in SQLAlchemy 2.0+).
267+
"""
268+
if self.as_uuid:
269+
270+
def process(value: t.Any) -> uuid.UUID | None:
271+
if value is None:
272+
return None
273+
if isinstance(value, uuid.UUID):
274+
return value
275+
if isinstance(value, str):
276+
# psqlpy returns UUID as string, convert to uuid.UUID
277+
return uuid.UUID(value)
278+
if isinstance(value, bytes):
279+
# Handle bytes representation
280+
return uuid.UUID(bytes=value)
281+
# For other types, try to convert
282+
return uuid.UUID(str(value))
283+
284+
return process
285+
return None
286+
259287

260288
class PSQLPyAsyncDialect(PGDialect):
261289
driver = "psqlpy"
@@ -314,7 +342,7 @@ def import_dbapi(cls) -> ModuleType:
314342
return t.cast(ModuleType, PSQLPyAdaptDBAPI(__import__("psqlpy")))
315343

316344
@util.memoized_property
317-
def _isolation_lookup(self) -> Dict[str, Any]:
345+
def _isolation_lookup(self) -> dict[str, Any]:
318346
"""Mapping of SQLAlchemy isolation levels to psqlpy isolation levels"""
319347
return {
320348
"READ_COMMITTED": psqlpy.IsolationLevel.ReadCommitted,
@@ -325,7 +353,7 @@ def _isolation_lookup(self) -> Dict[str, Any]:
325353
def create_connect_args(
326354
self,
327355
url: URL,
328-
) -> Tuple[Sequence[str], MutableMapping[str, Any]]:
356+
) -> tuple[Sequence[str], MutableMapping[str, Any]]:
329357
opts = url.translate_connect_args()
330358
return (
331359
[],

0 commit comments

Comments
 (0)