import 'dart:io'; import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:path/path.dart' as p; part 'database.g.dart'; enum JobStatus { queued, running, completed, failed, skipped } class ProjectStates extends Table { TextColumn get projectKey => text()(); TextColumn get repo => text()(); TextColumn get lastSeenUpdatedAt => text().nullable()(); TextColumn get updatedAt => text()(); @override Set> get primaryKey => {projectKey}; } class IssueThreadStates extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get projectKey => text()(); IntColumn get issueNumber => integer()(); TextColumn get fingerprint => text()(); TextColumn get lastEvaluatedFingerprint => text().nullable()(); TextColumn get lastDecision => text().nullable()(); IntColumn get lastCommentId => integer().nullable()(); TextColumn get lastCommentUrl => text().nullable()(); TextColumn get updatedAt => text()(); @override List>> get uniqueKeys => [ {projectKey, issueNumber}, ]; } class Jobs extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get projectKey => text()(); IntColumn get issueNumber => integer()(); TextColumn get fingerprint => text()(); TextColumn get trigger => text()(); TextColumn get status => textEnum()(); TextColumn get createdAt => text()(); TextColumn get updatedAt => text()(); } class ExecutionRuns extends Table { IntColumn get id => integer().autoIncrement()(); IntColumn get jobId => integer()(); TextColumn get responderId => text()(); TextColumn get mode => text()(); TextColumn get decision => text()(); BoolColumn get success => boolean()(); TextColumn get workspacePath => text().nullable()(); TextColumn get stdout => text()(); TextColumn get stderr => text()(); TextColumn get createdAt => text()(); } class Comments extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get projectKey => text()(); IntColumn get issueNumber => integer()(); TextColumn get fingerprint => text()(); IntColumn get githubCommentId => integer()(); TextColumn get githubCommentUrl => text()(); TextColumn get body => text()(); TextColumn get createdAt => text()(); } class PollRuns extends Table { IntColumn get id => integer().autoIncrement()(); TextColumn get projectKey => text()(); TextColumn get startedAt => text()(); TextColumn get finishedAt => text().nullable()(); BoolColumn get success => boolean().nullable()(); TextColumn get error => text().nullable()(); } @DriftDatabase( tables: [ ProjectStates, IssueThreadStates, Jobs, ExecutionRuns, Comments, PollRuns, ], ) class AppDatabase extends _$AppDatabase { AppDatabase._(super.e); factory AppDatabase.open(String path) { return AppDatabase._( NativeDatabase.createInBackground(File(p.absolute(path))), ); } @override int get schemaVersion => 1; Future findProjectState(String projectKey) { return (select( projectStates, )..where((tbl) => tbl.projectKey.equals(projectKey))).getSingleOrNull(); } Future upsertProjectState({ required String projectKey, required String repo, required DateTime? lastSeenUpdatedAt, }) { final now = DateTime.now().toUtc().toIso8601String(); return into(projectStates).insertOnConflictUpdate( ProjectStatesCompanion.insert( projectKey: projectKey, repo: repo, lastSeenUpdatedAt: Value(lastSeenUpdatedAt?.toIso8601String()), updatedAt: now, ), ); } Future findIssueThread( String projectKey, int issueNumber, ) { return (select(issueThreadStates) ..where((tbl) => tbl.projectKey.equals(projectKey)) ..where((tbl) => tbl.issueNumber.equals(issueNumber))) .getSingleOrNull(); } Future upsertIssueThread({ required String projectKey, required int issueNumber, required String fingerprint, required DateTime updatedAt, String? lastEvaluatedFingerprint, String? lastDecision, int? lastCommentId, String? lastCommentUrl, }) async { final existing = await findIssueThread(projectKey, issueNumber); final companion = IssueThreadStatesCompanion( projectKey: Value(projectKey), issueNumber: Value(issueNumber), fingerprint: Value(fingerprint), lastEvaluatedFingerprint: Value(lastEvaluatedFingerprint), lastDecision: Value(lastDecision), lastCommentId: Value(lastCommentId), lastCommentUrl: Value(lastCommentUrl), updatedAt: Value(updatedAt.toUtc().toIso8601String()), ); if (existing == null) { await into(issueThreadStates).insert( IssueThreadStatesCompanion.insert( projectKey: projectKey, issueNumber: issueNumber, fingerprint: fingerprint, updatedAt: updatedAt.toUtc().toIso8601String(), lastEvaluatedFingerprint: Value(lastEvaluatedFingerprint), lastDecision: Value(lastDecision), lastCommentId: Value(lastCommentId), lastCommentUrl: Value(lastCommentUrl), ), ); return; } await (update( issueThreadStates, )..where((tbl) => tbl.id.equals(existing.id))).write(companion); } Future createJob({ required String projectKey, required int issueNumber, required String fingerprint, required String trigger, required JobStatus status, }) { final now = DateTime.now().toUtc().toIso8601String(); return into(jobs).insert( JobsCompanion.insert( projectKey: projectKey, issueNumber: issueNumber, fingerprint: fingerprint, trigger: trigger, status: status, createdAt: now, updatedAt: now, ), ); } Future updateJobStatus(int jobId, JobStatus status) { return (update(jobs)..where((tbl) => tbl.id.equals(jobId))).write( JobsCompanion( status: Value(status), updatedAt: Value(DateTime.now().toUtc().toIso8601String()), ), ); } Future addExecutionRun({ required int jobId, required String responderId, required String mode, required String decision, required bool success, required String workspacePath, required String stdout, required String stderr, }) { return into(executionRuns).insert( ExecutionRunsCompanion.insert( jobId: jobId, responderId: responderId, mode: mode, decision: decision, success: success, workspacePath: Value(workspacePath), stdout: stdout, stderr: stderr, createdAt: DateTime.now().toUtc().toIso8601String(), ), ); } Future addCommentRecord({ required String projectKey, required int issueNumber, required String fingerprint, required int githubCommentId, required String githubCommentUrl, required String body, }) { return into(comments).insert( CommentsCompanion.insert( projectKey: projectKey, issueNumber: issueNumber, fingerprint: fingerprint, githubCommentId: githubCommentId, githubCommentUrl: githubCommentUrl, body: body, createdAt: DateTime.now().toUtc().toIso8601String(), ), ); } Future startPollRun(String projectKey) { return into(pollRuns).insert( PollRunsCompanion.insert( projectKey: projectKey, startedAt: DateTime.now().toUtc().toIso8601String(), ), ); } Future finishPollRun({ required int pollRunId, required bool success, String? error, }) { return (update(pollRuns)..where((tbl) => tbl.id.equals(pollRunId))).write( PollRunsCompanion( finishedAt: Value(DateTime.now().toUtc().toIso8601String()), success: Value(success), error: Value(error), ), ); } }