Skip to content

Commit f0ce089

Browse files
committed
fix(projects): retain healthy indexes during schema push
1 parent 8323bd3 commit f0ce089

2 files changed

Lines changed: 53 additions & 2 deletions

File tree

‎packages/db/scripts/push.integration.ts‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('patched Drizzle push against PostgreSQL', () => {
5858
async function schema(source: string) {
5959
await writeFile(
6060
join(directory, 'schema.ts'),
61-
`import { pgTable, pgSchema, pgEnum, text, integer, bigint, boolean, check } from ${JSON.stringify(import.meta.resolve('drizzle-orm/pg-core'))}
61+
`import { pgTable, pgSchema, pgEnum, text, integer, bigint, boolean, check, index } from ${JSON.stringify(import.meta.resolve('drizzle-orm/pg-core'))}
6262
import { sql } from ${JSON.stringify(import.meta.resolve('drizzle-orm'))}
6363
${source}`
6464
)
@@ -195,7 +195,7 @@ export const knowledgeBases = pgTable('knowledge_base', {
195195
await schema(`export const projects = pgTable('project', { id: text('id').primaryKey() })
196196
export const workspaces = pgTable('workspace', {
197197
id: text('id').primaryKey(), forkedFromWorkspaceId: text('forked_from_workspace_id'), projectId: text('project_id'),
198-
})
198+
}, (table) => [index('workspace_project_id_id_idx').on(table.projectId, table.id)])
199199
export const memberships = pgTable('project_workspace', {
200200
projectId: text('project_id').notNull(), workspaceId: text('workspace_id').notNull().unique(),
201201
})`)
@@ -212,8 +212,15 @@ export const memberships = pgTable('project_workspace', {
212212
])
213213
await sql`UPDATE project_workspace SET project_id = 'singleton' WHERE workspace_id = 'fork'`
214214
await sql`UPDATE workspace SET project_id = 'family' WHERE id = 'standalone'`
215+
const indexBeforeReplay = await sql`SELECT indexrelid, indisvalid FROM pg_index
216+
WHERE indexrelid = 'workspace_project_id_id_idx'::regclass`
217+
expect(indexBeforeReplay).toEqual([{ indexrelid: expect.any(Number), indisvalid: true }])
215218
const repeated = runPush(['--force'])
216219
expect(repeated.error, repeated.stderr).toBeUndefined()
220+
expect(
221+
await sql`SELECT indexrelid, indisvalid FROM pg_index
222+
WHERE indexrelid = 'workspace_project_id_id_idx'::regclass`
223+
).toEqual(indexBeforeReplay)
217224
expect(
218225
await sql`SELECT w.id, w.project_id, pw.project_id AS legacy FROM workspace w
219226
JOIN project_workspace pw ON pw.workspace_id = w.id ORDER BY w.id`
@@ -224,6 +231,42 @@ export const memberships = pgTable('project_workspace', {
224231
])
225232
}, 60_000)
226233

234+
it('repairs an interrupted Project index build during schema-push reconciliation', async () => {
235+
await sql`CREATE TABLE project (id text PRIMARY KEY)`
236+
await sql`CREATE TABLE workspace (id text PRIMARY KEY, project_id text)`
237+
await sql`CREATE TABLE project_workspace (project_id text NOT NULL, workspace_id text NOT NULL UNIQUE)`
238+
await sql`INSERT INTO project VALUES ('family')`
239+
await sql`INSERT INTO workspace VALUES ('root', 'family'), ('fork', 'family')`
240+
await expect(
241+
sql`CREATE UNIQUE INDEX CONCURRENTLY workspace_project_id_id_idx ON workspace(project_id)`
242+
).rejects.toMatchObject({ code: '23505' })
243+
expect(
244+
await sql`SELECT indisvalid FROM pg_index WHERE indexrelid = 'workspace_project_id_id_idx'::regclass`
245+
).toEqual([{ indisvalid: false }])
246+
const result = spawnSync(
247+
'bun',
248+
[
249+
'--no-env-file',
250+
fileURLToPath(new URL('./reconcile-project-membership.ts', import.meta.url)),
251+
],
252+
{
253+
env: { ...process.env, DATABASE_URL: fixtureUrl, MIGRATION_DATABASE_URL: fixtureUrl },
254+
encoding: 'utf8',
255+
timeout: 15_000,
256+
}
257+
)
258+
expect(result.error).toBeUndefined()
259+
expect(result.status, result.stdout + result.stderr).toBe(0)
260+
expect(
261+
await sql`SELECT indisvalid, indisunique FROM pg_index
262+
WHERE indexrelid = 'workspace_project_id_id_idx'::regclass`
263+
).toEqual([{ indisvalid: true, indisunique: false }])
264+
expect(await sql`SELECT id, project_id FROM workspace ORDER BY id`).toEqual([
265+
{ id: 'fork', project_id: 'family' },
266+
{ id: 'root', project_id: 'family' },
267+
])
268+
})
269+
227270
it('refuses a schema downgrade before recreating the retired Project connector', async () => {
228271
await sql`CREATE TABLE project (id text PRIMARY KEY)`
229272
await sql`CREATE TABLE workspace (id text PRIMARY KEY, project_id text NOT NULL)`

‎packages/db/scripts/reconcile-project-membership.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ try {
3131
const connection = await sql.reserve()
3232
try {
3333
for (const statement of source.split('--> statement-breakpoint')) {
34+
if (statement.includes('DROP INDEX CONCURRENTLY IF EXISTS "workspace_project_id_id_idx"')) {
35+
// Migration retries rebuild interrupted indexes; routine push keeps healthy indexes intact.
36+
const [index] = await connection<{ healthy: boolean }[]>`
37+
SELECT indisvalid AND indisready AS healthy FROM pg_index
38+
WHERE indexrelid = to_regclass('public.workspace_project_id_id_idx')
39+
`
40+
if (index?.healthy) continue
41+
}
3442
if (statement.trim()) await connection.unsafe(statement)
3543
}
3644
} finally {

0 commit comments

Comments
 (0)