[Architect] #204 design spec — Flutter bootstrap + 18 tables
Phase 1 설계서 작성 완료. docs/design/204-flutter-bootstrap/ 13 개 파일: - README.md (12 섹션 모두 채움, 함수 19 개 명세, AC 16 항) - 01-project-structure.md (feature-first + layer-first 하이브리드) - 02-drift-schema-catalog.md (Catalog 7 테이블 Dart 정의) - 03-drift-schema-user.md (User 11 테이블 + R1~R10 강제 매트릭스) - 04-migrations.md (schemaVersion v1 + 인덱스 17 개) - 05-seed-data.md (assets/seed/*.json + first-run import) - 06-ux-contracts.md (체크인 R8 ≤ 60 초 흐름) - fn-recommend-variant / fn-compute-streak / fn-weekly-minimum-ratio - fn-validate-frame-level / fn-active-habit-quota / fn-seed-importer 핵심 결정: dose_variants 는 별도 habit_dose_variant 테이블로 정규화 (FK 무결성 + recommendVariant SQL 단순성). ADR-0002 승격 권장. Refs #204 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
157
docs/design/204-flutter-bootstrap/04-migrations.md
Normal file
157
docs/design/204-flutter-bootstrap/04-migrations.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 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<void> _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.
|
||||
Reference in New Issue
Block a user