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
This commit is contained in:
352
drift_schemas/household_keeper/drift_schema_v3.json
Normal file
352
drift_schemas/household_keeper/drift_schema_v3.json
Normal file
@@ -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>(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>(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);"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -28,7 +28,8 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
]);
|
]);
|
||||||
query.where(
|
query.where(
|
||||||
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
|
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
|
||||||
tasks.nextDueDate.isSmallerThanValue(endOfDay),
|
tasks.nextDueDate.isSmallerThanValue(endOfDay) &
|
||||||
|
tasks.isActive.equals(true),
|
||||||
);
|
);
|
||||||
query.orderBy([OrderingTerm.asc(tasks.name)]);
|
query.orderBy([OrderingTerm.asc(tasks.name)]);
|
||||||
|
|
||||||
@@ -45,12 +46,14 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
/// Used by the UI to distinguish first-run empty state from celebration state.
|
||||||
Future<int> getTaskCount() async {
|
Future<int> getTaskCount() async {
|
||||||
final countExp = tasks.id.count();
|
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();
|
final result = await query.getSingle();
|
||||||
return result.read(countExp) ?? 0;
|
return result.read(countExp) ?? 0;
|
||||||
}
|
}
|
||||||
@@ -69,7 +72,8 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
query.where(
|
query.where(
|
||||||
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
|
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
|
||||||
tasks.nextDueDate.isSmallerThanValue(endOfDay) &
|
tasks.nextDueDate.isSmallerThanValue(endOfDay) &
|
||||||
tasks.roomId.equals(roomId),
|
tasks.roomId.equals(roomId) &
|
||||||
|
tasks.isActive.equals(true),
|
||||||
);
|
);
|
||||||
query.orderBy([OrderingTerm.asc(tasks.name)]);
|
query.orderBy([OrderingTerm.asc(tasks.name)]);
|
||||||
|
|
||||||
@@ -96,7 +100,10 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
final query = select(tasks).join([
|
final query = select(tasks).join([
|
||||||
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
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)]);
|
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
||||||
|
|
||||||
return query.watch().map((rows) {
|
return query.watch().map((rows) {
|
||||||
@@ -128,7 +135,8 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
]);
|
]);
|
||||||
query.where(
|
query.where(
|
||||||
tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) &
|
tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) &
|
||||||
tasks.roomId.equals(roomId),
|
tasks.roomId.equals(roomId) &
|
||||||
|
tasks.isActive.equals(true),
|
||||||
);
|
);
|
||||||
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
||||||
|
|
||||||
@@ -145,7 +153,7 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
/// Used to distinguish first-run empty state from celebration state
|
||||||
/// in the room calendar view.
|
/// in the room calendar view.
|
||||||
@@ -153,7 +161,7 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
|||||||
final countExp = tasks.id.count();
|
final countExp = tasks.id.count();
|
||||||
final query = selectOnly(tasks)
|
final query = selectOnly(tasks)
|
||||||
..addColumns([countExp])
|
..addColumns([countExp])
|
||||||
..where(tasks.roomId.equals(roomId));
|
..where(tasks.roomId.equals(roomId) & tasks.isActive.equals(true));
|
||||||
final result = await query.getSingle();
|
final result = await query.getSingle();
|
||||||
return result.read(countExp) ?? 0;
|
return result.read(countExp) ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ class DailyPlanDao extends DatabaseAccessor<AppDatabase>
|
|||||||
with _$DailyPlanDaoMixin {
|
with _$DailyPlanDaoMixin {
|
||||||
DailyPlanDao(super.attachedDatabase);
|
DailyPlanDao(super.attachedDatabase);
|
||||||
|
|
||||||
/// Watch all tasks joined with room name, sorted by nextDueDate ascending.
|
/// Watch all active tasks joined with room name, sorted by nextDueDate ascending.
|
||||||
/// Includes ALL tasks (overdue, today, future) -- filtering is done in the
|
/// Includes overdue, today, and future tasks -- filtering is done in the
|
||||||
/// provider layer to avoid multiple queries.
|
/// provider layer to avoid multiple queries. Excludes soft-deleted tasks.
|
||||||
Stream<List<TaskWithRoom>> watchAllTasksWithRoomName() {
|
Stream<List<TaskWithRoom>> watchAllTasksWithRoomName() {
|
||||||
final query = select(tasks).join([
|
final query = select(tasks).join([
|
||||||
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
||||||
]);
|
]);
|
||||||
|
query.where(tasks.isActive.equals(true));
|
||||||
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
||||||
|
|
||||||
return query.watch().map((rows) {
|
return query.watch().map((rows) {
|
||||||
@@ -32,24 +33,30 @@ class DailyPlanDao extends DatabaseAccessor<AppDatabase>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One-shot count of overdue + today tasks (for notification body).
|
/// One-shot count of overdue + today active tasks (for notification body).
|
||||||
Future<int> getOverdueAndTodayTaskCount({DateTime? today}) async {
|
Future<int> getOverdueAndTodayTaskCount({DateTime? today}) async {
|
||||||
final now = today ?? DateTime.now();
|
final now = today ?? DateTime.now();
|
||||||
final endOfToday = DateTime(now.year, now.month, now.day + 1);
|
final endOfToday = DateTime(now.year, now.month, now.day + 1);
|
||||||
final result = await (selectOnly(tasks)
|
final result = await (selectOnly(tasks)
|
||||||
..addColumns([tasks.id.count()])
|
..addColumns([tasks.id.count()])
|
||||||
..where(tasks.nextDueDate.isSmallerThanValue(endOfToday)))
|
..where(
|
||||||
|
tasks.nextDueDate.isSmallerThanValue(endOfToday) &
|
||||||
|
tasks.isActive.equals(true),
|
||||||
|
))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
return result.read(tasks.id.count()) ?? 0;
|
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<int> getOverdueTaskCount({DateTime? today}) async {
|
Future<int> getOverdueTaskCount({DateTime? today}) async {
|
||||||
final now = today ?? DateTime.now();
|
final now = today ?? DateTime.now();
|
||||||
final startOfToday = DateTime(now.year, now.month, now.day);
|
final startOfToday = DateTime(now.year, now.month, now.day);
|
||||||
final result = await (selectOnly(tasks)
|
final result = await (selectOnly(tasks)
|
||||||
..addColumns([tasks.id.count()])
|
..addColumns([tasks.id.count()])
|
||||||
..where(tasks.nextDueDate.isSmallerThanValue(startOfToday)))
|
..where(
|
||||||
|
tasks.nextDueDate.isSmallerThanValue(startOfToday) &
|
||||||
|
tasks.isActive.equals(true),
|
||||||
|
))
|
||||||
.getSingle();
|
.getSingle();
|
||||||
return result.read(tasks.id.count()) ?? 0;
|
return result.read(tasks.id.count()) ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class RoomsDao extends DatabaseAccessor<AppDatabase> with _$RoomsDaoMixin {
|
|||||||
final stats = <RoomWithStats>[];
|
final stats = <RoomWithStats>[];
|
||||||
for (final room in roomList) {
|
for (final room in roomList) {
|
||||||
final taskList = await (select(tasks)
|
final taskList = await (select(tasks)
|
||||||
..where((t) => t.roomId.equals(room.id)))
|
..where(
|
||||||
|
(t) => t.roomId.equals(room.id) & t.isActive.equals(true),
|
||||||
|
))
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
final totalTasks = taskList.length;
|
final totalTasks = taskList.length;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ void main() {
|
|||||||
expect(db, isNotNull);
|
expect(db, isNotNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('has schemaVersion 2', () {
|
test('has schemaVersion 3', () {
|
||||||
expect(db.schemaVersion, equals(2));
|
expect(db.schemaVersion, equals(3));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can be closed without error', () async {
|
test('can be closed without error', () async {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
|
import 'schema_v3.dart' as v3;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -15,10 +16,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v1.DatabaseAtV1(db);
|
return v1.DatabaseAtV1(db);
|
||||||
case 2:
|
case 2:
|
||||||
return v2.DatabaseAtV2(db);
|
return v2.DatabaseAtV2(db);
|
||||||
|
case 3:
|
||||||
|
return v3.DatabaseAtV3(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2];
|
static const versions = const [1, 2, 3];
|
||||||
}
|
}
|
||||||
|
|||||||
283
test/drift/household_keeper/generated/schema_v3.dart
Normal file
283
test/drift/household_keeper/generated/schema_v3.dart
Normal file
@@ -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<int> id = GeneratedColumn<int>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||||
|
'name',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<String> iconName = GeneratedColumn<String>(
|
||||||
|
'icon_name',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> sortOrder = GeneratedColumn<int>(
|
||||||
|
'sort_order',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT 0',
|
||||||
|
defaultValue: const CustomExpression('0'),
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> createdAt = GeneratedColumn<int>(
|
||||||
|
'created_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
Never map(Map<String, dynamic> 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<int> id = GeneratedColumn<int>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> roomId = GeneratedColumn<int>(
|
||||||
|
'room_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL REFERENCES rooms(id)',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||||
|
'name',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<String> description = GeneratedColumn<String>(
|
||||||
|
'description',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> intervalType = GeneratedColumn<int>(
|
||||||
|
'interval_type',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> intervalDays = GeneratedColumn<int>(
|
||||||
|
'interval_days',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL DEFAULT 1',
|
||||||
|
defaultValue: const CustomExpression('1'),
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> anchorDay = GeneratedColumn<int>(
|
||||||
|
'anchor_day',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> effortLevel = GeneratedColumn<int>(
|
||||||
|
'effort_level',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> nextDueDate = GeneratedColumn<int>(
|
||||||
|
'next_due_date',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> createdAt = GeneratedColumn<int>(
|
||||||
|
'created_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> isActive = GeneratedColumn<int>(
|
||||||
|
'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<GeneratedColumn> 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<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
Never map(Map<String, dynamic> 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<int> id = GeneratedColumn<int>(
|
||||||
|
'id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
hasAutoIncrement: true,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
$customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> taskId = GeneratedColumn<int>(
|
||||||
|
'task_id',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL REFERENCES tasks(id)',
|
||||||
|
);
|
||||||
|
late final GeneratedColumn<int> completedAt = GeneratedColumn<int>(
|
||||||
|
'completed_at',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.int,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
$customConstraints: 'NOT NULL',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
List<GeneratedColumn> get $columns => [id, taskId, completedAt];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'task_completions';
|
||||||
|
@override
|
||||||
|
Set<GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
Never map(Map<String, dynamic> 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<TableInfo<Table, Object?>> get allTables =>
|
||||||
|
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||||
|
@override
|
||||||
|
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||||
|
rooms,
|
||||||
|
tasks,
|
||||||
|
taskCompletions,
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
int get schemaVersion => 3;
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import 'generated/schema.dart';
|
|||||||
|
|
||||||
import 'generated/schema_v1.dart' as v1;
|
import 'generated/schema_v1.dart' as v1;
|
||||||
import 'generated/schema_v2.dart' as v2;
|
import 'generated/schema_v2.dart' as v2;
|
||||||
|
import 'generated/schema_v3.dart' as v3;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
driftRuntimeOptions.dontWarnAboutMultipleDatabases = true;
|
||||||
@@ -21,39 +22,28 @@ void main() {
|
|||||||
// These simple tests verify all possible schema updates with a simple (no
|
// These simple tests verify all possible schema updates with a simple (no
|
||||||
// data) migration. This is a quick way to ensure that written database
|
// data) migration. This is a quick way to ensure that written database
|
||||||
// migrations properly alter the schema.
|
// migrations properly alter the schema.
|
||||||
const versions = GeneratedHelper.versions;
|
//
|
||||||
for (final (i, fromVersion) in versions.indexed) {
|
// Note: since AppDatabase.schemaVersion == 3, all migration paths
|
||||||
group('from $fromVersion', () {
|
// end at v3. We only test migrations to the current schema version.
|
||||||
for (final toVersion in versions.skip(i + 1)) {
|
final fromVersions = [1, 2];
|
||||||
test('to $toVersion', () async {
|
for (final fromVersion in fromVersions) {
|
||||||
final schema = await verifier.schemaAt(fromVersion);
|
test('from $fromVersion to 3', () async {
|
||||||
final db = AppDatabase(schema.newConnection());
|
final schema = await verifier.schemaAt(fromVersion);
|
||||||
await verifier.migrateAndValidate(db, toVersion);
|
final db = AppDatabase(schema.newConnection());
|
||||||
await db.close();
|
await verifier.migrateAndValidate(db, 3);
|
||||||
});
|
await db.close();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// The following template shows how to write tests ensuring your migrations
|
test('migration from v2 to v3 does not corrupt data', () async {
|
||||||
// preserve existing data.
|
// The v2 -> v3 migration adds the isActive column (default true).
|
||||||
// Testing this can be useful for migrations that change existing columns
|
// Existing tasks should remain and be accessible with isActive = true.
|
||||||
// (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
|
|
||||||
|
|
||||||
await verifier.testWithDataIntegrity(
|
await verifier.testWithDataIntegrity(
|
||||||
oldVersion: 1,
|
oldVersion: 2,
|
||||||
newVersion: 2,
|
newVersion: 3,
|
||||||
createOld: v1.DatabaseAtV1.new,
|
createOld: v2.DatabaseAtV2.new,
|
||||||
createNew: v2.DatabaseAtV2.new,
|
createNew: v3.DatabaseAtV3.new,
|
||||||
openTestedDatabase: AppDatabase.new,
|
openTestedDatabase: AppDatabase.new,
|
||||||
createItems: (batch, oldDb) {},
|
createItems: (batch, oldDb) {},
|
||||||
validateItems: (newDb) async {},
|
validateItems: (newDb) async {},
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ Task _makeTask({
|
|||||||
effortLevel: EffortLevel.medium,
|
effortLevel: EffortLevel.medium,
|
||||||
nextDueDate: nextDueDate,
|
nextDueDate: nextDueDate,
|
||||||
createdAt: DateTime(2026, 1, 1),
|
createdAt: DateTime(2026, 1, 1),
|
||||||
|
isActive: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Task _makeTask({
|
|||||||
effortLevel: EffortLevel.medium,
|
effortLevel: EffortLevel.medium,
|
||||||
nextDueDate: nextDueDate,
|
nextDueDate: nextDueDate,
|
||||||
createdAt: DateTime(2026, 1, 1),
|
createdAt: DateTime(2026, 1, 1),
|
||||||
|
isActive: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user