import 'package:postgres/postgres.dart'; import 'market_history_session_slot.dart'; /// Persisted guess slot assignment (one row per user, slot pair, and symbol). class ProspectiveGuessAssignmentsDb { ProspectiveGuessAssignmentsDb(this._connection); final Connection _connection; static const String statusPending = 'pending'; static const String statusAnswered = 'answered'; /// Question id for a pending assignment whose question is still unanswered. Future findPendingQuestionId(String firebaseUid) async { final Result result = await _connection.execute( Sql.named( ''' SELECT a.question_id FROM market_history_prospective_assignments a INNER JOIN questions q ON q.id = a.question_id WHERE a.assigned_user_id = @uid AND a.status = @pending AND q.user_response IS NULL ORDER BY a.created_at ASC LIMIT 1 ''', ), parameters: { 'uid': firebaseUid, 'pending': statusPending, }, ); if (result.isEmpty) { return null; } return result.first[0].toString(); } Future hasPendingAssignmentForSlotPair({ required String firebaseUid, required DateTime olderSlotStart, required DateTime newerSlotStart, }) async { final Result result = await _connection.execute( Sql.named( ''' SELECT 1 FROM market_history_prospective_assignments a INNER JOIN questions q ON q.id = a.question_id WHERE a.assigned_user_id = @uid AND a.older_slot_start = @older AND a.newer_slot_start = @newer AND a.status = @pending AND q.user_response IS NULL LIMIT 1 ''', ), parameters: { 'uid': firebaseUid, 'older': _slot(olderSlotStart), 'newer': _slot(newerSlotStart), 'pending': statusPending, }, ); return result.isNotEmpty; } Future hasAssignmentForSymbolSlotPair({ required String firebaseUid, required DateTime olderSlotStart, required DateTime newerSlotStart, required String symbol, }) async { final Result result = await _connection.execute( Sql.named( ''' SELECT 1 FROM market_history_prospective_assignments WHERE assigned_user_id = @uid AND older_slot_start = @older AND newer_slot_start = @newer AND symbol = @symbol LIMIT 1 ''', ), parameters: { 'uid': firebaseUid, 'older': _slot(olderSlotStart), 'newer': _slot(newerSlotStart), 'symbol': symbol, }, ); return result.isNotEmpty; } Future> assignedSymbolsForSlotPair({ required String firebaseUid, required DateTime olderSlotStart, required DateTime newerSlotStart, }) async { final Result result = await _connection.execute( Sql.named( ''' SELECT symbol FROM market_history_prospective_assignments WHERE assigned_user_id = @uid AND older_slot_start = @older AND newer_slot_start = @newer ''', ), parameters: { 'uid': firebaseUid, 'older': _slot(olderSlotStart), 'newer': _slot(newerSlotStart), }, ); return result.map((ResultRow row) => row[0]! as String).toSet(); } Future> answeredSymbolsForSlotPair({ required String firebaseUid, required DateTime olderSlotStart, required DateTime newerSlotStart, }) async { final Result result = await _connection.execute( Sql.named( ''' SELECT symbol FROM market_history_prospective_assignments WHERE assigned_user_id = @uid AND older_slot_start = @older AND newer_slot_start = @newer AND status = @answered ''', ), parameters: { 'uid': firebaseUid, 'older': _slot(olderSlotStart), 'newer': _slot(newerSlotStart), 'answered': statusAnswered, }, ); return result.map((ResultRow row) => row[0]! as String).toSet(); } /// Inserts a pending row when none exists for this user/slot pair/symbol. /// /// Returns false when an assignment already exists (unique constraint). Future insertPendingIfAbsent({ required String firebaseUid, required DateTime olderSlotStart, required DateTime newerSlotStart, required String symbol, required String prospectiveQuestionId, required String questionId, }) async { final Result result = await _connection.execute( Sql.named( ''' INSERT INTO market_history_prospective_assignments ( assigned_user_id, older_slot_start, newer_slot_start, symbol, prospective_question_id, question_id, status ) VALUES ( @uid, @older, @newer, @symbol, @prospective_question_id::uuid, @question_id::uuid, @pending ) ON CONFLICT (assigned_user_id, older_slot_start, newer_slot_start, symbol) DO NOTHING RETURNING id ''', ), parameters: { 'uid': firebaseUid, 'older': _slot(olderSlotStart), 'newer': _slot(newerSlotStart), 'symbol': symbol, 'prospective_question_id': prospectiveQuestionId, 'question_id': questionId, 'pending': statusPending, }, ); return result.isNotEmpty; } Future insertPending({ required String firebaseUid, required DateTime olderSlotStart, required DateTime newerSlotStart, required String symbol, required String prospectiveQuestionId, required String questionId, }) async { final bool inserted = await insertPendingIfAbsent( firebaseUid: firebaseUid, olderSlotStart: olderSlotStart, newerSlotStart: newerSlotStart, symbol: symbol, prospectiveQuestionId: prospectiveQuestionId, questionId: questionId, ); if (!inserted) { throw StateError( 'Assignment already exists for $firebaseUid $symbol ' '${_slot(olderSlotStart).toIso8601String()}', ); } } Future markAnsweredByQuestionId({ required String questionId, required DateTime answeredAt, }) async { await _connection.execute( Sql.named( ''' UPDATE market_history_prospective_assignments SET status = @answered, answered_at = @answered_at WHERE question_id = @question_id::uuid AND status = @pending ''', ), parameters: { 'question_id': questionId, 'answered': statusAnswered, 'answered_at': answeredAt.toUtc(), 'pending': statusPending, }, ); } Future deleteAllForUser(String firebaseUid) async { await _connection.execute( Sql.named( ''' DELETE FROM market_history_prospective_assignments WHERE assigned_user_id = @uid ''', ), parameters: {'uid': firebaseUid}, ); } static DateTime _slot(DateTime value) => MarketHistorySessionSlot.slotStartContaining(value.toUtc()); }