diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts index d614e0badd3..3c710816514 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest' import { getTableSyncLagLabel } from './ReplicationPipelineStatus.utils' describe('getTableSyncLagLabel', () => { - test('omits healthy slot details when the table has caught up', () => { + test('reports unlimited WAL retention for a healthy caught-up table slot', () => { expect( getTableSyncLagLabel({ active: true, @@ -12,20 +12,20 @@ describe('getTableSyncLagLabel', () => { confirmed_flush_lsn_bytes: 0, safe_wal_size_bytes: null, }) - ).toEqual([]) + ).toEqual(['Unlimited WAL retention']) }) - test('reports the backlog and last check-in', () => { + test('reports the backlog, finite WAL retention, and last check-in', () => { expect( getTableSyncLagLabel({ active: true, wal_status: 'reserved', restart_lsn_bytes: 4096, confirmed_flush_lsn_bytes: 2048, - safe_wal_size_bytes: null, + safe_wal_size_bytes: 1_363_148_800, reply_time_lag: 4800, }) - ).toEqual(['2 KB waiting to sync', 'Last check-in 4.80 s']) + ).toEqual(['2 KB waiting to sync', '1.3 GB WAL retention remaining', 'Last check-in 4.80 s']) }) test('reports slot risk without treating the expected inactive connection as a fault', () => { @@ -35,8 +35,43 @@ describe('getTableSyncLagLabel', () => { wal_status: 'unreserved', restart_lsn_bytes: 0, confirmed_flush_lsn_bytes: 0, - safe_wal_size_bytes: null, + safe_wal_size_bytes: 0, }) - ).toEqual(['Some changes at risk']) + ).toEqual(['0 bytes WAL retention remaining', 'Some changes at risk']) + }) + + test('omits WAL retention when the API does not provide it', () => { + expect( + getTableSyncLagLabel({ + active: true, + wal_status: 'reserved', + restart_lsn_bytes: 0, + confirmed_flush_lsn_bytes: 0, + }) + ).toEqual([]) + }) + + test('omits an invalid WAL retention value', () => { + expect( + getTableSyncLagLabel({ + active: true, + wal_status: 'reserved', + restart_lsn_bytes: 0, + confirmed_flush_lsn_bytes: 0, + safe_wal_size_bytes: Number.NaN, + }) + ).toEqual([]) + }) + + test('omits a negative WAL retention value', () => { + expect( + getTableSyncLagLabel({ + active: true, + wal_status: 'reserved', + restart_lsn_bytes: 0, + confirmed_flush_lsn_bytes: 0, + safe_wal_size_bytes: -1, + }) + ).toEqual([]) }) }) diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx index 29e15b43e3d..32401d89afb 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx @@ -276,6 +276,18 @@ export const getTableSyncLagLabel = (metrics: SlotLagMetrics): string[] => { parts.push(`${formatBytes(pendingBytes, pendingBytes < 1024 ? 0 : 1)} waiting to sync`) } + const safeWalSizeBytes = metrics.safe_wal_size_bytes + if (safeWalSizeBytes === null) { + parts.push('Unlimited WAL retention') + } else if ( + typeof safeWalSizeBytes === 'number' && + Number.isFinite(safeWalSizeBytes) && + safeWalSizeBytes >= 0 + ) { + const formattedBytes = formatBytes(safeWalSizeBytes, safeWalSizeBytes < 1024 ? 0 : 1) + parts.push(`${formattedBytes} WAL retention remaining`) + } + if (metrics.wal_status === 'unreserved') parts.push('Some changes at risk') if (metrics.wal_status === 'lost') parts.push('Some changes lost')