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.view_order_at ASC, a.question_id 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; } /// Moves a pending assignment to the tail of [firebaseUid]'s FIFO queue. /// /// Sets [view_order_at] to the current time (or later than any pending peer) /// so the deferred question is served last on the next fetch. Future pushToBackOfQueue({ required String questionId, required String firebaseUid, DateTime? now, }) async { final DateTime tick = (now ?? DateTime.now()).toUtc(); final Result result = await _connection.execute( Sql.named( ''' UPDATE market_history_prospective_assignments a SET view_order_at = ( SELECT GREATEST( @now, COALESCE(MAX(a2.view_order_at), @now) + INTERVAL '1 millisecond' ) FROM market_history_prospective_assignments a2 INNER JOIN questions q2 ON q2.id = a2.question_id WHERE a2.assigned_user_id = @uid AND a2.status = @pending AND q2.user_response IS NULL AND a2.question_id <> @question_id::uuid ) WHERE a.question_id = @question_id::uuid AND a.assigned_user_id = @uid AND a.status = @pending RETURNING a.id ''', ), parameters: { 'question_id': questionId, 'uid': firebaseUid, 'pending': statusPending, 'now': tick, }, ); 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, DateTime? viewOrderAt, }) 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, view_order_at ) VALUES ( @uid, @older, @newer, @symbol, @prospective_question_id::uuid, @question_id::uuid, @pending, COALESCE(@view_order_at, now()) ) 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, 'view_order_at': viewOrderAt?.toUtc(), }, ); 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()); }