---
id: S03
parent: M014
milestone: M014
provides:
  - body_sections_format column on technique_pages (default 'v1')
  - technique_page_videos association table
  - SourceVideoSummary schema
  - source_videos field on TechniquePageDetail API response
  - body_sections accepts list | dict | None
requires:
  []
affects:
  - S04
  - S05
  - S06
key_files:
  - alembic/versions/012_multi_source_format.py
  - backend/models.py
  - backend/schemas.py
  - backend/routers/techniques.py
key_decisions:
  - Used TIMESTAMP (not WITH TIME ZONE) for added_at to stay consistent with existing schema convention
patterns_established:
  - Association table pattern with dual CASCADE FKs and unique constraint for many-to-many with metadata (added_at)
  - body_sections_format discriminator column for handling multiple content formats in the same table
observability_surfaces:
  - none
drill_down_paths:
  - .gsd/milestones/M014/slices/S03/tasks/T01-SUMMARY.md
  - .gsd/milestones/M014/slices/S03/tasks/T02-SUMMARY.md
duration: ""
verification_result: passed
completed_at: 2026-04-03T01:20:27.897Z
blocker_discovered: false
---

# S03: Data Model + Migration

**Added body_sections_format column, technique_page_videos association table, and wired both into the API response for multi-source technique pages.**

## What Happened

This slice laid the data foundation for M014's multi-source, nested-section technique pages. Two tasks delivered three changes:

**T01 — Schema + Migration:** Created Alembic migration 012 adding `body_sections_format` (VARCHAR(20), NOT NULL, default 'v1') to technique_pages and a new `technique_page_videos` association table with dual CASCADE foreign keys and a unique constraint on (technique_page_id, source_video_id). Updated SQLAlchemy models with the new `TechniquePageVideo` class and `body_sections_format` column on `TechniquePage`. Widened the Pydantic `body_sections` type from `dict | None` to `list | dict | None` to support both v1 (dict) and v2 (list-of-objects) formats. Added `SourceVideoSummary` schema and `source_videos` field to `TechniquePageDetail`.

**T02 — API Wiring:** Updated `get_technique()` to eagerly load `source_video_links` → `source_video` via chained `selectinload`. Builds the `source_videos` list from association table rows. Ran migration on ub01 and verified the API returns both new fields with correct defaults (`body_sections_format: "v1"`, `source_videos: []`).

All existing technique pages continue to work unchanged — the v1 default ensures backward compatibility.

## Verification

All slice-level checks passed:
1. `from models import TechniquePageVideo, TechniquePage; assert hasattr(TechniquePage, 'body_sections_format')` → OK
2. `from schemas import SourceVideoSummary, TechniquePageDetail` → OK
3. `alembic upgrade head` on ub01 Docker → clean (migration 012 applied)
4. `curl` to live API for existing technique → `body_sections_format: "v1"` and `source_videos: []` present in response

## Requirements Advanced

None.

## Requirements Validated

None.

## New Requirements Surfaced

None.

## Requirements Invalidated or Re-scoped

None.

## Deviations

None.

## Known Limitations

None.

## Follow-ups

None.

## Files Created/Modified

- `alembic/versions/012_multi_source_format.py` — New migration: body_sections_format column + technique_page_videos table
- `backend/models.py` — Added TechniquePageVideo model, body_sections_format column, source_video_links relationship
- `backend/schemas.py` — Widened body_sections type, added SourceVideoSummary, added source_videos to TechniquePageDetail
- `backend/routers/techniques.py` — Eager-load source_video_links, build source_videos list in technique detail response
