/// SQLite repository implementations backed by [SchedulerDb]. library; import 'dart:convert'; import 'package:drift/drift.dart' as drift; import 'package:scheduler_core/scheduler_core.dart' as core; import 'package:scheduler_persistence/persistence.dart'; import 'scheduler_db.dart'; const _snapshotRetention = Duration(days: 30); final _epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); /// Drift-backed task repository. final class SqliteTaskRepository implements TaskRepository { /// Creates a task repository using [db]. SqliteTaskRepository(this.db); /// Shared SQLite database handle. final SchedulerDb db; @override Future> findById({ required core.OwnerId ownerId, required String taskId, }) async { final row = await _taskById(taskId); if (row == null || row.ownerId != ownerId.value) { return const Right(null); } return Right(_taskFromRow(row)); } @override Future>> findByOwner({ required core.OwnerId ownerId, core.PageRequest page = const core.PageRequest(), }) async { return Right(await _pageTaskRows( page: page, where: (table) => table.ownerId.equals(ownerId.value), )); } @override Future>> findByParentTaskId({ required core.OwnerId ownerId, required String parentTaskId, core.PageRequest page = const core.PageRequest(), }) async { return Right(await _pageTaskRows( page: page, where: (table) => table.ownerId.equals(ownerId.value) & table.parentId.equals(parentTaskId), )); } @override Future>> findByProjectId({ required core.OwnerId ownerId, required String projectId, core.PageRequest page = const core.PageRequest(), }) async { return Right(await _pageTaskRows( page: page, where: (table) => table.ownerId.equals(ownerId.value) & table.projectId.equals(projectId), )); } @override Future>> findByStatus({ required core.OwnerId ownerId, required core.TaskStatus status, core.PageRequest page = const core.PageRequest(), }) async { return Right(await _pageTaskRows( page: page, where: (table) => table.ownerId.equals(ownerId.value) & table.status.equals(core.PersistenceEnumMapping.encodeTaskStatus( status, )), )); } @override Future> insert({ required core.OwnerId ownerId, required core.Task task, }) async { if (await _taskById(task.id) != null) { return Left(RepositoryDuplicateId(entityId: task.id)); } await db.into(db.tasks).insert(_taskCompanion( task, ownerId: ownerId, revision: core.Revision.initial, )); return const Right(core.Revision.initial); } @override Future> save({ required core.OwnerId ownerId, required core.Task task, required core.Revision expectedRevision, }) async { final checked = await _checkTaskWrite( task.id, ownerId, expectedRevision, ); if (checked is Left) { return Left(checked.value); } final nextRevision = checked.right.revision.next(); final updates = _taskCompanion( task, ownerId: ownerId, revision: nextRevision, ); final updated = await (db.update(db.tasks) ..where( (table) => table.id.equals(task.id) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(updates); if (updated != 1) { return Left(await _staleTaskFailure(task.id, expectedRevision)); } return Right(nextRevision); } @override Future> delete({ required core.OwnerId ownerId, required String taskId, required core.Revision expectedRevision, }) async { final checked = await _checkTaskWrite(taskId, ownerId, expectedRevision); if (checked is Left) { return Left(checked.value); } final nextRevision = checked.right.revision.next(); final deleted = await (db.delete(db.tasks) ..where( (table) => table.id.equals(taskId) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .go(); if (deleted != 1) { return Left(await _staleTaskFailure(taskId, expectedRevision)); } return Right(nextRevision); } Future> _pageTaskRows({ required core.PageRequest page, required drift.Expression Function($TasksTable table) where, }) async { final offset = _cursorOffset(page.cursor); final query = db.select(db.tasks) ..where(where) ..orderBy([(table) => drift.OrderingTerm.asc(table.id)]) ..limit(page.limit + 1, offset: offset); final rows = await query.get(); final pageRows = rows.take(page.limit).toList(growable: false); return core.Page( items: pageRows.map(_taskFromRow), nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null, ); } Future _taskById(String id) { return (db.select(db.tasks)..where((table) => table.id.equals(id))) .getSingleOrNull(); } Future> _checkTaskWrite( String id, core.OwnerId ownerId, core.Revision expectedRevision, ) async { if (expectedRevision.value <= 0) { return Left(RepositoryInvalidRevision( entityId: id, expectedRevision: expectedRevision, )); } final row = await _taskById(id); if (row == null) { return Left(RepositoryNotFound(entityId: id)); } if (row.ownerId != ownerId.value) { return Left(RepositoryOwnerMismatch(entityId: id)); } if (row.revision != expectedRevision.value) { return Left(RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: core.Revision(row.revision), )); } return Right(row); } Future _staleTaskFailure( String id, core.Revision expectedRevision, ) async { final row = await _taskById(id); return RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: row == null ? expectedRevision.next() : core.Revision(row.revision), ); } } /// Drift-backed project repository. final class SqliteProjectRepository implements ProjectRepository { /// Creates a project repository using [db]. SqliteProjectRepository(this.db); /// Shared SQLite database handle. final SchedulerDb db; @override Future> findById({ required core.OwnerId ownerId, required String projectId, }) async { final row = await _projectById(projectId); if (row == null || row.ownerId != ownerId.value) { return const Right(null); } return Right(_projectFromRow(row)); } @override Future>> findByOwner({ required core.OwnerId ownerId, bool includeArchived = false, core.PageRequest page = const core.PageRequest(), }) async { final offset = _cursorOffset(page.cursor); final query = db.select(db.projects) ..where((table) { final ownerClause = table.ownerId.equals(ownerId.value); return includeArchived ? ownerClause : ownerClause & table.archivedAtUtc.isNull(); }) ..orderBy([(table) => drift.OrderingTerm.asc(table.id)]) ..limit(page.limit + 1, offset: offset); final rows = await query.get(); final pageRows = rows.take(page.limit).toList(growable: false); return Right(core.Page( items: pageRows.map(_projectFromRow), nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null, )); } @override Future> insert({ required core.OwnerId ownerId, required core.ProjectProfile project, }) async { if (await _projectById(project.id) != null) { return Left(RepositoryDuplicateId(entityId: project.id)); } await db.into(db.projects).insert(_projectCompanion( project, ownerId: ownerId, revision: core.Revision.initial, createdAt: _epoch, updatedAt: _epoch, )); return const Right(core.Revision.initial); } @override Future> save({ required core.OwnerId ownerId, required core.ProjectProfile project, required core.Revision expectedRevision, }) async { final checked = await _checkProjectWrite( project.id, ownerId, expectedRevision, ); if (checked is Left) { return Left(checked.value); } final row = checked.right; final nextRevision = core.Revision(row.revision).next(); final updated = await (db.update(db.projects) ..where( (table) => table.id.equals(project.id) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(_projectCompanion( project, ownerId: ownerId, revision: nextRevision, createdAt: row.createdAtUtc, updatedAt: _epoch, )); if (updated != 1) { return Left(await _staleProjectFailure(project.id, expectedRevision)); } return Right(nextRevision); } @override Future> archive({ required core.OwnerId ownerId, required String projectId, required core.Revision expectedRevision, required DateTime archivedAtUtc, }) async { final checked = await _checkProjectWrite( projectId, ownerId, expectedRevision, ); if (checked is Left) { return Left(checked.value); } final row = checked.right; final nextRevision = core.Revision(row.revision).next(); final archived = _projectFromRow(row).copyWith(archivedAt: archivedAtUtc); final updated = await (db.update(db.projects) ..where( (table) => table.id.equals(projectId) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(_projectCompanion( archived, ownerId: ownerId, revision: nextRevision, createdAt: row.createdAtUtc, updatedAt: archivedAtUtc, )); if (updated != 1) { return Left(await _staleProjectFailure(projectId, expectedRevision)); } return Right(nextRevision); } Future _projectById(String id) { return (db.select(db.projects)..where((table) => table.id.equals(id))) .getSingleOrNull(); } Future> _checkProjectWrite( String id, core.OwnerId ownerId, core.Revision expectedRevision, ) async { if (expectedRevision.value <= 0) { return Left(RepositoryInvalidRevision( entityId: id, expectedRevision: expectedRevision, )); } final row = await _projectById(id); if (row == null) { return Left(RepositoryNotFound(entityId: id)); } if (row.ownerId != ownerId.value) { return Left(RepositoryOwnerMismatch(entityId: id)); } if (row.revision != expectedRevision.value) { return Left(RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: core.Revision(row.revision), )); } return Right(row); } Future _staleProjectFailure( String id, core.Revision expectedRevision, ) async { final row = await _projectById(id); return RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: row == null ? expectedRevision.next() : core.Revision(row.revision), ); } } /// Drift-backed locked-block and override repository. final class SqliteLockedBlockRepository implements LockedBlockRepository { /// Creates a locked-time repository using [db]. SqliteLockedBlockRepository(this.db); /// Shared SQLite database handle. final SchedulerDb db; @override Future> findBlockById({ required core.OwnerId ownerId, required String blockId, }) async { final row = await _blockById(blockId); if (row == null || row.ownerId != ownerId.value) { return const Right(null); } return Right(_lockedBlockFromRow(row)); } @override Future>> findBlocksByOwner({ required core.OwnerId ownerId, bool includeArchived = false, core.PageRequest page = const core.PageRequest(), }) async { final offset = _cursorOffset(page.cursor); final query = db.select(db.lockedBlocks) ..where((table) { final ownerClause = table.ownerId.equals(ownerId.value); return includeArchived ? ownerClause : ownerClause & table.archivedAtUtc.isNull(); }) ..orderBy([(table) => drift.OrderingTerm.asc(table.id)]) ..limit(page.limit + 1, offset: offset); final rows = await query.get(); final pageRows = rows.take(page.limit).toList(growable: false); return Right(core.Page( items: pageRows.map(_lockedBlockFromRow), nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null, )); } @override Future> findOverrideById({ required core.OwnerId ownerId, required String overrideId, }) async { final row = await _overrideById(overrideId); if (row == null || row.ownerId != ownerId.value) { return const Right(null); } return Right(_lockedOverrideFromRow(row)); } @override Future>> findOverridesForDate({ required core.OwnerId ownerId, required core.CivilDate date, core.PageRequest page = const core.PageRequest(), }) async { final offset = _cursorOffset(page.cursor); final query = db.select(db.lockedOverrides) ..where( (table) => table.ownerId.equals(ownerId.value) & table.date.equals(date.toIsoString()), ) ..orderBy([(table) => drift.OrderingTerm.asc(table.id)]) ..limit(page.limit + 1, offset: offset); final rows = await query.get(); final pageRows = rows.take(page.limit).toList(growable: false); return Right(core.Page( items: pageRows.map(_lockedOverrideFromRow), nextCursor: rows.length > page.limit ? '${offset + page.limit}' : null, )); } @override Future> insertBlock({ required core.OwnerId ownerId, required core.LockedBlock block, }) async { if (await _blockById(block.id) != null) { return Left(RepositoryDuplicateId(entityId: block.id)); } await db.into(db.lockedBlocks).insert(_lockedBlockCompanion( block, ownerId: ownerId, revision: core.Revision.initial, )); return const Right(core.Revision.initial); } @override Future> saveBlock({ required core.OwnerId ownerId, required core.LockedBlock block, required core.Revision expectedRevision, }) async { final checked = await _checkBlockWrite( block.id, ownerId, expectedRevision, ); if (checked is Left) { return Left(checked.value); } final nextRevision = core.Revision(checked.right.revision).next(); final updated = await (db.update(db.lockedBlocks) ..where( (table) => table.id.equals(block.id) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(_lockedBlockCompanion( block, ownerId: ownerId, revision: nextRevision, )); if (updated != 1) { return Left(await _staleBlockFailure(block.id, expectedRevision)); } return Right(nextRevision); } @override Future> archiveBlock({ required core.OwnerId ownerId, required String blockId, required core.Revision expectedRevision, required DateTime archivedAtUtc, }) async { final checked = await _checkBlockWrite(blockId, ownerId, expectedRevision); if (checked is Left) { return Left(checked.value); } final row = checked.right; final nextRevision = core.Revision(row.revision).next(); final archived = _lockedBlockFromRow(row).copyWith(archivedAt: archivedAtUtc); final updated = await (db.update(db.lockedBlocks) ..where( (table) => table.id.equals(blockId) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(_lockedBlockCompanion( archived, ownerId: ownerId, revision: nextRevision, )); if (updated != 1) { return Left(await _staleBlockFailure(blockId, expectedRevision)); } return Right(nextRevision); } @override Future> insertOverride({ required core.OwnerId ownerId, required core.LockedBlockOverride override, }) async { if (await _overrideById(override.id) != null) { return Left(RepositoryDuplicateId(entityId: override.id)); } await db.into(db.lockedOverrides).insert(_lockedOverrideCompanion( override, ownerId: ownerId, revision: core.Revision.initial, )); return const Right(core.Revision.initial); } @override Future> saveOverride({ required core.OwnerId ownerId, required core.LockedBlockOverride override, required core.Revision expectedRevision, }) async { final checked = await _checkOverrideWrite( override.id, ownerId, expectedRevision, ); if (checked is Left) { return Left(checked.value); } final nextRevision = core.Revision(checked.right.revision).next(); final updated = await (db.update(db.lockedOverrides) ..where( (table) => table.id.equals(override.id) & table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(_lockedOverrideCompanion( override, ownerId: ownerId, revision: nextRevision, )); if (updated != 1) { return Left(await _staleOverrideFailure( override.id, expectedRevision, )); } return Right(nextRevision); } Future _blockById(String id) { return (db.select(db.lockedBlocks)..where((table) => table.id.equals(id))) .getSingleOrNull(); } Future _overrideById(String id) { return (db.select(db.lockedOverrides) ..where((table) => table.id.equals(id))) .getSingleOrNull(); } Future> _checkBlockWrite( String id, core.OwnerId ownerId, core.Revision expectedRevision, ) async { if (expectedRevision.value <= 0) { return Left(RepositoryInvalidRevision( entityId: id, expectedRevision: expectedRevision, )); } final row = await _blockById(id); if (row == null) return Left(RepositoryNotFound(entityId: id)); if (row.ownerId != ownerId.value) { return Left(RepositoryOwnerMismatch(entityId: id)); } if (row.revision != expectedRevision.value) { return Left(RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: core.Revision(row.revision), )); } return Right(row); } Future> _checkOverrideWrite( String id, core.OwnerId ownerId, core.Revision expectedRevision, ) async { if (expectedRevision.value <= 0) { return Left(RepositoryInvalidRevision( entityId: id, expectedRevision: expectedRevision, )); } final row = await _overrideById(id); if (row == null) return Left(RepositoryNotFound(entityId: id)); if (row.ownerId != ownerId.value) { return Left(RepositoryOwnerMismatch(entityId: id)); } if (row.revision != expectedRevision.value) { return Left(RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: core.Revision(row.revision), )); } return Right(row); } Future _staleBlockFailure( String id, core.Revision expectedRevision, ) async { final row = await _blockById(id); return RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: row == null ? expectedRevision.next() : core.Revision(row.revision), ); } Future _staleOverrideFailure( String id, core.Revision expectedRevision, ) async { final row = await _overrideById(id); return RepositoryStaleRevision( entityId: id, expectedRevision: expectedRevision, actualRevision: row == null ? expectedRevision.next() : core.Revision(row.revision), ); } } /// Drift-backed owner settings repository. final class SqliteSettingsRepository implements SettingsRepository { /// Creates a settings repository using [db]. SqliteSettingsRepository(this.db); /// Shared SQLite database handle. final SchedulerDb db; @override Future> findByOwner({ required core.OwnerId ownerId, }) async { final row = await _settingsByOwner(ownerId.value); if (row == null) return const Right(null); return Right(_settingsFromRow(row)); } @override Future> insert({ required core.OwnerId ownerId, required core.OwnerSettings settings, }) async { if (await _settingsByOwner(ownerId.value) != null) { return Left(RepositoryDuplicateId(entityId: ownerId.value)); } final ownedSettings = settings.ownerId == ownerId.value ? settings : settings.copyWith(ownerId: ownerId.value); await db.into(db.settingsTable).insert(_settingsCompanion( ownedSettings, revision: core.Revision.initial, createdAt: _epoch, updatedAt: _epoch, )); return const Right(core.Revision.initial); } @override Future> save({ required core.OwnerId ownerId, required core.OwnerSettings settings, required core.Revision expectedRevision, }) async { final checked = await _checkSettingsWrite(ownerId, expectedRevision); if (checked is Left) { return Left(checked.value); } final row = checked.right; final nextRevision = core.Revision(row.revision).next(); final ownedSettings = settings.ownerId == ownerId.value ? settings : settings.copyWith(ownerId: ownerId.value); final updated = await (db.update(db.settingsTable) ..where( (table) => table.ownerId.equals(ownerId.value) & table.revision.equals(expectedRevision.value), )) .write(_settingsCompanion( ownedSettings, revision: nextRevision, createdAt: row.createdAtUtc, updatedAt: _epoch, )); if (updated != 1) { return Left(await _staleSettingsFailure(ownerId, expectedRevision)); } return Right(nextRevision); } Future _settingsByOwner(String ownerId) { return (db.select(db.settingsTable) ..where((table) => table.ownerId.equals(ownerId))) .getSingleOrNull(); } Future> _checkSettingsWrite( core.OwnerId ownerId, core.Revision expectedRevision, ) async { if (expectedRevision.value <= 0) { return Left(RepositoryInvalidRevision( entityId: ownerId.value, expectedRevision: expectedRevision, )); } final row = await _settingsByOwner(ownerId.value); if (row == null) return Left(RepositoryNotFound(entityId: ownerId.value)); if (row.revision != expectedRevision.value) { return Left(RepositoryStaleRevision( entityId: ownerId.value, expectedRevision: expectedRevision, actualRevision: core.Revision(row.revision), )); } return Right(row); } Future _staleSettingsFailure( core.OwnerId ownerId, core.Revision expectedRevision, ) async { final row = await _settingsByOwner(ownerId.value); return RepositoryStaleRevision( entityId: ownerId.value, expectedRevision: expectedRevision, actualRevision: row == null ? expectedRevision.next() : core.Revision(row.revision), ); } } /// Drift-backed schedule snapshot repository. final class SqliteScheduleSnapshotRepository implements ScheduleSnapshotRepository { /// Creates a snapshot repository using [db]. SqliteScheduleSnapshotRepository(this.db); /// Shared SQLite database handle. final SchedulerDb db; @override Future> findById({ required core.OwnerId ownerId, required String snapshotId, }) async { final row = await _snapshotById(snapshotId); if (row == null || row.ownerId != ownerId.value) { return const Right(null); } return Right(_snapshotFromRow(row)); } @override Future>> findInWindow({ required core.OwnerId ownerId, required core.SchedulingWindow window, core.PageRequest page = const core.PageRequest(), }) async { final rows = await (db.select(db.snapshots) ..where((table) => table.ownerId.equals(ownerId.value)) ..orderBy([(table) => drift.OrderingTerm.asc(table.id)])) .get(); final matching = rows .map(_snapshotFromRow) .where((snapshot) => _windowsOverlap(snapshot.window, window)) .toList(growable: false); final offset = _cursorOffset(page.cursor); final pageItems = matching.skip(offset).take(page.limit).toList(); final nextOffset = offset + page.limit; return Right(core.Page( items: pageItems, nextCursor: nextOffset < matching.length ? '$nextOffset' : null, )); } @override Future> insert({ required core.OwnerId ownerId, required core.SchedulingStateSnapshot snapshot, }) async { if (await _snapshotById(snapshot.id) != null) { return Left(RepositoryDuplicateId(entityId: snapshot.id)); } await db.into(db.snapshots).insert(_snapshotCompanion( snapshot, ownerId: ownerId, revision: core.Revision.initial, )); return const Right(core.Revision.initial); } @override Future> deleteExpired({ required core.OwnerId ownerId, required DateTime nowUtc, }) async { final deleted = await (db.delete(db.snapshots) ..where( (table) => table.ownerId.equals(ownerId.value) & table.retentionExpiresUtc.isSmallerThanValue(nowUtc.toUtc()), )) .go(); return Right(deleted); } Future _snapshotById(String id) { return (db.select(db.snapshots)..where((table) => table.id.equals(id))) .getSingleOrNull(); } } TasksCompanion _taskCompanion( core.Task task, { required core.OwnerId ownerId, required core.Revision revision, }) { final doc = core.TaskDocumentMapping.toDocument( task, ownerId: ownerId.value, revision: revision.value, ); return TasksCompanion.insert( id: task.id, ownerId: ownerId.value, title: _string(doc, core.TaskDocumentFields.title), projectId: _string(doc, core.TaskDocumentFields.projectId), parentId: drift.Value(_nullableString( doc, core.TaskDocumentFields.parentTaskId, )), type: _string(doc, core.TaskDocumentFields.type), status: _string(doc, core.TaskDocumentFields.status), priority: drift.Value(_nullableString( doc, core.TaskDocumentFields.priority, )), reward: _string(doc, core.TaskDocumentFields.reward), difficulty: _string(doc, core.TaskDocumentFields.difficulty), durationMinutes: drift.Value(_nullableInt( doc, core.TaskDocumentFields.durationMinutes, )), scheduledStartUtc: drift.Value(_nullableDateTime( doc, core.TaskDocumentFields.scheduledStart, )), scheduledEndUtc: drift.Value(_nullableDateTime( doc, core.TaskDocumentFields.scheduledEnd, )), actualStartUtc: drift.Value(_nullableDateTime( doc, core.TaskDocumentFields.actualStart, )), actualEndUtc: drift.Value(_nullableDateTime( doc, core.TaskDocumentFields.actualEnd, )), completedAtUtc: drift.Value(_nullableDateTime( doc, core.TaskDocumentFields.completedAt, )), backlogTagsJson: jsonEncode(doc[core.TaskDocumentFields.backlogTags]), reminderOverride: drift.Value(_nullableString( doc, core.TaskDocumentFields.reminderOverride, )), statsJson: jsonEncode(doc[core.TaskDocumentFields.stats]), backlogEnteredAtUtc: drift.Value(_nullableDateTime( doc, core.TaskDocumentFields.backlogEnteredAt, )), backlogEnteredProvenance: drift.Value(_nullableString( doc, core.TaskDocumentFields.backlogEnteredAtProvenance, )), revision: revision.value, createdAtUtc: task.createdAt.toUtc(), updatedAtUtc: task.updatedAt.toUtc(), ); } core.Task _taskFromRow(TaskRow row) { return core.TaskDocumentMapping.fromDocument({ ..._commonDocumentFields( row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc), core.TaskDocumentFields.title: row.title, core.TaskDocumentFields.projectId: row.projectId, core.TaskDocumentFields.type: row.type, core.TaskDocumentFields.status: row.status, core.TaskDocumentFields.priority: row.priority, core.TaskDocumentFields.reward: row.reward, core.TaskDocumentFields.difficulty: row.difficulty, core.TaskDocumentFields.durationMinutes: row.durationMinutes, core.TaskDocumentFields.scheduledStart: _storedDateTimeOrNull(row.scheduledStartUtc), core.TaskDocumentFields.scheduledEnd: _storedDateTimeOrNull(row.scheduledEndUtc), core.TaskDocumentFields.actualStart: _storedDateTimeOrNull(row.actualStartUtc), core.TaskDocumentFields.actualEnd: _storedDateTimeOrNull(row.actualEndUtc), core.TaskDocumentFields.completedAt: _storedDateTimeOrNull(row.completedAtUtc), core.TaskDocumentFields.parentTaskId: row.parentId, core.TaskDocumentFields.backlogTags: _jsonList(row.backlogTagsJson), core.TaskDocumentFields.reminderOverride: row.reminderOverride, core.TaskDocumentFields.stats: _jsonMap(row.statsJson), core.TaskDocumentFields.backlogEnteredAt: _storedDateTimeOrNull(row.backlogEnteredAtUtc), core.TaskDocumentFields.backlogEnteredAtProvenance: row.backlogEnteredProvenance, }); } ProjectsCompanion _projectCompanion( core.ProjectProfile project, { required core.OwnerId ownerId, required core.Revision revision, required DateTime createdAt, required DateTime updatedAt, }) { final doc = core.ProjectDocumentMapping.toDocument( project, ownerId: ownerId.value, revision: revision.value, createdAt: createdAt, updatedAt: updatedAt, ); return ProjectsCompanion.insert( id: project.id, ownerId: ownerId.value, name: _string(doc, core.ProjectDocumentFields.name), colorKey: _string(doc, core.ProjectDocumentFields.colorKey), defaultPriority: _string(doc, core.ProjectDocumentFields.defaultPriority), defaultReward: _string(doc, core.ProjectDocumentFields.defaultReward), defaultDifficulty: _string(doc, core.ProjectDocumentFields.defaultDifficulty), defaultReminderProfile: _string(doc, core.ProjectDocumentFields.defaultReminderProfile), defaultDurationMinutes: drift.Value(_nullableInt( doc, core.ProjectDocumentFields.defaultDurationMinutes, )), archivedAtUtc: drift.Value(_nullableDateTime( doc, core.ProjectDocumentFields.archivedAt, )), revision: revision.value, createdAtUtc: createdAt.toUtc(), updatedAtUtc: updatedAt.toUtc(), ); } core.ProjectProfile _projectFromRow(ProjectRow row) { return core.ProjectDocumentMapping.fromDocument({ ..._commonDocumentFields( row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc), core.ProjectDocumentFields.name: row.name, core.ProjectDocumentFields.colorKey: row.colorKey, core.ProjectDocumentFields.defaultPriority: row.defaultPriority, core.ProjectDocumentFields.defaultReward: row.defaultReward, core.ProjectDocumentFields.defaultDifficulty: row.defaultDifficulty, core.ProjectDocumentFields.defaultReminderProfile: row.defaultReminderProfile, core.ProjectDocumentFields.defaultDurationMinutes: row.defaultDurationMinutes, core.ProjectDocumentFields.archivedAt: _storedDateTimeOrNull( row.archivedAtUtc, ), }); } LockedBlocksCompanion _lockedBlockCompanion( core.LockedBlock block, { required core.OwnerId ownerId, required core.Revision revision, }) { final doc = core.LockedBlockDocumentMapping.toDocument( block, ownerId: ownerId.value, revision: revision.value, ); return LockedBlocksCompanion.insert( id: block.id, ownerId: ownerId.value, name: _string(doc, core.LockedBlockDocumentFields.name), date: drift.Value(_nullableString(doc, core.LockedBlockDocumentFields.date)), startTime: _string(doc, core.LockedBlockDocumentFields.startTime), endTime: _string(doc, core.LockedBlockDocumentFields.endTime), recurrenceJson: drift.Value(_jsonEncodeNullable( doc[core.LockedBlockDocumentFields.recurrence], )), hiddenByDefault: _bool(doc, core.LockedBlockDocumentFields.hiddenByDefault), projectId: drift.Value(_nullableString( doc, core.LockedBlockDocumentFields.projectId, )), archivedAtUtc: drift.Value(_nullableDateTime( doc, core.LockedBlockDocumentFields.archivedAt, )), revision: revision.value, createdAtUtc: block.createdAt.toUtc(), updatedAtUtc: block.updatedAt.toUtc(), ); } core.LockedBlock _lockedBlockFromRow(LockedBlockRow row) { return core.LockedBlockDocumentMapping.fromDocument({ ..._commonDocumentFields( row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc), core.LockedBlockDocumentFields.name: row.name, core.LockedBlockDocumentFields.startTime: row.startTime, core.LockedBlockDocumentFields.endTime: row.endTime, core.LockedBlockDocumentFields.date: row.date, core.LockedBlockDocumentFields.recurrence: row.recurrenceJson == null ? null : _jsonMap(row.recurrenceJson!), core.LockedBlockDocumentFields.hiddenByDefault: row.hiddenByDefault, core.LockedBlockDocumentFields.projectId: row.projectId, core.LockedBlockDocumentFields.archivedAt: _storedDateTimeOrNull(row.archivedAtUtc), }); } LockedOverridesCompanion _lockedOverrideCompanion( core.LockedBlockOverride override, { required core.OwnerId ownerId, required core.Revision revision, }) { final doc = core.LockedBlockOverrideDocumentMapping.toDocument( override, ownerId: ownerId.value, revision: revision.value, ); return LockedOverridesCompanion.insert( id: override.id, ownerId: ownerId.value, lockedBlockId: drift.Value(_nullableString( doc, core.LockedBlockOverrideDocumentFields.lockedBlockId, )), date: _string(doc, core.LockedBlockOverrideDocumentFields.date), type: _string(doc, core.LockedBlockOverrideDocumentFields.type), name: drift.Value( _nullableString(doc, core.LockedBlockOverrideDocumentFields.name), ), startTime: drift.Value(_nullableString( doc, core.LockedBlockOverrideDocumentFields.startTime, )), endTime: drift.Value(_nullableString( doc, core.LockedBlockOverrideDocumentFields.endTime, )), hiddenByDefault: _bool(doc, core.LockedBlockOverrideDocumentFields.hiddenByDefault), projectId: drift.Value(_nullableString( doc, core.LockedBlockOverrideDocumentFields.projectId, )), revision: revision.value, createdAtUtc: override.createdAt.toUtc(), updatedAtUtc: override.updatedAt.toUtc(), ); } core.LockedBlockOverride _lockedOverrideFromRow(LockedOverrideRow row) { return core.LockedBlockOverrideDocumentMapping.fromDocument({ ..._commonDocumentFields( row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc), core.LockedBlockOverrideDocumentFields.lockedBlockId: row.lockedBlockId, core.LockedBlockOverrideDocumentFields.date: row.date, core.LockedBlockOverrideDocumentFields.type: row.type, core.LockedBlockOverrideDocumentFields.name: row.name, core.LockedBlockOverrideDocumentFields.startTime: row.startTime, core.LockedBlockOverrideDocumentFields.endTime: row.endTime, core.LockedBlockOverrideDocumentFields.hiddenByDefault: row.hiddenByDefault, core.LockedBlockOverrideDocumentFields.projectId: row.projectId, }); } SettingsTableCompanion _settingsCompanion( core.OwnerSettings settings, { required core.Revision revision, required DateTime createdAt, required DateTime updatedAt, }) { final doc = core.OwnerSettingsDocumentMapping.toDocument( settings, revision: revision.value, createdAt: createdAt, updatedAt: updatedAt, ); return SettingsTableCompanion.insert( ownerId: settings.ownerId, timeZoneId: _string(doc, core.OwnerSettingsDocumentFields.timeZoneId), dayStartMinutes: settings.dayStart.minutesSinceMidnight, dayEndMinutes: settings.dayEnd.minutesSinceMidnight, compactMode: _bool(doc, core.OwnerSettingsDocumentFields.compactModeEnabled), backlogStalenessJson: jsonEncode(doc[core.OwnerSettingsDocumentFields.backlogStaleness]), revision: revision.value, createdAtUtc: createdAt.toUtc(), updatedAtUtc: updatedAt.toUtc(), ); } core.OwnerSettings _settingsFromRow(SettingsRow row) { return core.OwnerSettingsDocumentMapping.fromDocument({ ..._commonDocumentFields(row.ownerId, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc), core.OwnerSettingsDocumentFields.timeZoneId: row.timeZoneId, core.OwnerSettingsDocumentFields.dayStart: _wallTimeFromMinutes(row.dayStartMinutes).toIsoString(), core.OwnerSettingsDocumentFields.dayEnd: _wallTimeFromMinutes(row.dayEndMinutes).toIsoString(), core.OwnerSettingsDocumentFields.compactModeEnabled: row.compactMode, core.OwnerSettingsDocumentFields.backlogStaleness: _jsonMap(row.backlogStalenessJson), }); } SnapshotsCompanion _snapshotCompanion( core.SchedulingStateSnapshot snapshot, { required core.OwnerId ownerId, required core.Revision revision, }) { final retentionExpiresAt = snapshot.capturedAt.add(_snapshotRetention); final doc = core.SchedulingSnapshotDocumentMapping.toDocument( snapshot, ownerId: ownerId.value, revision: revision.value, retentionExpiresAt: retentionExpiresAt, ); return SnapshotsCompanion.insert( id: snapshot.id, ownerId: ownerId.value, capturedAtUtc: snapshot.capturedAt.toUtc(), operationName: drift.Value(_nullableString( doc, core.SchedulingSnapshotDocumentFields.operationName, )), sourceDate: drift.Value(_nullableString( doc, core.SchedulingSnapshotDocumentFields.sourceDate, )), targetDate: drift.Value(_nullableString( doc, core.SchedulingSnapshotDocumentFields.targetDate, )), windowJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.window]), tasksJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.tasks]), lockedIntervalsJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.lockedIntervals]), requiredVisibleIntervalsJson: jsonEncode( doc[core.SchedulingSnapshotDocumentFields.requiredVisibleIntervals], ), noticesJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.notices]), changesJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.changes]), overlapsJson: jsonEncode(doc[core.SchedulingSnapshotDocumentFields.overlaps]), retentionExpiresUtc: retentionExpiresAt.toUtc(), truncated: _bool(doc, core.SchedulingSnapshotDocumentFields.truncated), revision: revision.value, createdAtUtc: snapshot.capturedAt.toUtc(), updatedAtUtc: snapshot.capturedAt.toUtc(), ); } core.SchedulingStateSnapshot _snapshotFromRow(SnapshotRow row) { return core.SchedulingSnapshotDocumentMapping.fromDocument({ ..._commonDocumentFields( row.id, row.ownerId, row.revision, row.createdAtUtc, row.updatedAtUtc), core.SchedulingSnapshotDocumentFields.capturedAt: core.PersistenceDateTimeConvention.toStoredString(row.capturedAtUtc), core.SchedulingSnapshotDocumentFields.sourceDate: row.sourceDate, core.SchedulingSnapshotDocumentFields.targetDate: row.targetDate, core.SchedulingSnapshotDocumentFields.window: _jsonMap(row.windowJson), core.SchedulingSnapshotDocumentFields.tasks: _jsonList(row.tasksJson), core.SchedulingSnapshotDocumentFields.lockedIntervals: _jsonList(row.lockedIntervalsJson), core.SchedulingSnapshotDocumentFields.requiredVisibleIntervals: _jsonList(row.requiredVisibleIntervalsJson), core.SchedulingSnapshotDocumentFields.notices: _jsonList(row.noticesJson), core.SchedulingSnapshotDocumentFields.changes: _jsonList(row.changesJson), core.SchedulingSnapshotDocumentFields.overlaps: _jsonList(row.overlapsJson), core.SchedulingSnapshotDocumentFields.operationName: row.operationName, core.SchedulingSnapshotDocumentFields.retentionExpiresAt: core.PersistenceDateTimeConvention.toStoredString( row.retentionExpiresUtc, ), core.SchedulingSnapshotDocumentFields.truncated: row.truncated, }); } Map _commonDocumentFields( String id, String ownerId, int revision, DateTime createdAt, DateTime updatedAt, ) { return { core.DocumentFields.schemaVersion: core.v1SchemaVersion, core.DocumentFields.id: id, core.DocumentFields.ownerId: ownerId, core.DocumentFields.revision: revision, core.DocumentFields.createdAt: core.PersistenceDateTimeConvention.toStoredString(createdAt), core.DocumentFields.updatedAt: core.PersistenceDateTimeConvention.toStoredString(updatedAt), }; } String _string(Map doc, String fieldName) { return doc[fieldName] as String; } String? _nullableString(Map doc, String fieldName) { return doc[fieldName] as String?; } int? _nullableInt(Map doc, String fieldName) { return doc[fieldName] as int?; } bool _bool(Map doc, String fieldName) { return doc[fieldName] as bool; } DateTime? _nullableDateTime(Map doc, String fieldName) { final value = doc[fieldName] as String?; return value == null ? null : core.PersistenceDateTimeConvention.fromStoredString(value); } String? _storedDateTimeOrNull(DateTime? value) { return value == null ? null : core.PersistenceDateTimeConvention.toStoredString(value); } String? _jsonEncodeNullable(Object? value) { return value == null ? null : jsonEncode(value); } Map _jsonMap(String value) { return Map.from( jsonDecode(value) as Map, ); } List _jsonList(String value) { return List.from(jsonDecode(value) as List); } core.WallTime _wallTimeFromMinutes(int minutes) { return core.WallTime(hour: minutes ~/ 60, minute: minutes % 60); } int _cursorOffset(String? cursor) { if (cursor == null) return 0; final parsed = int.tryParse(cursor); if (parsed == null || parsed < 0) return 0; return parsed; } extension on int { core.Revision next() => core.Revision(this).next(); } bool _windowsOverlap(core.SchedulingWindow left, core.SchedulingWindow right) { return left.start.isBefore(right.end) && right.start.isBefore(left.end); }