Files
life-helper/docs/design/226-catalog-gallery/fn-migration_v1_to_v2.md
joungmin 4665f06a94 [02-Architect] #226 design spec + ADR-0004
- docs/design/226-catalog-gallery/README.md — 12 changed files, DisplayCategory enum, sealed CatalogItem, 5 scenarios
- docs/design/226-catalog-gallery/fn-catalog_repository.md — 3-source unification algorithm + helpers
- docs/design/226-catalog-gallery/fn-migration_v1_to_v2.md — first schema migration (DROP + reseed pattern)
- docs/adr/0004-catalog-recategorization-and-first-migration.md — Catalog 재분류 + 첫 마이그레이션 정책

Refs #226
2026-06-12 16:59:31 +09:00

6.0 KiB

함수 설계서: migrateV1ToV2 (#226)

부모 설계서: ./README.md · 상태: Draft 작성: [AI] Architect (2026-06-12) · 구현: app/lib/data/db/app_database.dartmigration.onUpgrade 인라인 또는 file-private top-level · 테스트: app/test/data/db/migration_v1_to_v2_test.dart

1. 시그니처

Future<void> _migrateV1ToV2(Migrator m, AppDatabase db) async { ... }

MigrationStrategy.onUpgrade 에서 dispatch:

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<void>.
  • 부수효과:
    • 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).