Skip to content

Commit bc45e04

Browse files
committed
feat: add soft delete mixin and session
- Add SoftDeleteMixin class with deleted_at field - Add SoftDeleteSession class that overrides delete method for soft deletes - Add tests for soft delete functionality - Fix datetime deprecation warning
1 parent 50b61d6 commit bc45e04

3 files changed

Lines changed: 87 additions & 0 deletions

File tree

sqlmodel/soft_delete.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
4+
from sqlmodel import Field
5+
6+
7+
class SoftDeleteMixin:
8+
deleted_at: Optional[datetime] = Field(default=None, nullable=True)

sqlmodel/soft_delete_session.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from typing import Any
2+
3+
from sqlmodel.orm.session import Session
4+
5+
from sqlmodel.soft_delete import SoftDeleteMixin
6+
7+
8+
class SoftDeleteSession(Session):
9+
def delete(self, instance: Any, hard_delete: bool = False) -> Any:
10+
if not hard_delete and isinstance(instance, SoftDeleteMixin):
11+
instance.deleted_at = self._now()
12+
self.add(instance)
13+
return instance
14+
return super().delete(instance)
15+
16+
def _now(self) -> Any:
17+
from datetime import datetime, timezone
18+
return datetime.now(timezone.utc)

tests/test_session_delete.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import pytest
2+
from datetime import datetime
3+
from typing import Optional
4+
5+
from sqlmodel import SQLModel, create_engine
6+
from sqlmodel.main import default_registry
7+
from sqlmodel.soft_delete import SoftDeleteMixin
8+
from sqlmodel.soft_delete_session import SoftDeleteSession
9+
10+
11+
from sqlmodel import Field
12+
13+
class SoftDeleteHero(SQLModel, SoftDeleteMixin, table=True):
14+
id: Optional[int] = Field(default=None, primary_key=True)
15+
name: str
16+
17+
18+
@pytest.fixture(autouse=True)
19+
def clear_sqlmodel():
20+
# Local override to avoid global dispose
21+
pass
22+
23+
24+
def test_soft_delete():
25+
engine = create_engine("sqlite:///:memory:")
26+
SQLModel.metadata.create_all(engine)
27+
28+
with SoftDeleteSession(engine) as session:
29+
hero = SoftDeleteHero(name="Test Hero")
30+
session.add(hero)
31+
session.commit()
32+
33+
# Soft delete
34+
session.delete(hero)
35+
session.commit()
36+
37+
# Check soft deleted
38+
assert hero.deleted_at is not None
39+
assert isinstance(hero.deleted_at, datetime)
40+
41+
# Refresh to confirm
42+
session.refresh(hero)
43+
assert hero.deleted_at is not None
44+
45+
46+
def test_hard_delete():
47+
engine = create_engine("sqlite:///:memory:")
48+
SQLModel.metadata.create_all(engine)
49+
50+
with SoftDeleteSession(engine) as session:
51+
hero = SoftDeleteHero(name="Test Hero")
52+
session.add(hero)
53+
session.commit()
54+
55+
# Hard delete
56+
session.delete(hero, hard_delete=True)
57+
session.commit()
58+
59+
# Check hard deleted (would raise if not handled, but since hard delete, it's gone)
60+
# In SQLAlchemy, after hard delete, the object is detached
61+
assert hero.id is None or session.get(SoftDeleteHero, hero.id) is None

0 commit comments

Comments
 (0)