mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-04-15 22:26:25 +02:00
fix: force auto-begin in create_db_dependency so lock_tables always uses savepoints (#176)
This commit is contained in:
@@ -214,12 +214,12 @@ The `changes` dict maps each watched field that changed to `{"old": ..., "new":
|
|||||||
|
|
||||||
!!! warning "Callbacks fire only for ORM-level changes. Rows updated via raw SQL (`UPDATE ... SET ...`) are not detected."
|
!!! warning "Callbacks fire only for ORM-level changes. Rows updated via raw SQL (`UPDATE ... SET ...`) are not detected."
|
||||||
|
|
||||||
!!! warning "Callbacks fire after the **outermost** transaction commits."
|
!!! warning "Callbacks fire when the **outermost active context** (savepoint or transaction) commits."
|
||||||
If you create several related objects using `CrudFactory.create` and need
|
If you create several related objects using `CrudFactory.create` and need
|
||||||
callbacks to see all of them (including associations), wrap the whole
|
callbacks to see all of them (including associations), wrap the whole
|
||||||
operation in a single [`get_transaction`](db.md) block. Without it, each
|
operation in a single [`get_transaction`](db.md) or [`lock_tables`](db.md)
|
||||||
`create` call commits independently and `on_create` fires before the
|
block. Without it, each `create` call commits its own savepoint and
|
||||||
remaining objects exist.
|
`on_create` fires before the remaining objects exist.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from fastapi_toolsets.db import get_transaction
|
from fastapi_toolsets.db import get_transaction
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ def create_db_dependency(
|
|||||||
|
|
||||||
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
||||||
async with session_maker() as session:
|
async with session_maker() as session:
|
||||||
|
await session.connection()
|
||||||
yield session
|
yield session
|
||||||
if session.in_transaction():
|
if session.in_transaction():
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -68,6 +68,55 @@ class TestCreateDbDependency:
|
|||||||
await conn.run_sync(Base.metadata.drop_all)
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_in_transaction_on_yield(self):
|
||||||
|
"""Session is already in a transaction when the endpoint body starts."""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
get_db = create_db_dependency(session_factory)
|
||||||
|
|
||||||
|
async for session in get_db():
|
||||||
|
assert session.in_transaction()
|
||||||
|
break
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_update_after_lock_tables_is_persisted(self):
|
||||||
|
"""Changes made after lock_tables exits (before endpoint returns) are committed.
|
||||||
|
|
||||||
|
Regression: without the auto-begin fix, lock_tables would start and commit a
|
||||||
|
real outer transaction, leaving the session idle. Any modifications after that
|
||||||
|
point were silently dropped.
|
||||||
|
"""
|
||||||
|
engine = create_async_engine(DATABASE_URL, echo=False)
|
||||||
|
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||||
|
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_db = create_db_dependency(session_factory)
|
||||||
|
|
||||||
|
async for session in get_db():
|
||||||
|
async with lock_tables(session, [Role]):
|
||||||
|
role = Role(name="lock_then_update")
|
||||||
|
session.add(role)
|
||||||
|
await session.flush()
|
||||||
|
# lock_tables has exited — outer transaction must still be open
|
||||||
|
assert session.in_transaction()
|
||||||
|
role.name = "updated_after_lock"
|
||||||
|
|
||||||
|
async with session_factory() as verify:
|
||||||
|
result = await RoleCrud.first(
|
||||||
|
verify, [Role.name == "updated_after_lock"]
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
finally:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
class TestCreateDbContext:
|
class TestCreateDbContext:
|
||||||
"""Tests for create_db_context."""
|
"""Tests for create_db_context."""
|
||||||
|
|||||||
Reference in New Issue
Block a user