# 함수 설계서: `migrateV1ToV2` (#226) > **부모 설계서**: ./README.md · **상태**: Draft > **작성**: [AI] Architect (2026-06-12) · **구현**: `app/lib/data/db/app_database.dart` 의 `migration.onUpgrade` 인라인 또는 file-private top-level · **테스트**: `app/test/data/db/migration_v1_to_v2_test.dart` ## 1. 시그니처 ```dart Future _migrateV1ToV2(Migrator m, AppDatabase db) async { ... } ``` `MigrationStrategy.onUpgrade` 에서 dispatch: ```dart onUpgrade: (m, from, to) async { if (from == 1 && to >= 2) { await _migrateV1ToV2(m, this); // this = AppDatabase } // future: // if (from <= 2 && to >= 3) await _migrateV2ToV3(m, this); if (from > to || to > schemaVersion) { assert(false, 'Unknown upgrade path: from=$from to=$to (schemaVersion=$schemaVersion)'); } }, ``` ## 2. 책임 (단일 책임, 1줄) v1 DB 의 `protocols` 테이블을 v2 CHECK 제약으로 교체하고 시드 flag 클리어 — read-only catalog 의 첫 마이그레이션 패턴. ## 3. 입력 | 파라미터 | 타입 | 제약 | 설명 | |---|---|---|---| | `m` | `Migrator` | drift 의 schema migrator | DDL API | | `db` | `AppDatabase` | non-null | metaKv 클리어용 | ## 4. 출력 - **반환**: `Future`. - **부수효과**: - `protocols` 테이블 DROP + CREATE (CHECK 제약 7 카테고리로) + 인덱스 `IDX_protocols_category` 재생성. - `meta_kv` 에서 `kSeededV1Flag` row DELETE. - 다음 부팅 시 `SeedImporter.importIfNeeded()` 가 재시드 트리거. - **user 테이블 (Habits, Phases, TrackerEntries, ...) 무변화**. ## 5. 동작 / 알고리즘 ``` 1. await m.deleteTable(db.protocols); # SQLite: DROP TABLE protocols # 인덱스도 자동 cascade drop. 2. await m.createTable(db.protocols); # v2 schema 로 CREATE TABLE protocols ( # id TEXT PRIMARY KEY, # category TEXT CHECK (category IN ( # 'light_circadian','sleep','movement','nutrition', # 'focus_cognition','recovery_stress','emotion_relationship' # )) NOT NULL, # title TEXT NOT NULL, # ... # ); 3. await m.createIndex(Index( 'IDX_protocols_category', 'CREATE INDEX IDX_protocols_category ON protocols(category)', )); # drop 시 자동 cascade 됐어도 명시적 재생성. 4. await (db.delete(db.metaKv) ..where((t) => t.key.equals(kSeededV1Flag))) .go(); # 시드 flag 1 row 삭제. 다음 부팅이 importIfNeeded() 호출 → 새 JSON 으로 reseed. ``` ## 6. 에러 & 실패 모드 | 조건 | 처리 | 반환/예외 | |---|---|---| | DROP 실패 (이론상 없음, 사용자 락) | drift 가 transaction 롤백 → 부팅 실패. 사용자에겐 명시적 에러. | SqliteException 전파 | | CREATE 실패 (이론상 없음) | 동상 | SqliteException 전파 | | metaKv 삭제 실패 (이론상 없음) | 동상 — but **여기까지 도달 시 protocols 테이블은 v2 형태**. 다음 부팅 시 flag 가 'true' 인 채라 reseed 안 함 → protocols 빈 상태. **위험.** 그래서 metaKv 삭제는 트랜잭션 내 마지막 단계가 아니라 순서가 중요. | drift onUpgrade 전체가 트랜잭션 — drop/create 와 metaKv 삭제 같이 묶임. | | 다음 부팅 시 seed JSON 손상 | SeedImporter 가 CHECK 위배 throw → 부팅 실패. dev 단말 1대 영향이라 수용. | FormatException / SqliteException | ## 7. 엣지케이스 - **신규 설치 (`onCreate`)** — 본 함수 호출 0. createAll 이 v2 schema 그대로 적용 후 seed 가 v2 JSON 로드. 정상. - **v1 → v3+ 점프 (이론상 없음, 현재 schemaVersion=2)** — `from=1, to=3` 이면 v1→v2 → v2→v3 순차 실행 가정. `_migrateV2ToV3` 가 아직 없어 dispatch 가 발견 못 함 → assert false. v3 도입 시점에 명시. - **트랜잭션 중단** — drift 의 onUpgrade 는 db.transaction 안에서 실행. 부분 실패 시 자동 롤백 → 사용자 DB 는 v1 그대로. 다음 시도에서 재실행. - **사용자 데이터 보호** — 본 함수는 Protocols 만 건드림. Habits/TrackerEntries 등 user 테이블 0 영향. `migration_v1_to_v2_test.dart` 가 명시적 검증. - **인덱스 재생성 누락 시** — query latency ↓ 만 영향 (정상 동작). 본 함수가 명시적으로 createIndex 호출하므로 보호. ## 8. 복잡도 / 성능 - 시간: O(1) — DDL 4건. - 실측: < 50ms (dev 단말, drift 의 transaction overhead 포함). - 호출 빈도: **dev 단말 평생 1회** (v1 → v2 한 번). 사용자 신규 설치는 호출 0. ## 9. 의존성 - drift `Migrator` API (deleteTable, createTable, createIndex). - `kSeededV1Flag` 상수 (`core/constants.dart`). - AppDatabase 의 `protocols` getter (스키마 가져오기). ## 10. 테스트 케이스 - [ ] **smoke**: in-memory DB 를 v1 schema 로 raw SQL 로 생성 → `_migrateV1ToV2(m, db)` 호출 → protocols 테이블의 CHECK 제약이 v2 7 카테고리인지 검증 (PRAGMA / sqlite_master 조회) - [ ] **flag 클리어**: 사전에 metaKv 에 `seeded_v1='true'` insert → migrate → metaKv 조회 시 row 없음 - [ ] **user 데이터 보호**: 사전에 Habits / Phases / TrackerEntries 에 row insert → migrate → 모두 그대로 - [ ] **v2 CHECK 위배 negative**: migrate 후 `INSERT INTO protocols (..., category='health', ...)` 시도 → SqliteException - [ ] **v2 CHECK 통과 positive**: `category='light_circadian'` insert → 성공 - [ ] **인덱스 존재**: migrate 후 `sqlite_master` 에서 `IDX_protocols_category` 발견 - [ ] **이중 호출 안전성**: 동일 DB 에 migrate 2회 호출 → 두 번째도 성공 (idempotent 가정. drift `deleteTable` 이 미존재 테이블에 graceful 인지 확인 필요 — OQ) - [ ] **integration with onUpgrade**: schemaVersion=2 인 AppDatabase 로 v1 DB 열기 → onUpgrade 자동 호출 → 정상 동작 ## 11. 추적성 - 인수조건: #226 AC-4 (8 카테고리 재분류 — DB CHECK 갱신 부분), AC-8 (user 테이블 무변화), AC-9 (마이그레이션 unit test). - 관련 ADR: **ADR-0004** (본 이슈에서 발행 — Catalog re-categorization + first schema migration policy).