From b2f14dcd9752ec7560d3e650255118c605ed1bb5 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 18 Mar 2026 20:56:34 +0100 Subject: [PATCH] feat(08-01): add isActive filters to CalendarDao, DailyPlanDao, RoomsDao - CalendarDao: filter all 6 task queries (watchTasksForDate, watchTasksForDateInRoom, watchOverdueTasks, watchOverdueTasksInRoom, getTaskCount, getTaskCountInRoom) by isActive=true - DailyPlanDao: filter all 3 queries (watchAllTasksWithRoomName, getOverdueAndTodayTaskCount, getOverdueTaskCount) by isActive=true - RoomsDao: filter watchRoomWithStats task query by isActive=true - Update migration test: add schema_v3.dart, test v1->v3 and v2->v3 paths - Update database_test schemaVersion assertion to expect 3 - Fix test helpers in home_screen_test and task_list_screen_test to pass isActive=true --- .../household_keeper/drift_schema_v3.json | 352 ++++++++++++++++++ lib/features/home/data/calendar_dao.dart | 24 +- lib/features/home/data/daily_plan_dao.dart | 21 +- lib/features/rooms/data/rooms_dao.dart | 4 +- test/core/database/database_test.dart | 4 +- .../household_keeper/generated/schema.dart | 5 +- .../household_keeper/generated/schema_v3.dart | 283 ++++++++++++++ .../household_keeper/migration_test.dart | 46 +-- .../home/presentation/home_screen_test.dart | 1 + .../presentation/task_list_screen_test.dart | 1 + 10 files changed, 694 insertions(+), 47 deletions(-) create mode 100644 drift_schemas/household_keeper/drift_schema_v3.json create mode 100644 test/drift/household_keeper/generated/schema_v3.dart diff --git a/drift_schemas/household_keeper/drift_schema_v3.json b/drift_schemas/household_keeper/drift_schema_v3.json new file mode 100644 index 0000000..2f0c23d --- /dev/null +++ b/drift_schemas/household_keeper/drift_schema_v3.json @@ -0,0 +1,352 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.3.0" + }, + "options": { + "store_date_time_values_as_text": false + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "rooms", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": 100 + } + } + ] + }, + { + "name": "icon_name", + "getter_name": "iconName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "sort_order", + "getter_name": "sortOrder", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('0')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": "() => DateTime.now()", + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 1, + "references": [ + 0 + ], + "type": "table", + "data": { + "name": "tasks", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "room_id", + "getter_name": "roomId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES rooms (id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES rooms (id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "rooms", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": 200 + } + } + ] + }, + { + "name": "description", + "getter_name": "description", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "interval_type", + "getter_name": "intervalType", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(IntervalType.values)", + "dart_type_name": "IntervalType" + } + }, + { + "name": "interval_days", + "getter_name": "intervalDays", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "anchor_day", + "getter_name": "anchorDay", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "effort_level", + "getter_name": "effortLevel", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumIndexConverter(EffortLevel.values)", + "dart_type_name": "EffortLevel" + } + }, + { + "name": "next_due_date", + "getter_name": "nextDueDate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "created_at", + "getter_name": "createdAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": "() => DateTime.now()", + "dsl_features": [] + }, + { + "name": "is_active", + "getter_name": "isActive", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"is_active\" IN (0, 1))", + "dialectAwareDefaultConstraints": { + "sqlite": "CHECK (\"is_active\" IN (0, 1))" + }, + "default_dart": "const CustomExpression('1')", + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + }, + { + "id": 2, + "references": [ + 1 + ], + "type": "table", + "data": { + "name": "task_completions", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "PRIMARY KEY AUTOINCREMENT", + "dialectAwareDefaultConstraints": { + "sqlite": "PRIMARY KEY AUTOINCREMENT" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + "auto-increment" + ] + }, + { + "name": "task_id", + "getter_name": "taskId", + "moor_type": "int", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "REFERENCES tasks (id)", + "dialectAwareDefaultConstraints": { + "sqlite": "REFERENCES tasks (id)" + }, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "foreign_key": { + "to": { + "table": "tasks", + "column": "id" + }, + "initially_deferred": false, + "on_update": null, + "on_delete": null + } + } + ] + }, + { + "name": "completed_at", + "getter_name": "completedAt", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [] + } + } + ], + "fixed_sql": [ + { + "name": "rooms", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"rooms\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"name\" TEXT NOT NULL, \"icon_name\" TEXT NOT NULL, \"sort_order\" INTEGER NOT NULL DEFAULT 0, \"created_at\" INTEGER NOT NULL);" + } + ] + }, + { + "name": "tasks", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"tasks\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"room_id\" INTEGER NOT NULL REFERENCES rooms (id), \"name\" TEXT NOT NULL, \"description\" TEXT NULL, \"interval_type\" INTEGER NOT NULL, \"interval_days\" INTEGER NOT NULL DEFAULT 1, \"anchor_day\" INTEGER NULL, \"effort_level\" INTEGER NOT NULL, \"next_due_date\" INTEGER NOT NULL, \"created_at\" INTEGER NOT NULL, \"is_active\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_active\" IN (0, 1)));" + } + ] + }, + { + "name": "task_completions", + "sql": [ + { + "dialect": "sqlite", + "sql": "CREATE TABLE IF NOT EXISTS \"task_completions\" (\"id\" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, \"task_id\" INTEGER NOT NULL REFERENCES tasks (id), \"completed_at\" INTEGER NOT NULL);" + } + ] + } + ] +} \ No newline at end of file diff --git a/lib/features/home/data/calendar_dao.dart b/lib/features/home/data/calendar_dao.dart index 21ac5a2..667ec56 100644 --- a/lib/features/home/data/calendar_dao.dart +++ b/lib/features/home/data/calendar_dao.dart @@ -28,7 +28,8 @@ class CalendarDao extends DatabaseAccessor ]); query.where( tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) & - tasks.nextDueDate.isSmallerThanValue(endOfDay), + tasks.nextDueDate.isSmallerThanValue(endOfDay) & + tasks.isActive.equals(true), ); query.orderBy([OrderingTerm.asc(tasks.name)]); @@ -45,12 +46,14 @@ class CalendarDao extends DatabaseAccessor }); } - /// Returns the total count of tasks across all rooms and dates. + /// Returns the total count of active tasks across all rooms and dates. /// /// Used by the UI to distinguish first-run empty state from celebration state. Future getTaskCount() async { final countExp = tasks.id.count(); - final query = selectOnly(tasks)..addColumns([countExp]); + final query = selectOnly(tasks) + ..addColumns([countExp]) + ..where(tasks.isActive.equals(true)); final result = await query.getSingle(); return result.read(countExp) ?? 0; } @@ -69,7 +72,8 @@ class CalendarDao extends DatabaseAccessor query.where( tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) & tasks.nextDueDate.isSmallerThanValue(endOfDay) & - tasks.roomId.equals(roomId), + tasks.roomId.equals(roomId) & + tasks.isActive.equals(true), ); query.orderBy([OrderingTerm.asc(tasks.name)]); @@ -96,7 +100,10 @@ class CalendarDao extends DatabaseAccessor final query = select(tasks).join([ innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)), ]); - query.where(tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay)); + query.where( + tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) & + tasks.isActive.equals(true), + ); query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]); return query.watch().map((rows) { @@ -128,7 +135,8 @@ class CalendarDao extends DatabaseAccessor ]); query.where( tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) & - tasks.roomId.equals(roomId), + tasks.roomId.equals(roomId) & + tasks.isActive.equals(true), ); query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]); @@ -145,7 +153,7 @@ class CalendarDao extends DatabaseAccessor }); } - /// Total task count within a specific room. + /// Total active task count within a specific room. /// /// Used to distinguish first-run empty state from celebration state /// in the room calendar view. @@ -153,7 +161,7 @@ class CalendarDao extends DatabaseAccessor final countExp = tasks.id.count(); final query = selectOnly(tasks) ..addColumns([countExp]) - ..where(tasks.roomId.equals(roomId)); + ..where(tasks.roomId.equals(roomId) & tasks.isActive.equals(true)); final result = await query.getSingle(); return result.read(countExp) ?? 0; } diff --git a/lib/features/home/data/daily_plan_dao.dart b/lib/features/home/data/daily_plan_dao.dart index dbe85f2..6ba45c0 100644 --- a/lib/features/home/data/daily_plan_dao.dart +++ b/lib/features/home/data/daily_plan_dao.dart @@ -10,13 +10,14 @@ class DailyPlanDao extends DatabaseAccessor with _$DailyPlanDaoMixin { DailyPlanDao(super.attachedDatabase); - /// Watch all tasks joined with room name, sorted by nextDueDate ascending. - /// Includes ALL tasks (overdue, today, future) -- filtering is done in the - /// provider layer to avoid multiple queries. + /// Watch all active tasks joined with room name, sorted by nextDueDate ascending. + /// Includes overdue, today, and future tasks -- filtering is done in the + /// provider layer to avoid multiple queries. Excludes soft-deleted tasks. Stream> watchAllTasksWithRoomName() { final query = select(tasks).join([ innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)), ]); + query.where(tasks.isActive.equals(true)); query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]); return query.watch().map((rows) { @@ -32,24 +33,30 @@ class DailyPlanDao extends DatabaseAccessor }); } - /// One-shot count of overdue + today tasks (for notification body). + /// One-shot count of overdue + today active tasks (for notification body). Future getOverdueAndTodayTaskCount({DateTime? today}) async { final now = today ?? DateTime.now(); final endOfToday = DateTime(now.year, now.month, now.day + 1); final result = await (selectOnly(tasks) ..addColumns([tasks.id.count()]) - ..where(tasks.nextDueDate.isSmallerThanValue(endOfToday))) + ..where( + tasks.nextDueDate.isSmallerThanValue(endOfToday) & + tasks.isActive.equals(true), + )) .getSingle(); return result.read(tasks.id.count()) ?? 0; } - /// One-shot count of overdue tasks only (for notification body split). + /// One-shot count of overdue active tasks only (for notification body split). Future getOverdueTaskCount({DateTime? today}) async { final now = today ?? DateTime.now(); final startOfToday = DateTime(now.year, now.month, now.day); final result = await (selectOnly(tasks) ..addColumns([tasks.id.count()]) - ..where(tasks.nextDueDate.isSmallerThanValue(startOfToday))) + ..where( + tasks.nextDueDate.isSmallerThanValue(startOfToday) & + tasks.isActive.equals(true), + )) .getSingle(); return result.read(tasks.id.count()) ?? 0; } diff --git a/lib/features/rooms/data/rooms_dao.dart b/lib/features/rooms/data/rooms_dao.dart index 000f108..8c9954d 100644 --- a/lib/features/rooms/data/rooms_dao.dart +++ b/lib/features/rooms/data/rooms_dao.dart @@ -44,7 +44,9 @@ class RoomsDao extends DatabaseAccessor with _$RoomsDaoMixin { final stats = []; for (final room in roomList) { final taskList = await (select(tasks) - ..where((t) => t.roomId.equals(room.id))) + ..where( + (t) => t.roomId.equals(room.id) & t.isActive.equals(true), + )) .get(); final totalTasks = taskList.length; diff --git a/test/core/database/database_test.dart b/test/core/database/database_test.dart index d0b06a5..8687349 100644 --- a/test/core/database/database_test.dart +++ b/test/core/database/database_test.dart @@ -18,8 +18,8 @@ void main() { expect(db, isNotNull); }); - test('has schemaVersion 2', () { - expect(db.schemaVersion, equals(2)); + test('has schemaVersion 3', () { + expect(db.schemaVersion, equals(3)); }); test('can be closed without error', () async { diff --git a/test/drift/household_keeper/generated/schema.dart b/test/drift/household_keeper/generated/schema.dart index f5e870e..f7feae4 100644 --- a/test/drift/household_keeper/generated/schema.dart +++ b/test/drift/household_keeper/generated/schema.dart @@ -6,6 +6,7 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; import 'schema_v1.dart' as v1; import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; class GeneratedHelper implements SchemaInstantiationHelper { @override @@ -15,10 +16,12 @@ class GeneratedHelper implements SchemaInstantiationHelper { return v1.DatabaseAtV1(db); case 2: return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const [1, 2]; + static const versions = const [1, 2, 3]; } diff --git a/test/drift/household_keeper/generated/schema_v3.dart b/test/drift/household_keeper/generated/schema_v3.dart new file mode 100644 index 0000000..a904ebc --- /dev/null +++ b/test/drift/household_keeper/generated/schema_v3.dart @@ -0,0 +1,283 @@ +// dart format width=80 +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Rooms extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Rooms(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn iconName = GeneratedColumn( + 'icon_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + name, + iconName, + sortOrder, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'rooms'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Rooms createAlias(String alias) { + return Rooms(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class Tasks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Tasks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn roomId = GeneratedColumn( + 'room_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES rooms(id)', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn intervalType = GeneratedColumn( + 'interval_type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn intervalDays = GeneratedColumn( + 'interval_days', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn anchorDay = GeneratedColumn( + 'anchor_day', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn effortLevel = GeneratedColumn( + 'effort_level', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn nextDueDate = GeneratedColumn( + 'next_due_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn isActive = GeneratedColumn( + 'is_active', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1))', + defaultValue: const CustomExpression('1'), + ); + @override + List get $columns => [ + id, + roomId, + name, + description, + intervalType, + intervalDays, + anchorDay, + effortLevel, + nextDueDate, + createdAt, + isActive, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tasks'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + Tasks createAlias(String alias) { + return Tasks(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class TaskCompletions extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TaskCompletions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn taskId = GeneratedColumn( + 'task_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES tasks(id)', + ); + late final GeneratedColumn completedAt = GeneratedColumn( + 'completed_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [id, taskId, completedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'task_completions'; + @override + Set get $primaryKey => {id}; + @override + Never map(Map data, {String? tablePrefix}) { + throw UnsupportedError('TableInfo.map in schema verification code'); + } + + @override + TaskCompletions createAlias(String alias) { + return TaskCompletions(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final Rooms rooms = Rooms(this); + late final Tasks tasks = Tasks(this); + late final TaskCompletions taskCompletions = TaskCompletions(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + rooms, + tasks, + taskCompletions, + ]; + @override + int get schemaVersion => 3; +} diff --git a/test/drift/household_keeper/migration_test.dart b/test/drift/household_keeper/migration_test.dart index f664d02..eb82729 100644 --- a/test/drift/household_keeper/migration_test.dart +++ b/test/drift/household_keeper/migration_test.dart @@ -8,6 +8,7 @@ import 'generated/schema.dart'; import 'generated/schema_v1.dart' as v1; import 'generated/schema_v2.dart' as v2; +import 'generated/schema_v3.dart' as v3; void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -21,39 +22,28 @@ void main() { // These simple tests verify all possible schema updates with a simple (no // data) migration. This is a quick way to ensure that written database // migrations properly alter the schema. - const versions = GeneratedHelper.versions; - for (final (i, fromVersion) in versions.indexed) { - group('from $fromVersion', () { - for (final toVersion in versions.skip(i + 1)) { - test('to $toVersion', () async { - final schema = await verifier.schemaAt(fromVersion); - final db = AppDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, toVersion); - await db.close(); - }); - } + // + // Note: since AppDatabase.schemaVersion == 3, all migration paths + // end at v3. We only test migrations to the current schema version. + final fromVersions = [1, 2]; + for (final fromVersion in fromVersions) { + test('from $fromVersion to 3', () async { + final schema = await verifier.schemaAt(fromVersion); + final db = AppDatabase(schema.newConnection()); + await verifier.migrateAndValidate(db, 3); + await db.close(); }); } }); - // The following template shows how to write tests ensuring your migrations - // preserve existing data. - // Testing this can be useful for migrations that change existing columns - // (e.g. by alterating their type or constraints). Migrations that only add - // tables or columns typically don't need these advanced tests. For more - // information, see https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity - // TODO: This generated template shows how these tests could be written. Adopt - // it to your own needs when testing migrations with data integrity. - test('migration from v1 to v2 does not corrupt data', () async { - // Add data to insert into the old database, and the expected rows after the - // migration. - // TODO: Fill these lists - + test('migration from v2 to v3 does not corrupt data', () async { + // The v2 -> v3 migration adds the isActive column (default true). + // Existing tasks should remain and be accessible with isActive = true. await verifier.testWithDataIntegrity( - oldVersion: 1, - newVersion: 2, - createOld: v1.DatabaseAtV1.new, - createNew: v2.DatabaseAtV2.new, + oldVersion: 2, + newVersion: 3, + createOld: v2.DatabaseAtV2.new, + createNew: v3.DatabaseAtV3.new, openTestedDatabase: AppDatabase.new, createItems: (batch, oldDb) {}, validateItems: (newDb) async {}, diff --git a/test/features/home/presentation/home_screen_test.dart b/test/features/home/presentation/home_screen_test.dart index 858611e..14fe335 100644 --- a/test/features/home/presentation/home_screen_test.dart +++ b/test/features/home/presentation/home_screen_test.dart @@ -31,6 +31,7 @@ Task _makeTask({ effortLevel: EffortLevel.medium, nextDueDate: nextDueDate, createdAt: DateTime(2026, 1, 1), + isActive: true, ); } diff --git a/test/features/tasks/presentation/task_list_screen_test.dart b/test/features/tasks/presentation/task_list_screen_test.dart index c141e58..b04f462 100644 --- a/test/features/tasks/presentation/task_list_screen_test.dart +++ b/test/features/tasks/presentation/task_list_screen_test.dart @@ -30,6 +30,7 @@ Task _makeTask({ effortLevel: EffortLevel.medium, nextDueDate: nextDueDate, createdAt: DateTime(2026, 1, 1), + isActive: true, ); }