304 lines
8.7 KiB
Dart
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());
|
|
}
|