-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/sqlite repo: setup for GeoLocationServiceSqlRepoDBTest (#68)
Unstable, active and WIP: This is where things start taking shape for setting test coverage `GeoLocationServiceSqlRepoDBTest` to implement issue: #26 ; - major rewrite of test services for integration test for Sqlite Repo DB. refactored code to use container to setup database using start_test_database and asyncSetUp. WIP: test_fetch_facilities - added sqlite models to support sqlite database. database.py enhances app level infra setup using DI using rodi's Container that sets up database and session
- Loading branch information
Showing
10 changed files
with
243 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
""" | ||
Database Models and Delete Behavior Design Principles | ||
1. Query-Patient-Location Relationship: | ||
- Every Query must have both a Patient and a Location associated with it. | ||
- A Patient can have multiple Queries. | ||
- A Location can be associated with multiple Queries. | ||
2. Delete Restrictions: | ||
- Patient and Location records cannot be deleted if there are any Queries referencing them. | ||
- This is enforced by the "RESTRICT" ondelete option in the Query model's foreign keys. | ||
3. Orphan Deletion: | ||
- A Patient or Location should be deleted only when there are no more Queries referencing it. | ||
- This is handled by custom event listeners that check for remaining Queries after a Query deletion. | ||
4. Cascading Behavior: | ||
- There is no automatic cascading delete from Patient or Location to Query. | ||
- Queries must be explicitly deleted before their associated Patient or Location can be removed. | ||
5. Transaction Handling: | ||
- Delete operations and subsequent orphan checks should occur within the same transaction. | ||
- Event listeners use the existing database connection to ensure consistency with the main transaction. | ||
6. Error Handling: | ||
- Errors during the orphan deletion process should not silently fail. | ||
- Exceptions in event listeners are logged and re-raised to ensure proper transaction rollback. | ||
7. Data Integrity: | ||
- Database-level constraints (foreign keys, unique constraints) are used in conjunction with SQLAlchemy model definitions to ensure data integrity. | ||
These principles aim to maintain referential integrity while allowing for the cleanup of orphaned Patient and Location records when appropriate. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import List | ||
from sqlmodel import SQLModel, Field, Relationship | ||
from sqlalchemy import Column, Text, Float, Index | ||
from sqlalchemy.orm import relationship, Mapped | ||
import uuid | ||
from sqlalchemy.dialects.sqlite import TEXT | ||
|
||
|
||
class Patient(SQLModel, table=True): | ||
patient_id: str = Field( | ||
sa_column=Column( | ||
TEXT, unique=True, primary_key=True, default=str(uuid.uuid4()) | ||
), | ||
allow_mutation=False, | ||
) | ||
queries: Mapped[List["Query"]] = Relationship( | ||
# back_populates="patient", | ||
passive_deletes="all", | ||
cascade_delete=True, | ||
sa_relationship=relationship(back_populates="patient"), | ||
) | ||
|
||
|
||
class Query(SQLModel, table=True): | ||
"""Every Query must have both a Patient and a Location.""" | ||
|
||
query_id: str = Field( | ||
sa_column=Column( | ||
TEXT, unique=True, primary_key=True, default=str(uuid.uuid4()) | ||
), | ||
allow_mutation=False, | ||
) | ||
query: str = Field(allow_mutation=False, sa_column=Column(Text)) | ||
# Restrict deleting Patient record when there is atleast 1 query referencing it | ||
patient_id: str = Field(foreign_key="patient.patient_id", ondelete="RESTRICT") | ||
# Restrict deleting Location record when there is atleast 1 query referencing it | ||
location_id: str = Field(foreign_key="location.location_id", ondelete="RESTRICT") | ||
location: Location = Relationship(back_populates="queries") | ||
patient: Patient = Relationship(back_populates="queries") | ||
|
||
|
||
class Location(SQLModel, table=True): | ||
__table_args__ = ( | ||
Index("ix_location_composite_lat_lng", "latitude", "longitude", unique=True), | ||
) | ||
location_id: str = Field( | ||
sa_column=Column( | ||
TEXT, unique=True, primary_key=True, default=str(uuid.uuid4()) | ||
), | ||
allow_mutation=False, | ||
) | ||
latitude: float = Field(sa_column=Column(Float)) | ||
longitude: float = Field(sa_column=Column(Float)) | ||
queries: Mapped[List["Query"]] = Relationship( | ||
# back_populates="location", | ||
cascade_delete=True, | ||
passive_deletes=True, | ||
sa_relationship=relationship(back_populates="location"), | ||
) | ||
|
||
|
||
# TODO: Define Provider SQL model fields | ||
# class Provider(SQLModel, table=True): | ||
# # TODO: Compare with Github issue, domain model and noccodb | ||
# ... | ||
|
||
|
||
# TODO: Add Model events for database ops during testing | ||
# @event.listens_for(Query, "after_delete") | ||
# def delete_dangling_location(mapper: Mapper, connection: Engine, target: Query): | ||
# """Deletes orphan Location when no related queries exist.""" | ||
# local_session = sessionmaker(connection) | ||
# with local_session() as session: | ||
# stmt = ( | ||
# select(func.count()) | ||
# .select_from(Query) | ||
# .where(Query.location_id == target.location_id) | ||
# ) | ||
# if ( | ||
# num_queries := session.execute(stmt).scalar_one_or_none() | ||
# ) and num_queries <= 1: | ||
# location: Location = session.get(Location, target.location_id) | ||
# session.delete(location) | ||
# session.flush() | ||
|
||
|
||
# @event.listens_for(Query, "after_delete") | ||
# def delete_dangling_patient(mapper: Mapper, connection: Engine, target: Query): | ||
# """Deletes orphan Patient records when no related queries exist.""" | ||
# local_session = sessionmaker(connection) | ||
# with local_session() as session: | ||
# stmt = ( | ||
# select(func.count()) | ||
# .select_from(Query) | ||
# .where(Query.patient_id == target.patient_id) | ||
# ) | ||
# if ( | ||
# num_queries := session.execute(stmt).scalar_one_or_none() | ||
# ) and num_queries <= 1: | ||
# patient: Patient = session.get(Patient, target.patient_id) | ||
# session.delete(patient) | ||
# session.flush() |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
"""Dummy data to seed to database models. | ||
Mapped to SQLModel. | ||
dummy GeoLocation: | ||
lat=0 | ||
lng=0 | ||
cust_id=test_cust_id | ||
query_id=test_query_id | ||
""" | ||
|
||
from sqlalchemy import ScalarResult | ||
from sqlmodel import select | ||
from xcov19.infra.models import Patient, Query, Location | ||
from sqlmodel.ext.asyncio.session import AsyncSession as AsyncSessionWrapper | ||
|
||
|
||
async def seed_data(session: AsyncSessionWrapper): | ||
""" | ||
Now you can do: | ||
res = await self._session.exec(select(Query)) | ||
query = res.first() | ||
print("query", query) | ||
res = await self._session.exec(select(Patient).where(Patient.queries.any(Query.query_id == query.query_id))) | ||
print("patient", res.first()) | ||
res = await self._session.exec(select(Location).where(Location.queries.any(Query.query_id == query.query_id))) | ||
print("location", res.first()) | ||
""" | ||
query = Query( | ||
query=""" | ||
Runny nose and high fever suddenly lasting for few hours. | ||
Started yesterday. | ||
""" | ||
) # type: ignore | ||
|
||
patient = Patient(queries=[query]) # type: ignore | ||
|
||
patient_location = Location(latitude=0, longitude=0, queries=[query]) # type: ignore | ||
session.add_all([patient_location, patient]) | ||
await session.commit() | ||
query_result: ScalarResult = await session.exec(select(Query)) | ||
if not query_result.first(): | ||
raise RuntimeError("Database seeding failed") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,27 @@ | ||
from collections.abc import AsyncGenerator | ||
from xcov19.app.main import app | ||
from blacksheep import Application | ||
from contextlib import asynccontextmanager | ||
from rodi import Container, ContainerProtocol | ||
from xcov19.app.database import configure_database_session, setup_database | ||
from xcov19.app.settings import load_settings | ||
from sqlalchemy.ext.asyncio import AsyncEngine | ||
|
||
|
||
async def start_server() -> AsyncGenerator[Application, None]: | ||
@asynccontextmanager | ||
async def start_server(app: Application) -> AsyncGenerator[Application, None]: | ||
"""Start a test server for automated testing.""" | ||
try: | ||
await app.start() | ||
yield app | ||
finally: | ||
if app.started: | ||
await app.stop() | ||
|
||
|
||
async def start_test_database(container: ContainerProtocol) -> None: | ||
"""Database setup for integration tests.""" | ||
if not isinstance(container, Container): | ||
raise RuntimeError("container not of type Container.") | ||
configure_database_session(container, load_settings()) | ||
engine = container.resolve(AsyncEngine) | ||
await setup_database(engine) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters