"""Event — a typed, dated, placed fact attached to a Person or a partnership. Genealogical dates are messy, so we keep both: - ``date_value`` — the original string, verbatim (e.g. "ABT 1850", "BET 1850 AND 1855"), for fidelity and GEDCOM round-trip. - ``date_start`` / ``date_end`` — a normalized range for sorting and filtering (an exact date sets start == end). A CHECK enforces that exactly one subject (person XOR relationship) is set. """ import uuid from datetime import date from sqlalchemy import CheckConstraint, Date, ForeignKey, String, Text from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base from app.models.mixins import SoftDelete, TenantScoped, Timestamps, UUIDPrimaryKey class Event(Base, UUIDPrimaryKey, TenantScoped, Timestamps, SoftDelete): __tablename__ = "events" __table_args__ = ( CheckConstraint( "(person_id IS NOT NULL) <> (relationship_id IS NOT NULL)", name="subject_person_xor_relationship", ), ) # Open vocabulary (birth, death, marriage, residence, immigration, ...). event_type: Mapped[str] = mapped_column(String(64), index=True) person_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("persons.id", ondelete="CASCADE"), index=True ) relationship_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("relationships.id", ondelete="CASCADE"), index=True ) place_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("places.id", ondelete="SET NULL"), index=True ) date_value: Mapped[str | None] = mapped_column(String(255)) date_start: Mapped[date | None] = mapped_column(Date) date_end: Mapped[date | None] = mapped_column(Date) date_precision: Mapped[str | None] = mapped_column(String(32)) # exact|about|before|after|range calendar: Mapped[str] = mapped_column( String(32), default="gregorian", server_default="gregorian" ) detail: Mapped[str | None] = mapped_column(String(512)) # e.g. occupation, address notes: Mapped[str | None] = mapped_column(Text)