- Drift 21 tables (8 catalog + 11 user + habit_dose_variants + meta_kv) with R1~R10 CHECK constraints and 19 indexes - 8 hand-crafted seed JSON catalogs in app/assets/seed/ (refs 84, protocols 34, methodologies 21, frame_patterns 30, reward_menu_items 30, break_protocols 8, common_frames 5, diet_patterns 5) - 6 domain functions: recommend_variant, compute_streak, validate_frame_level, active_habit_quota, weekly_minimum_ratio, seed_importer (transactional, idempotent) - 4 vertical-slice Riverpod screens: HabitList, HabitCreate, CheckIn, Streak - 31 unit tests passing; flutter analyze clean - OQ-5 streak semantics: missing entry ≠ explicit blank (missing = end of history; only TrackerValue.blank triggers Never-miss-twice) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
145 lines
4.8 KiB
Dart
145 lines
4.8 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import 'tables/catalog_tables.dart';
|
|
import 'tables/user_tables.dart';
|
|
|
|
part 'app_database.g.dart';
|
|
|
|
@DriftDatabase(tables: [
|
|
// Catalog 8
|
|
Protocols,
|
|
BreakProtocols,
|
|
CommonFrames,
|
|
Methodologies,
|
|
FramePatterns,
|
|
RewardMenuItems,
|
|
References,
|
|
DietPatterns,
|
|
// User 11 + 정규화 부속 1
|
|
Users,
|
|
Phases,
|
|
Habits,
|
|
HabitDoseVariants,
|
|
IfThenRules,
|
|
TrackerEntries,
|
|
LapseLogs,
|
|
UrgeLogs,
|
|
RewardDeclarations,
|
|
RewardClaims,
|
|
Reflections,
|
|
// Meta
|
|
MetaKv,
|
|
])
|
|
class AppDatabase extends _$AppDatabase {
|
|
AppDatabase(super.e);
|
|
|
|
/// In-memory for tests.
|
|
AppDatabase.memory() : super(NativeDatabase.memory());
|
|
|
|
@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 only has v1. Reaching here is a bug.
|
|
assert(false, 'Phase 1 has no upgrade path. from=$from to=$to');
|
|
},
|
|
);
|
|
|
|
Future<void> _createIndexes(Migrator m) async {
|
|
// Catalog indexes
|
|
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'));
|
|
await m.createIndex(Index(
|
|
'IDX_diet_patterns_evidence',
|
|
'CREATE INDEX IDX_diet_patterns_evidence '
|
|
'ON diet_patterns(evidence_strength)'));
|
|
await m.createIndex(Index(
|
|
'IDX_diet_patterns_kfit',
|
|
'CREATE INDEX IDX_diet_patterns_kfit '
|
|
'ON diet_patterns(korean_context_fit) '
|
|
'WHERE korean_context_fit IS NOT NULL'));
|
|
|
|
// User indexes
|
|
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)'));
|
|
}
|
|
}
|
|
|
|
Future<File> appDatabaseFile() async {
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
return File(p.join(dir.path, 'life_helper.sqlite'));
|
|
}
|