258 lines
7.2 KiB
Dart
258 lines
7.2 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.created_at 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;
|
|
}
|
|
|
|
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,
|
|
}) 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: <String, dynamic>{
|
|
'uid': firebaseUid,
|
|
'older': _slot(olderSlotStart),
|
|
'newer': _slot(newerSlotStart),
|
|
'symbol': symbol,
|
|
'prospective_question_id': prospectiveQuestionId,
|
|
'question_id': questionId,
|
|
'pending': statusPending,
|
|
},
|
|
);
|
|
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());
|
|
}
|