# 04 — Drift 마이그레이션 전략 (#204) > 부모 설계서: [README.md](./README.md) ## 1. schemaVersion 정책 | version | 범위 | 작성자 | 비고 | |---------|------|--------|------| | **v1** | 18 테이블 + `habit_dose_variants` + `meta_kv` 초기 생성 | Architect (이 설계서) → Developer (구현) | Phase 1 종료 시점 = production v1 | | v2+ | TBD (Phase 2+) | 해당 시점 Architect | data-model.md §8 의 후속 엔티티 (habit_stack, notification_rule 등) | 규칙: - schema 가 바뀌면 **반드시 schemaVersion 증가** + `onUpgrade` 콜백 작성. - 컬럼 추가만은 가능하나, drop/rename 은 destructive — `m.alterTable(TableMigration(...))` 사용. - production user 가 0 명인 본 Phase (v1 미배포 상태) 에서만 destructive recreate 허용. v1 이 디바이스에 올라간 후엔 데이터 보존 마이그레이션 필수. ## 2. v1 진입점 (Drift `MigrationStrategy`) ```dart @DriftDatabase(tables: [ // catalog Protocols, BreakProtocols, CommonFrames, Methodologies, FramePatterns, RewardMenuItems, References, // user Users, Phases, Habits, HabitDoseVariants, IfThenRules, TrackerEntries, LapseLogs, UrgeLogs, RewardDeclarations, RewardClaims, Reflections, // meta MetaKv, ]) class AppDatabase extends _$AppDatabase { AppDatabase(QueryExecutor e) : super(e); @override int get schemaVersion => 1; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (m) async { await m.createAll(); await _createIndexes(m); }, onUpgrade: (m, from, to) async { // Phase 1 한정: v1 만 존재. 도달 불가 보장. assert(false, 'Phase 1 has no upgrade path. from=$from to=$to'); }, beforeOpen: (details) async { if (details.wasCreated) { // SeedImporter 는 main.dart 에서 별도 호출 (트랜잭션 분리 위해) } }, ); Future _createIndexes(Migrator m) async { // catalog await m.createIndex(Index('IDX_protocols_category', 'CREATE INDEX IDX_protocols_category ON protocols(category)')); await m.createIndex(Index('IDX_break_protocols_category', 'CREATE UNIQUE INDEX IDX_break_protocols_category ON break_protocols(category)')); await m.createIndex(Index('IDX_methodologies_core', 'CREATE INDEX IDX_methodologies_core ON methodologies(is_core_engine) ' 'WHERE is_core_engine = 1')); await m.createIndex(Index('IDX_frame_patterns_keyword', 'CREATE INDEX IDX_frame_patterns_keyword ON frame_patterns(avoidance_keyword)')); await m.createIndex(Index('IDX_reward_menu_tier', 'CREATE INDEX IDX_reward_menu_tier ON reward_menu_items(tier_recommended)')); await m.createIndex(Index('IDX_references_kind', 'CREATE INDEX IDX_references_kind ON "references"(kind)')); await m.createIndex(Index('IDX_references_doi', 'CREATE INDEX IDX_references_doi ON "references"(doi) WHERE doi IS NOT NULL')); // user await m.createIndex(Index('IDX_phases_user_status', 'CREATE INDEX IDX_phases_user_status ON phases(user_id, status)')); await m.createIndex(Index('IDX_habits_user_status_type', 'CREATE INDEX IDX_habits_user_status_type ON habits(user_id, status, type)')); await m.createIndex(Index('IDX_habits_phase', 'CREATE INDEX IDX_habits_phase ON habits(phase_id)')); await m.createIndex(Index('IDX_habit_dose_variants_habit', 'CREATE INDEX IDX_habit_dose_variants_habit ON habit_dose_variants(habit_id)')); await m.createIndex(Index('IDX_habit_dose_variants_habit_min', 'CREATE INDEX IDX_habit_dose_variants_habit_min ' 'ON habit_dose_variants(habit_id, is_minimum)')); await m.createIndex(Index('IDX_if_then_habit', 'CREATE INDEX IDX_if_then_habit ON if_then_rules(habit_id)')); await m.createIndex(Index('UQ_tracker_habit_date', 'CREATE UNIQUE INDEX UQ_tracker_habit_date ON tracker_entries(habit_id, date)')); await m.createIndex(Index('IDX_tracker_habit_date', 'CREATE INDEX IDX_tracker_habit_date ON tracker_entries(habit_id, date)')); await m.createIndex(Index('IDX_tracker_date', 'CREATE INDEX IDX_tracker_date ON tracker_entries(date)')); await m.createIndex(Index('IDX_lapse_habit_date', 'CREATE INDEX IDX_lapse_habit_date ON lapse_logs(habit_id, date)')); await m.createIndex(Index('IDX_urge_habit_occurred', 'CREATE INDEX IDX_urge_habit_occurred ON urge_logs(habit_id, occurred_at)')); await m.createIndex(Index('IDX_reflections_user_scope', 'CREATE INDEX IDX_reflections_user_scope ON reflections(user_id, scope)')); } } ``` > `references` 는 SQL reserved-ish (특히 일부 dialects). 본 SQLite 에선 안전하나 Drift 가 안전한 quoting 자동 처리. 직접 SQL 쓸 땐 따옴표 사용. ## 3. 향후 변경 패턴 (v1 → v2 가이드) ### 3.1 컬럼 추가 (안전, 권장) ```dart onUpgrade: (m, from, to) async { if (from < 2) { await m.addColumn(habits, habits.newColumnName); } } ``` ### 3.2 새 테이블 추가 (안전) ```dart if (from < 2) await m.createTable(newTable); ``` ### 3.3 컬럼 drop / rename (위험 — `TableMigration` 사용) ```dart if (from < 2) { await m.alterTable(TableMigration( habits, columnTransformer: { habits.oldCol: const Constant(null) }, newColumns: [habits.replacementCol], )); } ``` - 데이터 보존 마이그레이션은 Drift 의 `TableMigration` 으로 한 번에 처리. - 컬럼 의미 변경 (예: timezone 의미 변경) 은 마이그레이션 + 데이터 정규화 스크립트 별도. ### 3.4 enum 값 추가 (안전) - CHECK 제약 문자열 업데이트 = `alterTable` 안에서 다시 만들거나, Drift 의 enum CHECK 재생성. ### 3.5 enum 값 제거 / rename (위험) - 기존 데이터 변환 필요. 마이그레이션 + 별도 코드 경로. ## 4. 회귀 방지 — schema dump diff - v1 통과 시점에 `tool/schema/v1.json` 으로 Drift schema dump (`dart run drift_dev schema dump`). - CI 가 schema dump 와 v1.json 의 diff 를 비교 — schemaVersion 증가 없이 schema 가 변경되면 fail. ## 5. 본 Phase 의 운영 규칙 - v1 출시 전이므로 schema 변경은 자유롭게 가능. 단 **이 설계서를 먼저 수정한 뒤** 코드 변경 (Design-First, CLAUDE.md §2). - 첫 디바이스 배포 (Architect → QA → Designer → Release) 이후엔 schemaVersion 증가 + onUpgrade 명시 필수. ## 6. 도구 - `dart run drift_dev schema dump lib/data/db/app_database.dart tool/schema/` - `dart run drift_dev schema generate tool/schema/ test/generated_migrations/` — 마이그레이션 step 검증용 generated test.