|
3 | 3 | Validates that SQLModel table=True models can be used with AA repositories and services. |
4 | 4 | """ |
5 | 5 |
|
| 6 | +from collections.abc import Generator |
6 | 7 | from typing import Any, Optional, cast |
7 | 8 | from unittest.mock import MagicMock |
8 | 9 |
|
9 | 10 | import pytest |
| 11 | +from sqlalchemy import create_engine |
| 12 | +from sqlalchemy.orm import Session, sessionmaker |
10 | 13 |
|
11 | 14 | sqlmodel = pytest.importorskip("sqlmodel") |
12 | 15 |
|
13 | 16 | from sqlmodel import Field as SQLModelField # noqa: E402 |
14 | 17 | from sqlmodel import SQLModel # noqa: E402 |
15 | 18 |
|
16 | 19 | from advanced_alchemy.base import ModelProtocol, model_to_dict # noqa: E402 |
| 20 | +from advanced_alchemy.repository import SQLAlchemySyncRepository # noqa: E402 |
17 | 21 | from advanced_alchemy.repository._util import get_instrumented_attr, get_primary_key_info, model_from_dict # noqa: E402 |
18 | 22 | from advanced_alchemy.service.typing import ( # noqa: E402 |
19 | 23 | is_pydantic_model, |
@@ -284,3 +288,73 @@ def test_model_to_dict_roundtrip_via_model_from_dict() -> None: |
284 | 288 | assert rebuilt.name == hero.name |
285 | 289 | assert rebuilt.secret_name == hero.secret_name |
286 | 290 | assert rebuilt.age == hero.age |
| 291 | + |
| 292 | + |
| 293 | +# --------------------------------------------------------------------------- |
| 294 | +# Chapter 4: Repository integration with SQLModel (in-memory SQLite) |
| 295 | +# --------------------------------------------------------------------------- |
| 296 | + |
| 297 | + |
| 298 | +class HeroRepository(SQLAlchemySyncRepository[HeroModel]): |
| 299 | + """Repository for HeroModel.""" |
| 300 | + |
| 301 | + model_type = HeroModel |
| 302 | + |
| 303 | + |
| 304 | +@pytest.fixture() |
| 305 | +def hero_session() -> "Generator[Session, None, None]": |
| 306 | + """Create an in-memory SQLite session with the HeroModel table.""" |
| 307 | + engine = create_engine("sqlite:///:memory:") |
| 308 | + SQLModel.metadata.create_all(engine) |
| 309 | + session_factory = sessionmaker(engine, expire_on_commit=False) |
| 310 | + with session_factory() as session: |
| 311 | + yield session # type: ignore[misc] |
| 312 | + |
| 313 | + |
| 314 | +def test_repo_update_many_with_sqlmodel(hero_session: "Session") -> None: |
| 315 | + """Repository.update_many should handle SQLModel model instances via model_to_dict.""" |
| 316 | + repo = HeroRepository(session=hero_session) |
| 317 | + hero = repo.add(HeroModel(name="Spider-Boy", secret_name="Pedro", age=10)) |
| 318 | + hero_session.commit() |
| 319 | + |
| 320 | + hero.age = 20 |
| 321 | + updated = repo.update_many([hero]) |
| 322 | + hero_session.commit() |
| 323 | + assert updated[0].age == 20 |
| 324 | + |
| 325 | + |
| 326 | +def test_repo_upsert_creates_with_sqlmodel(hero_session: "Session") -> None: |
| 327 | + """Repository.upsert should create a new SQLModel instance when not found.""" |
| 328 | + repo = HeroRepository(session=hero_session) |
| 329 | + hero = HeroModel(name="Spider-Boy", secret_name="Pedro", age=10) |
| 330 | + result = repo.upsert(hero) |
| 331 | + hero_session.commit() |
| 332 | + assert result.name == "Spider-Boy" |
| 333 | + assert result.id is not None |
| 334 | + |
| 335 | + |
| 336 | +def test_repo_upsert_updates_with_sqlmodel(hero_session: "Session") -> None: |
| 337 | + """Repository.upsert should update existing SQLModel instance when found by match_fields.""" |
| 338 | + repo = HeroRepository(session=hero_session) |
| 339 | + existing = repo.add(HeroModel(name="Spider-Boy", secret_name="Pedro", age=10)) |
| 340 | + hero_session.commit() |
| 341 | + |
| 342 | + updated_hero = HeroModel(name="Spider-Boy", secret_name="Pedro P", age=15) |
| 343 | + result = repo.upsert(updated_hero, match_fields=["name"]) |
| 344 | + hero_session.commit() |
| 345 | + assert result.id == existing.id |
| 346 | + assert result.secret_name == "Pedro P" |
| 347 | + |
| 348 | + |
| 349 | +def test_repo_upsert_fallback_match_by_all_fields(hero_session: "Session") -> None: |
| 350 | + """Repository.upsert should match by all non-PK fields when no id and no match_fields.""" |
| 351 | + repo = HeroRepository(session=hero_session) |
| 352 | + repo.add(HeroModel(name="Spider-Boy", secret_name="Pedro", age=10)) |
| 353 | + hero_session.commit() |
| 354 | + |
| 355 | + # No id set, no match_fields — triggers model_to_dict(data, exclude=exclude_cols) fallback |
| 356 | + lookup = HeroModel(name="Spider-Boy", secret_name="Pedro", age=10) |
| 357 | + result = repo.upsert(lookup) |
| 358 | + hero_session.commit() |
| 359 | + assert result.name == "Spider-Boy" |
| 360 | + assert result.id is not None |
0 commit comments