cyberhybridhub/server/lib/trading/prospective_guess_assignments_db.dart
2026-06-03 04:21:42 -05:00

304 lines
8.7 KiB
Dart

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<String?> 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: <String, dynamic>{
'uid': firebaseUid,
'pending': statusPending,
},
);
if (result.isEmpty) {
return null;
}
return result.first[0].toString();
}
Future<bool> 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: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
'pending': statusPending,
},
);
return result.isNotEmpty;
}
Future<bool> 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: <String, dynamic>{
'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<bool> 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: <String, dynamic>{
'question_id': questionId,
'uid': firebaseUid,
'pending': statusPending,
'now': tick,
},
);
return result.isNotEmpty;
}
Future<Set<String>> 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: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
},
);
return result.map((ResultRow row) => row[0]! as String).toSet();
}
Future<Set<String>> 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: <String, dynamic>{
'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<bool> 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: <String, dynamic>{
'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<void> 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<void> 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: <String, dynamic>{
'question_id': questionId,
'answered': statusAnswered,
'answered_at': answeredAt.toUtc(),
'pending': statusPending,
},
);
}
Future<void> deleteAllForUser(String firebaseUid) async {
await _connection.execute(
Sql.named(
'''
DELETE FROM market_history_prospective_assignments
WHERE assigned_user_id = @uid
''',
),
parameters: <String, dynamic>{'uid': firebaseUid},
);
}
static DateTime _slot(DateTime value) =>
MarketHistorySessionSlot.slotStartContaining(value.toUtc());
}