Stage 5: Alembic migrations, durable call rows, one transcript truth
Alembic replaces create_all as the schema authority: async env.py against Base.metadata (CLI and in-app entry paths share it via config.attributes["connection"]), an autogenerated baseline of the create_all-era schema, and init_db now runs upgrade head — stamping the baseline first on a pre-Alembic database so existing deployments adopt cleanly. create_all remains for tests only. Calls are durable from the start: CallManager gains an on_call_created hook (wired to persist_call_on_create) that inserts an in_progress CallRecord the moment a call is created; persist_call_on_end finalizes that same row. A SIGKILL mid-call now leaves an in_progress row instead of erasing the call from history (verified live against the dev database). One transcript representation: ActiveCall.transcript_chunks holds TranscriptEntry (t_offset_ms, speaker, text) — add_transcript stamps real offsets from connect time, receptionist passes speaker instead of encoding it into "caller: ..." strings, persisted chunks carry real seek offsets, and the dead CallRecord.transcript Text column is dropped by migration. Device.is_online migrates String → Boolean (with a USING cast for existing rows). Model de-triplication: CallResponse/CallStatusResponse build via from_call classmethods (one ActiveCall→response mapping); DeviceStatus deleted — can_receive_call is a computed field on Device and the list endpoint returns the domain model; all row↔dict and row↔domain mapping now lives in call_persistence.py (record_summary/record_detail/chunk_to_dict + device row functions). New tests/test_data_layer.py: upgrade-head-matches-models, pre-Alembic adoption, durable in_progress rows, end-without-create fallback, transcript offsets, consolidated response models. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ PostgreSQL via asyncpg + SQLAlchemy async.
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
@@ -51,7 +51,6 @@ class CallRecord(Base):
|
||||
hold_time = Column(Integer, default=0) # seconds spent on hold
|
||||
device_used = Column(String)
|
||||
recording_path = Column(String, nullable=True)
|
||||
transcript = Column(Text, nullable=True)
|
||||
summary = Column(Text, nullable=True)
|
||||
action_items = Column(JSON, nullable=True)
|
||||
sentiment = Column(String, nullable=True)
|
||||
@@ -94,7 +93,7 @@ class Device(Base):
|
||||
sip_uri = Column(String, nullable=True) # sip:robert@gateway.helu.ca
|
||||
phone_number = Column(String, nullable=True) # For PSTN devices
|
||||
priority = Column(Integer, default=10) # Routing priority (lower = higher priority)
|
||||
is_online = Column(String, default="false")
|
||||
is_online = Column(Boolean, default=False, nullable=False)
|
||||
capabilities = Column(JSON, default=list) # ["voice", "video", "sms"]
|
||||
dnd = Column(Boolean, default=False, nullable=False)
|
||||
last_seen = Column(DateTime, nullable=True)
|
||||
@@ -211,11 +210,31 @@ async def get_db() -> AsyncIterator[AsyncSession]:
|
||||
yield session
|
||||
|
||||
|
||||
# The autogenerated baseline revision — a schema created by the old
|
||||
# create_all path is identical to it, so such databases are stamped
|
||||
# here and then migrated forward like any other.
|
||||
_BASELINE_REVISION = "1173a71329ed"
|
||||
|
||||
|
||||
def _upgrade_to_head(connection) -> None:
|
||||
from alembic import command
|
||||
from alembic.config import Config
|
||||
from sqlalchemy import inspect
|
||||
|
||||
cfg = Config(str(Path(__file__).resolve().parent.parent / "alembic.ini"))
|
||||
cfg.attributes["connection"] = connection
|
||||
|
||||
inspector = inspect(connection)
|
||||
if not inspector.has_table("alembic_version") and inspector.has_table("call_records"):
|
||||
command.stamp(cfg, _BASELINE_REVISION)
|
||||
command.upgrade(cfg, "head")
|
||||
|
||||
|
||||
async def init_db():
|
||||
"""Create all tables. For development; use Alembic migrations in production."""
|
||||
"""Bring the schema to Alembic head (tests create tables directly)."""
|
||||
engine = get_engine()
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await conn.run_sync(_upgrade_to_head)
|
||||
|
||||
|
||||
async def close_db():
|
||||
|
||||
Reference in New Issue
Block a user