Files
justin 297cb797d6 Add core data model (12 tables) and initial Alembic migration
All core entities from ARCHITECTURE §5: tenancy (User, Tree, TreeMembership), people (Person, Name, Relationship), facts (Event, Place, PlaceName), provenance (Source, Citation), and the append-only AuditEntry. Cross-cutting mixins give every row a UUID key, timestamps, soft delete, and (where tree-owned) a tree_id for uniform tenant isolation.

Modeling choices: parentage as qualified edges (biological/adoptive/step/foster/donor/guardian) so non-traditional families are first-class; events keep both a verbatim date string and a normalized start/end range; closed sets are PG enums while GEDCOM-extensible vocabularies (event/name/source type) stay strings; CHECK constraints enforce single-subject events and single-target citations. Place is tree-scoped in Phase 0 (see ARCHITECTURE note). The migration is verified reversible (upgrade/downgrade drops tables and enum types) and matches the models (alembic check clean); applied on the deploy target. Dockerfile now ships migrations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
2026-06-06 10:40:00 -04:00

52 lines
2.1 KiB
Python

"""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)