33import logging
44import time
55
6+ from collections .abc import AsyncGenerator
67from collections .abc import Generator
78from typing import Any
89
910import pytest
1011
1112from sqlalchemy import text
1213from sqlalchemy .engine import Engine
14+ from sqlalchemy .ext .asyncio import AsyncEngine
15+ from sqlalchemy .ext .asyncio import AsyncSession
16+ from sqlalchemy .ext .asyncio import async_sessionmaker
1317from sqlalchemy .orm import Session
1418from sqlalchemy .orm import sessionmaker
1519
1620from py_pglite .config import PGliteConfig
1721from py_pglite .sqlalchemy .manager import SQLAlchemyPGliteManager
22+ from py_pglite .sqlalchemy .manager_async import SQLAlchemyAsyncPGliteManager
1823
1924
2025# Try to import SQLModel
2126try :
2227 from sqlmodel import Session as SQLModelSession
2328 from sqlmodel import SQLModel
29+ from sqlmodel .ext .asyncio .session import AsyncSession as SQLModelAsyncSession
2430
2531 HAS_SQLMODEL = True
2632except ImportError :
@@ -55,6 +61,25 @@ def pglite_sqlalchemy_manager(
5561 manager .stop ()
5662
5763
64+ @pytest .fixture (scope = "session" )
65+ async def pglite_async_sqlalchemy_manager (
66+ pglite_config : PGliteConfig ,
67+ ) -> AsyncGenerator [SQLAlchemyAsyncPGliteManager , None , None ]:
68+ """Pytest fixture providing an async SQLAlchemy-enabled PGlite manager."""
69+ manager = SQLAlchemyAsyncPGliteManager (pglite_config )
70+ manager .start ()
71+
72+ is_ready = await manager .wait_for_ready ()
73+ # Wait for database to be ready
74+ if not is_ready :
75+ raise RuntimeError ("Failed to start PGlite database" )
76+
77+ try :
78+ yield manager
79+ finally :
80+ await manager .stop ()
81+
82+
5883@pytest .fixture (scope = "session" )
5984def pglite_engine (pglite_sqlalchemy_manager : SQLAlchemyPGliteManager ) -> Engine :
6085 """Pytest fixture providing a SQLAlchemy engine connected to PGlite.
@@ -65,6 +90,18 @@ def pglite_engine(pglite_sqlalchemy_manager: SQLAlchemyPGliteManager) -> Engine:
6590 return pglite_sqlalchemy_manager .get_engine ()
6691
6792
93+ @pytest .fixture (scope = "session" )
94+ def pglite_async_engine (
95+ pglite_async_sqlalchemy_manager : SQLAlchemyAsyncPGliteManager ,
96+ ) -> AsyncEngine :
97+ """Pytest fixture providing a SQLAlchemy async engine connected to PGlite.
98+
99+ Uses the SQLAlchemy-enabled manager to ensure proper SQLAlchemy integration.
100+ """
101+ # Use the shared engine from manager (no custom parameters to avoid conflicts)
102+ return pglite_async_sqlalchemy_manager .get_engine ()
103+
104+
68105@pytest .fixture (scope = "session" )
69106def pglite_sqlalchemy_engine (
70107 pglite_sqlalchemy_manager : SQLAlchemyPGliteManager ,
@@ -74,6 +111,15 @@ def pglite_sqlalchemy_engine(
74111 return pglite_sqlalchemy_manager .get_engine ()
75112
76113
114+ @pytest .fixture (scope = "session" )
115+ def pglite_sqlalchemy_async_engine (
116+ pglite_async_sqlalchemy_manager : SQLAlchemyAsyncPGliteManager ,
117+ ) -> AsyncEngine :
118+ """Pytest fixture providing an optimized SQLAlchemy async engine connected to PGlite."""
119+ # Use the shared engine from manager (no custom parameters to avoid conflicts)
120+ return pglite_async_sqlalchemy_manager .get_engine ()
121+
122+
77123@pytest .fixture (scope = "function" )
78124def pglite_session (pglite_engine : Engine ) -> Generator [Any , None , None ]:
79125 """Pytest fixture providing a SQLAlchemy/SQLModel session with proper isolation.
@@ -161,6 +207,96 @@ def pglite_session(pglite_engine: Engine) -> Generator[Any, None, None]:
161207 logger .warning (f"Error closing session: { e } " )
162208
163209
210+ @pytest .fixture (scope = "function" )
211+ async def pglite_async_session (
212+ pglite_async_engine : AsyncEngine ,
213+ ) -> AsyncGenerator [Any , None , None ]:
214+ """Pytest fixture providing a SQLAlchemy/SQLModel async session with proper isolation.
215+
216+ This fixture ensures database isolation between tests by cleaning all data
217+ at the start of each test.
218+ """
219+ # Clean up data before test starts
220+ logger .info ("Starting database cleanup before test..." )
221+ retry_count = 3
222+ for attempt in range (retry_count ):
223+ try :
224+ async with pglite_async_engine .connect () as conn :
225+ # Get all table names from information_schema
226+ result = await conn .execute (
227+ text ("""
228+ SELECT table_name
229+ FROM information_schema.tables
230+ WHERE table_schema = 'public'
231+ AND table_type = 'BASE TABLE'
232+ """ )
233+ )
234+
235+ table_names = [row [0 ] for row in result ]
236+ logger .info (f"Found tables to clean: { table_names } " )
237+
238+ if table_names :
239+ # Disable foreign key checks for faster cleanup
240+ await conn .execute (text ("SET session_replication_role = replica;" ))
241+
242+ # Truncate all tables
243+ for table_name in table_names :
244+ logger .info (f"Truncating table: { table_name } " )
245+ await conn .execute (
246+ text (
247+ f'TRUNCATE TABLE "{ table_name } " '
248+ f"RESTART IDENTITY CASCADE;"
249+ )
250+ )
251+
252+ # Re-enable foreign key checks
253+ await conn .execute (text ("SET session_replication_role = DEFAULT;" ))
254+
255+ # Commit the cleanup
256+ await conn .commit ()
257+ logger .info ("Database cleanup completed successfully" )
258+ else :
259+ logger .info ("No tables found to clean" )
260+ break # Success, exit retry loop
261+
262+ except Exception as e :
263+ logger .info (f"Database cleanup attempt { attempt + 1 } failed: { e } " )
264+ if attempt == retry_count - 1 :
265+ logger .warning (
266+ "Database cleanup failed after all retries, continuing anyway"
267+ )
268+ else :
269+ time .sleep (0.5 ) # Brief pause before retry
270+
271+ # Create session - prefer SQLModel if available
272+ if HAS_SQLMODEL and SQLModelAsyncSession is not None :
273+ session = SQLModelAsyncSession (pglite_async_engine )
274+ # Create tables if using SQLModel with retry logic
275+ if SQLModel is not None :
276+ for attempt in range (3 ):
277+ try :
278+ async with pglite_async_engine .begin () as conn :
279+ await conn .run_sync (SQLModel .metadata .create_all )
280+ break
281+ except Exception as e :
282+ logger .warning (f"Table creation attempt { attempt + 1 } failed: { e } " )
283+ if attempt == 2 :
284+ raise
285+ time .sleep (0.5 )
286+ else :
287+ session_local = async_sessionmaker (bind = pglite_async_engine )
288+ session = session_local () # type: ignore[assignment]
289+
290+ try :
291+ yield session
292+ finally :
293+ # Close the session safely
294+ try :
295+ await session .close ()
296+ except Exception as e :
297+ logger .warning (f"Error closing session: { e } " )
298+
299+
164300@pytest .fixture (scope = "function" )
165301def pglite_sqlalchemy_session (pglite_session : Session ) -> Session :
166302 """Legacy fixture name for backwards compatibility."""
0 commit comments