diff --git a/apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts index 3633ed4c6..2cb93a491 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/external-db-sync-race.test.ts @@ -406,380 +406,5 @@ describe.sequential('External DB Sync - Race Condition Tests', () => { }, LOCAL_TEST_TIMEOUT, ); - - /** - * Scenario 2: - * First transaction commits, then poller runs. - * Poller should pick up Transaction 1 and sequenceId should increase. - */ - test( - 'Poller picks up first committed transaction', - async () => { - const dbName = 'race_after_first_commit_test'; - const { externalClient, user } = - await setupExternalDbWithBaseline(dbName); - - const internalDbUrl = makeInternalDbUrl(); - const internalClient = new Client({ connectionString: internalDbUrl }); - - await internalClient.connect(); - - try { - // Commit Transaction 1 - await internalClient.query('BEGIN'); - await internalClient.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'Transaction 1', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user.userId], - ); - await internalClient.query('COMMIT'); - - await waitForCondition( - async () => { - const res = await externalClient.query<{ - display_name: string | null, - }>( - ` - SELECT "display_name" - FROM "users" - WHERE "primary_email" = $1 - `, - [`${dbName}@example.com`], - ); - return ( - res.rows.length === 1 && - res.rows[0].display_name === 'Transaction 1' - ); - }, - { description: 'Transaction 1 exported', timeoutMs: 90000 }, - ); - - const afterT1 = await externalClient.query<{ - display_name: string | null, - }>( - ` - SELECT "display_name" - FROM "users" - WHERE "primary_email" = $1 - `, - [`${dbName}@example.com`], - ); - - expect(afterT1.rows.length).toBe(1); - expect(afterT1.rows[0].display_name).toBe('Transaction 1'); - } finally { - await internalClient.end(); - } - }, - LOCAL_TEST_TIMEOUT, - ); - - /** - * Scenario 3: - * First transaction is committed and synced. - * Second transaction has UPDATE done but is still uncommitted. - * Poller should STILL see Transaction 1 (not Transaction 2). - */ - test( - 'Poller does not see second update until commit', - async () => { - const dbName = 'race_second_uncommitted_poll_test'; - const { externalClient, user } = - await setupExternalDbWithBaseline(dbName); - - const internalDbUrl = makeInternalDbUrl(); - const internalClient = new Client({ connectionString: internalDbUrl }); - - await internalClient.connect(); - - try { - await internalClient.query('BEGIN'); - await internalClient.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'Transaction 1', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user.userId], - ); - await internalClient.query('COMMIT'); - - await waitForCondition( - async () => { - const res = await externalClient.query<{ display_name: string | null }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - [`${dbName}@example.com`], - ); - return res.rows.length === 1 && res.rows[0].display_name === 'Transaction 1'; - }, - { description: 'Transaction 1 exported', timeoutMs: 60000 }, - ); - - // Start uncommitted Transaction 2 - await internalClient.query('BEGIN'); - await internalClient.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'Transaction 2', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user.userId], - ); - - await sleep(7000); - - const duringT2 = await externalClient.query<{ - display_name: string | null, - }>( - ` - SELECT "display_name" - FROM "users" - WHERE "primary_email" = $1 - `, - [`${dbName}@example.com`], - ); - - expect(duringT2.rows.length).toBe(1); - // Uncommitted Transaction 2 should not be visible - expect(duringT2.rows[0].display_name).not.toBe('Transaction 2'); - expect(duringT2.rows[0].display_name).toBe('Transaction 1'); - - await internalClient.query('ROLLBACK'); - } finally { - await internalClient.end(); - } - }, - LOCAL_TEST_TIMEOUT, - ); - - /** - * Scenario 4: - * Two different rows, out-of-order commits: - * - T1 starts - * - T2 starts - * - T2 updates row2 - * - T1 updates row1 - * - T2 commits - * - Sync → only T2's row visible, T1's row unchanged - * - T1 commits - * - Sync → T1's row now visible - * - * Uses two different users to avoid row-level locking. - */ - test( - 'Out-of-order commits on different rows: uncommitted changes invisible', - async () => { - const dbName = 'race_two_rows_out_of_order_test'; - const connectionString = await dbManager.createDatabase(dbName); - - await createProjectWithExternalDb({ - main: { - type: 'postgres', - connectionString, - }, - }); - - const externalClient = dbManager.getClient(dbName); - - const user1 = await User.create({ primary_email: 'row1@example.com' }); - const user2 = await User.create({ primary_email: 'row2@example.com' }); - - await waitForTable(externalClient, 'users'); - - await waitForCondition( - async () => { - const res = await externalClient.query(`SELECT COUNT(*) as count FROM "users"`); - return parseInt(res.rows[0].count, 10) === 2; - }, - { description: 'both users synced initially', timeoutMs: 60000 }, - ); - - const internalDbUrl = makeInternalDbUrl(); - const t1Client = new Client({ connectionString: internalDbUrl }); - const t2Client = new Client({ connectionString: internalDbUrl }); - - await t1Client.connect(); - await t2Client.connect(); - - try { - await t1Client.query('BEGIN'); - - await t2Client.query('BEGIN'); - - await t2Client.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'T2 Updated', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user2.userId], - ); - - await t1Client.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'T1 Updated', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user1.userId], - ); - - await t2Client.query('COMMIT'); - - await waitForCondition( - async () => { - const res = await externalClient.query<{ display_name: string | null }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - ['row2@example.com'], - ); - return res.rows.length === 1 && res.rows[0].display_name === 'T2 Updated'; - }, - { description: 'T2 row synced after T2 commit', timeoutMs: 90000 }, - ); - - const row1BeforeT1Commit = await externalClient.query<{ display_name: string | null }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - ['row1@example.com'], - ); - expect(row1BeforeT1Commit.rows.length).toBe(1); - expect(row1BeforeT1Commit.rows[0].display_name).not.toBe('T1 Updated'); - - await t1Client.query('COMMIT'); - - await waitForCondition( - async () => { - const res = await externalClient.query<{ display_name: string | null }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - ['row1@example.com'], - ); - return res.rows.length === 1 && res.rows[0].display_name === 'T1 Updated'; - }, - { description: 'T1 row synced after T1 commit', timeoutMs: 90000 }, - ); - - const finalRow1 = await externalClient.query<{ display_name: string | null }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - ['row1@example.com'], - ); - const finalRow2 = await externalClient.query<{ display_name: string | null }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - ['row2@example.com'], - ); - - expect(finalRow1.rows[0].display_name).toBe('T1 Updated'); - expect(finalRow2.rows[0].display_name).toBe('T2 Updated'); - } finally { - await t1Client.end(); - await t2Client.end(); - } - }, - LOCAL_TEST_TIMEOUT, - ); - - /** - * Scenario 5: - * Full lifecycle: - * - baseline - * - Transaction 1 committed & synced - * - Transaction 2 committed after a later sync - * Final state must be Transaction 2. - */ - test( - 'Sequential updates both sync correctly', - async () => { - const dbName = 'race_full_lifecycle_test'; - const { externalClient, user } = - await setupExternalDbWithBaseline(dbName); - - const internalDbUrl = makeInternalDbUrl(); - const internalClient = new Client({ connectionString: internalDbUrl }); - - await internalClient.connect(); - - try { - await internalClient.query('BEGIN'); - await internalClient.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'Transaction 1', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user.userId], - ); - await internalClient.query('COMMIT'); - - await waitForCondition( - async () => { - const res = await externalClient.query<{ - display_name: string | null, - }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - [`${dbName}@example.com`], - ); - return res.rows.length === 1 && res.rows[0].display_name === 'Transaction 1'; - }, - { description: 'T1 synced', timeoutMs: 90000 }, - ); - - const afterT1 = await externalClient.query<{ - display_name: string | null, - }>( - ` - SELECT "display_name" - FROM "users" - WHERE "primary_email" = $1 - `, - [`${dbName}@example.com`], - ); - - expect(afterT1.rows.length).toBe(1); - expect(afterT1.rows[0].display_name).toBe('Transaction 1'); - - await internalClient.query('BEGIN'); - await internalClient.query( - ` - UPDATE "ProjectUser" - SET "displayName" = 'Transaction 2', "updatedAt" = NOW() - WHERE "projectUserId" = $1 - `, - [user.userId], - ); - await internalClient.query('COMMIT'); - - await waitForCondition( - async () => { - const res = await externalClient.query<{ - display_name: string | null, - }>( - `SELECT "display_name" FROM "users" WHERE "primary_email" = $1`, - [`${dbName}@example.com`], - ); - return res.rows.length === 1 && res.rows[0].display_name === 'Transaction 2'; - }, - { description: 'T2 synced', timeoutMs: 90000 }, - ); - - const afterT2 = await externalClient.query<{ - display_name: string | null, - }>( - ` - SELECT "display_name" - FROM "users" - WHERE "primary_email" = $1 - `, - [`${dbName}@example.com`], - ); - - expect(afterT2.rows.length).toBe(1); - expect(afterT2.rows[0].display_name).toBe('Transaction 2'); - } finally { - await internalClient.end(); - } - }, - LOCAL_TEST_TIMEOUT, - ); }); });