Skip to content

Commit d07907a

Browse files
authored
Merge pull request #234 from coder13/agent/preserve-dualwrite-history
Preserve historical PostgreSQL solves
2 parents f76cc7b + 66392ff commit d07907a

4 files changed

Lines changed: 50 additions & 7 deletions

File tree

server/models/room.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,9 @@ const collectPostgresChanges = (room) => {
527527
return {
528528
attempts,
529529
participantUserIds: [...participantUserIds],
530-
replaceAttempts: !room.isNew && room.isModified('event'),
530+
// An event switch starts a new attempt stream. The migration writer must
531+
// mirror it without deleting attempts from the previous event.
532+
syncAllAttempts: !room.isNew && room.isModified('event'),
531533
syncAllParticipants: room.isNew,
532534
syncRoomOwners: room.isNew || room.isModified('owner') || room.isModified('admin'),
533535
};

server/models/room.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,13 +407,13 @@ describe('room security helpers', () => {
407407
syncAllResults: false,
408408
}],
409409
participantUserIds: ['1234'],
410-
replaceAttempts: false,
410+
syncAllAttempts: false,
411411
syncAllParticipants: false,
412412
syncRoomOwners: false,
413413
});
414414
});
415415

416-
it('replaces PostgreSQL attempts when the room event resets them', () => {
416+
it('syncs the new PostgreSQL attempt stream when the room event resets it', () => {
417417
const room = RoomModel.hydrate({
418418
_id: '507f1f77bcf86cd799439011',
419419
name: 'Practice room',
@@ -439,7 +439,7 @@ describe('room security helpers', () => {
439439
syncAllResults: true,
440440
}],
441441
participantUserIds: [],
442-
replaceAttempts: true,
442+
syncAllAttempts: true,
443443
syncAllParticipants: false,
444444
syncRoomOwners: false,
445445
});

server/postgres/dualWrite.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ const syncAttemptResults = async (client, roomState, attemptState, attempt, user
537537

538538
const writeRoomChanges = async (client, room, changes) => {
539539
const resultUserIds = new Set();
540-
if (changes.replaceAttempts) {
540+
if (changes.syncAllAttempts) {
541541
(room.attempts || []).forEach((attempt) => {
542542
resultEntries(attempt.results).forEach(([userId]) => resultUserIds.add(userId));
543543
});
@@ -563,8 +563,7 @@ const writeRoomChanges = async (client, room, changes) => {
563563
return null;
564564
}
565565

566-
if (changes.replaceAttempts) {
567-
await client.query('DELETE FROM app.attempts WHERE room_id = $1', [roomState.roomId]);
566+
if (changes.syncAllAttempts) {
568567
for (const [attemptIndex, attempt] of (room.attempts || []).entries()) {
569568
const attemptState = await upsertAttempt(client, room, roomState, attempt, attemptIndex);
570569
await syncAttemptResults(

server/postgres/dualWrite.test.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,48 @@ describe('PostgreSQL dual writer', () => {
175175
.toContain(12001);
176176
});
177177

178+
it('retains prior-event rows when an event switch starts a new attempt stream', async () => {
179+
const updatedAt = new Date('2026-07-10T20:00:00.000Z');
180+
const room = {
181+
_id: '507f1f77bcf86cd799439011',
182+
name: 'Practice room',
183+
event: '222',
184+
accessCode: 'room-access-code',
185+
type: 'normal',
186+
owner: user,
187+
admin: user,
188+
users: [user],
189+
attempts: [{
190+
id: 0,
191+
scrambles: ['R U2 R\''],
192+
results: new Map([['1234', {
193+
time: 8000,
194+
penalties: {},
195+
createdAt: updatedAt,
196+
updatedAt,
197+
}]]),
198+
createdAt: updatedAt,
199+
updatedAt,
200+
}],
201+
createdAt: updatedAt,
202+
updatedAt,
203+
};
204+
205+
await mirrorRoomChanges(room, {
206+
attempts: [],
207+
participantUserIds: [],
208+
syncAllAttempts: true,
209+
syncAllParticipants: false,
210+
syncRoomOwners: true,
211+
});
212+
213+
const statements = client.query.mock.calls.map(([sql]) => sql);
214+
expect(statements.some((sql) => sql.includes('DELETE FROM app.attempts'))).toBe(false);
215+
expect(statements.some((sql) => sql.includes('DELETE FROM app.solves'))).toBe(false);
216+
expect(statements.filter((sql) => sql.includes('INSERT INTO app.attempts'))).toHaveLength(1);
217+
expect(statements.filter((sql) => sql.includes('INSERT INTO app.solves'))).toHaveLength(1);
218+
});
219+
178220
it('mirrors sanitized metric events and soft-deletes rooms', async () => {
179221
const occurredAt = new Date('2026-07-09T20:00:00.000Z');
180222
await mirrorMetricEvent({

0 commit comments

Comments
 (0)