Skip to content

Commit f91e47c

Browse files
committed
fix(db): repair partial member sync status on existing databases
1 parent 53f0c01 commit f91e47c

7 files changed

Lines changed: 27701 additions & 3 deletions

File tree

.github/workflows/test-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ jobs:
210210
working-directory: packages/db
211211
env:
212212
KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
213-
run: bunx vitest run script-migrations/0016_backfill_search_vectors.postgres.test.ts
213+
run: bunx vitest run script-migrations/0016_backfill_search_vectors.postgres.test.ts member-sync-status-migration.postgres.test.ts
214214

215215
- name: Verify Search progress, pagination, and outbox scheduling in PostgreSQL
216216
working-directory: apps/sim
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { readFile } from 'node:fs/promises'
2+
import { generateId } from '@sim/utils/id'
3+
import postgres, { type Sql } from 'postgres'
4+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
5+
6+
const databaseUrl = process.env.KNOWLEDGE_ACL_TEST_DATABASE_URL
7+
8+
describe.runIf(Boolean(databaseUrl))('member sync status upgrade in PostgreSQL', () => {
9+
let admin: Sql
10+
let sql: Sql
11+
let migration: string
12+
const schema = `member_sync_status_${generateId().replaceAll('-', '')}`
13+
14+
beforeAll(async () => {
15+
const url = new URL(databaseUrl!)
16+
if (
17+
!['localhost', '127.0.0.1'].includes(url.hostname) ||
18+
!/^\/sim_(acl_test|auth_scim)(_|$)/.test(url.pathname)
19+
) {
20+
throw new Error('Member sync migration tests require a disposable local test database')
21+
}
22+
admin = postgres(url.toString(), { max: 1, onnotice: () => undefined })
23+
await admin.unsafe(`CREATE SCHEMA "${schema}"`)
24+
sql = postgres(url.toString(), {
25+
max: 1,
26+
connection: { search_path: schema },
27+
onnotice: () => undefined,
28+
})
29+
const original = await readFile(
30+
new URL('./migrations/0319_permission_aware_knowledge.sql', import.meta.url),
31+
'utf8'
32+
)
33+
const table = original.match(
34+
/CREATE TABLE IF NOT EXISTS "knowledge_connector_member_sync_log" \([\s\S]*?\n\);/
35+
)?.[0]
36+
if (!table) throw new Error('Original member sync log table DDL was not found')
37+
await sql.unsafe(table)
38+
migration = await readFile(
39+
new URL('./migrations/0356_member_sync_partial_status.sql', import.meta.url),
40+
'utf8'
41+
)
42+
})
43+
44+
afterAll(async () => {
45+
await sql?.end()
46+
if (admin) {
47+
await admin.unsafe(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`)
48+
await admin.end()
49+
}
50+
})
51+
52+
it('repairs partial completion after an account failure without losing run history', async () => {
53+
await sql`INSERT INTO knowledge_connector_member_sync_log
54+
(id, connector_id, status, members_claimed, members_failed)
55+
VALUES ('current', 'connector', 'started', 1, 1),
56+
('history', 'connector', 'completed', 1, 0)`
57+
await expect(
58+
sql`UPDATE knowledge_connector_member_sync_log SET status = 'partial' WHERE id = 'current'`
59+
).rejects.toMatchObject({ code: '23514', constraint_name: 'kcmsl_status_check' })
60+
61+
await sql.unsafe(migration)
62+
await sql`UPDATE knowledge_connector_member_sync_log SET status = 'partial' WHERE id = 'current'`
63+
await sql.unsafe(migration)
64+
expect(
65+
await sql`SELECT id, status, members_failed FROM knowledge_connector_member_sync_log ORDER BY id`
66+
).toEqual([
67+
{ id: 'current', status: 'partial', members_failed: 1 },
68+
{ id: 'history', status: 'completed', members_failed: 0 },
69+
])
70+
for (const status of ['started', 'completed', 'failed']) {
71+
await sql`INSERT INTO knowledge_connector_member_sync_log (id, connector_id, status)
72+
VALUES (${status}, 'connector', ${status})`
73+
}
74+
await expect(
75+
sql`INSERT INTO knowledge_connector_member_sync_log (id, connector_id, status)
76+
VALUES ('invalid', 'connector', 'unknown')`
77+
).rejects.toMatchObject({ code: '23514', constraint_name: 'kcmsl_status_check' })
78+
})
79+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Older deployments already journaled 0326 before it included the partial status.
2+
-- Widen the existing check atomically; every previously valid status remains valid.
3+
-- migration-safe: Atomic widening of the 0319 status check to match the already-deployed partial writer from 0326; all old statuses remain accepted and no rows or columns are removed.
4+
ALTER TABLE "knowledge_connector_member_sync_log"
5+
DROP CONSTRAINT IF EXISTS "kcmsl_status_check",
6+
ADD CONSTRAINT "kcmsl_status_check"
7+
CHECK ("status" IN ('started', 'partial', 'completed', 'failed')) NOT VALID;

0 commit comments

Comments
 (0)