Stage 5: Alembic migrations, durable call rows, one transcript truth #5
Reference in New Issue
Block a user
Delete Branch "feature/stage5-data-layer"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stage 5 of the architecture-review roadmap (stacked on #4). The data layer stops being a demo: schema changes are migrations, a call exists in history the moment it starts, and there is exactly one representation each for transcripts, responses, and row mapping.
Alembic first (F8)
alembic.ini+ asyncdb/migrations/env.pyagainstBase.metadata. Two entry paths share it: CLI (alembic upgrade head) builds its own engine fromSettings.database_url; app startup passes its open connection viaconfig.attributes["connection"].1173a71329edautogenerated from the create_all-era schema;5187577efc23drops the deadcall_records.transcriptText column and convertsdevices.is_onlineString → Boolean (batch mode for SQLite,USING coalesce(lower(is_online) in ('true','t','1'), false)for Postgres).init_dbnow runsupgrade head; a pre-Alembic database (noalembic_version, tables present) is stamped at the baseline first, so existing deployments adopt cleanly.create_allremains for tests only.init_dbadopt + migrate it — now at head with the new schema. (Noted: an orphancontactstable from the Stage-4 model deletion still sits in the dev DB; left untouched.)Durable call rows (F8)
CallManagergains anon_call_createdhook; the composition root wires it topersist_call_on_create, which inserts anin_progressCallRecord immediately (same 3× retry policy).persist_call_on_endnow finalizes that row (update-or-insert, so a failed create still gets a record). Acceptance test run live: placed a mock call over REST,kill -9'd the server mid-call — thein_progressrow survived.One transcript representation (F8)
ActiveCall.transcript_chunksholdsTranscriptEntry(t_offset_ms, speaker, text);add_transcriptstamps real offsets from connect time — click-to-seek can actually work now.speaker="caller"instead of smuggling it in"caller: ..."string prefixes; the prefix-parsing hack in persistence is gone.TranscriptChunkrows carry the realt_offset_ms; the never-readCallRecord.transcriptText column is dropped by migration.Model de-triplication (F8)
CallResponse.from_call/CallStatusResponse.from_call— the one ActiveCall→response mapping, replacing hand-copied field lists inapi/calls.py.DeviceStatusdeleted:can_receive_callis now a serializedcomputed_fieldon theDevicedomain model and the list endpoint returnslist[Device].call_persistence.py:record_summary/record_detail/chunk_to_dict(call_history routes are now one-liners) andcreate/update/delete_device_row(devices routes no longer touch the ORM).Tests
New
tests/test_data_layer.py(7): upgrade-head-matches-models, pre-Alembic adoption (baseline schema withoutalembic_version→ stamped + migrated), durablein_progressrow from creation through finalize with transcript chunks, end-without-create fallback, real transcript offsets + speakers, consolidated response models. 134 passed + the 2 pre-existing failures on main.Live verification: boot with
USE_MOCK_SIP=truemigrated the dev DB (alembic_version=5187577efc23, transcript column gone,is_onlineboolean),/healthdegraded/mock/db-ok, history endpoint serves from the shared mappers.🤖 Generated with Claude Code