import 'package:drift/drift.dart'; import 'package:drift_postgres/drift_postgres.dart'; import '../database.dart'; import '../tables/tests.dart'; part 'test_dao.g.dart'; @DriftAccessor( tables: [Tests, TestQuestions, TestPackRelations, TestStatistics], ) class TestDao extends DatabaseAccessor with _$TestDaoMixin { TestDao(super.db); // ==================== Tests ==================== /// Получить тест по ID (только активные) Future getTestById(String id) { return (select(tests) ..where((t) => t.id.equals(id) & t.isDeleted.equals(false))) .getSingleOrNull(); } /// Получить все тесты (только активные) Future> getAllTests() { return (select(tests)..where((t) => t.isDeleted.equals(false))).get(); } /// Получить тесты пака (только активные) Future> getTestsByPackId(String packId) async { final query = select(tests).join([ innerJoin( testPackRelations, testPackRelations.testId.equalsExp(tests.id) & testPackRelations.packId.equals(packId), ), ])..where(tests.isDeleted.equals(false)); return query.map((row) => row.readTable(tests)).get(); } /// Создать тест Future createTest(TestsCompanion test) async { final inserted = await into(tests).insertReturning(test); return inserted.id; } /// Обновить тест Future updateTest(Test test) { return update(tests).replace(test); } /// Удалить тест (soft delete) Future softDeleteTest(String testId) { return (update(tests)..where((t) => t.id.equals(testId))).write( TestsCompanion( isDeleted: const Value(true), deletedAt: Value(PgDateTime(DateTime.now())), updatedAt: Value(PgDateTime(DateTime.now())), ), ); } /// Hard delete old generated tests that were soft-deleted. /// /// This is important because we use soft delete for regular operations, /// but generated tests are ephemeral and otherwise will accumulate in DB /// (along with their questions/stats). Hard delete triggers FK cascades. Future hardDeleteOldSoftDeletedGeneratedTests({ required Duration olderThan, }) async { final threshold = DateTime.now().subtract(olderThan); final toDelete = await (select(tests)..where( (t) => t.isDeleted.equals(true) & t.version.equals('generated') & t.deletedAt.isSmallerThanValue(PgDateTime(threshold)), )) .get(); var deleted = 0; for (final test in toDelete) { deleted += await (delete(tests)..where((t) => t.id.equals(test.id))).go(); } return deleted; } /// Hard delete generated tests that are not linked to any pack. /// /// These tests are unreachable from the product (no pack relation) and /// should not accumulate forever. This also cleans up historical leftovers /// from the earlier bug where generated tests were created but not linked. Future hardDeleteOrphanGeneratedTests({ required Duration olderThan, }) async { final threshold = DateTime.now().subtract(olderThan); final rows = await (select(tests).join([ leftOuterJoin( testPackRelations, testPackRelations.testId.equalsExp(tests.id), ), ])..where( tests.version.equals('generated') & tests.createdAt.isSmallerThanValue(PgDateTime(threshold)) & testPackRelations.testId.isNull(), )) .get(); final orphanTests = rows.map((r) => r.readTable(tests)).toList(); var deleted = 0; for (final test in orphanTests) { deleted += await (delete(tests)..where((t) => t.id.equals(test.id))).go(); } return deleted; } /// Связать тест с паком Future linkTestToPack(String testId, String packId) async { await into(testPackRelations).insert( TestPackRelationsCompanion.insert(testId: testId, packId: packId), mode: InsertMode.insertOrIgnore, ); } /// Удалить связь теста с паком Future unlinkTestFromPack(String testId, String packId) async { await (delete(testPackRelations)..where( (tpr) => tpr.testId.equals(testId) & tpr.packId.equals(packId), )) .go(); } /// Получить packId для теста Future getPackIdForTest(String testId) async { final relation = await (select(testPackRelations) ..where((tpr) => tpr.testId.equals(testId)) ..limit(1)) .getSingleOrNull(); return relation?.packId; } /// Получить все packId для теста Future> getPackIdsForTest(String testId) async { final relations = await (select( testPackRelations, )..where((tpr) => tpr.testId.equals(testId))).get(); return relations.map((r) => r.packId).toList(); } // ==================== TestQuestions ==================== /// Получить вопросы теста (только активные, отсортированные по orderIndex) Future> getTestQuestions(String testId) { return (select(testQuestions) ..where((tq) => tq.testId.equals(testId) & tq.isDeleted.equals(false)) ..orderBy([(tq) => OrderingTerm(expression: tq.orderIndex)])) .get(); } /// Создать вопрос теста Future createTestQuestion(TestQuestionsCompanion question) async { final inserted = await into(testQuestions).insertReturning(question); return inserted.id; } /// Обновить вопрос теста Future updateTestQuestion(TestQuestion question) { return update(testQuestions).replace(question); } /// Удалить вопрос теста (soft delete) Future softDeleteTestQuestion(String questionId) { return (update( testQuestions, )..where((tq) => tq.id.equals(questionId))).write( TestQuestionsCompanion( isDeleted: const Value(true), deletedAt: Value(PgDateTime(DateTime.now())), updatedAt: Value(PgDateTime(DateTime.now())), ), ); } // ==================== TestStatistics ==================== /// Получить статистику теста пользователя Future getTestStatistics(String userId, String testId) { return (select(testStatistics) ..where((ts) => ts.userId.equals(userId) & ts.testId.equals(testId))) .getSingleOrNull(); } /// Создать статистику теста Future createTestStatistics( TestStatisticsCompanion statistics, ) async { final inserted = await into(testStatistics).insertReturning(statistics); return inserted.id; } /// Обновить статистику теста Future updateTestStatistics(TestStatistic statistics) { return update(testStatistics).replace(statistics); } }