Skip to content

Commit f04ccba

Browse files
authored
Merge pull request #1692 from dcoric/feat/postgres-pushes-perf
perf: index the PostgreSQL pushes hot paths and slim list projections
2 parents 83377fe + 0ac64f0 commit f04ccba

5 files changed

Lines changed: 62 additions & 5 deletions

File tree

src/db/postgres/pushes.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,15 @@ export const getPushesForUserProfile = async (
163163
}
164164

165165
const result = await query<{ data: unknown }>(
166-
`SELECT data FROM pushes WHERE type = 'push' AND ${predicate} ORDER BY timestamp DESC`,
166+
`SELECT data - 'steps' AS data FROM pushes WHERE type = 'push' AND ${predicate} ORDER BY timestamp DESC`,
167167
values,
168168
);
169169
return result.rows.map(rowToAction);
170170
};
171171

172+
// List queries drop `steps` from the returned document: it holds the full diff
173+
// (largest part of a push row) and the mongo backend's list projection excludes
174+
// it as well. The push-detail path (`getPush`) still returns the whole document.
172175
export const getPushes = async (q: Partial<PushQuery> = defaultPushQuery): Promise<Action[]> => {
173176
const clauses: string[] = [];
174177
const values: unknown[] = [];
@@ -181,7 +184,7 @@ export const getPushes = async (q: Partial<PushQuery> = defaultPushQuery): Promi
181184

182185
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
183186
const result = await query<{ data: unknown }>(
184-
`SELECT data FROM pushes ${where} ORDER BY timestamp DESC`,
187+
`SELECT data - 'steps' AS data FROM pushes ${where} ORDER BY timestamp DESC`,
185188
values,
186189
);
187190
return result.rows.map(rowToAction);

src/db/postgres/schemaMigrations.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,31 @@ export const MIGRATIONS: Migration[] = [
145145
// legacy JSONB column (backfilled in migration 4) is no longer used.
146146
sql: `ALTER TABLE repos DROP COLUMN IF EXISTS users;`,
147147
},
148+
{
149+
version: 6,
150+
name: 'pushes_hot_path_indexes',
151+
sql: `
152+
-- Covering index for the repo activity rollup: the scan becomes index-only
153+
-- and never detoasts the large push JSONB documents.
154+
CREATE INDEX IF NOT EXISTS pushes_rollup_idx
155+
ON pushes ((data->>'url'), timestamp)
156+
INCLUDE (error, rejected, canceled, authorised, blocked, allow_push)
157+
WHERE type = 'push';
158+
159+
-- Matches the default dashboard query for pushes pending review.
160+
CREATE INDEX IF NOT EXISTS pushes_pending_idx
161+
ON pushes (timestamp DESC)
162+
WHERE type = 'push' AND blocked AND NOT error AND NOT authorised AND NOT allow_push;
163+
164+
-- User profile lookups filter on JSONB expressions; index both predicates.
165+
CREATE INDEX IF NOT EXISTS pushes_user_email_idx
166+
ON pushes ((data->>'userEmail'))
167+
WHERE type = 'push';
168+
CREATE INDEX IF NOT EXISTS pushes_reviewer_idx
169+
ON pushes ((lower(data->'attestation'->'reviewer'->>'username')))
170+
WHERE type = 'push';
171+
`,
172+
},
148173
];
149174

150175
const SCHEMA_MIGRATIONS_TABLE_SQL = `

test/db/postgres/pushes.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,23 @@ describe('PostgreSQL - Pushes', async () => {
213213
});
214214
});
215215

216+
describe('list projection', () => {
217+
it('drops steps from list results but not from the detail view', async () => {
218+
mockQuery.mockResolvedValue({ rowCount: 0, rows: [] });
219+
220+
await getPushes({});
221+
await getPushesForUserProfile([], 'alice');
222+
await getPush('p1');
223+
224+
const [listSql] = mockQuery.mock.calls[0];
225+
const [profileSql] = mockQuery.mock.calls[1];
226+
const [detailSql] = mockQuery.mock.calls[2];
227+
expect(listSql).toContain("data - 'steps'");
228+
expect(profileSql).toContain("data - 'steps'");
229+
expect(detailSql).not.toContain("data - 'steps'");
230+
});
231+
});
232+
216233
describe('getPushesForUserProfile', () => {
217234
it('matches the reviewer case-insensitively when there are no emails', async () => {
218235
mockQuery.mockResolvedValue({ rowCount: 0, rows: [] });

test/db/postgres/schemaMigrations.integration.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe.runIf(shouldRunPostgresTests)('PostgreSQL Schema Migration Integration
5353
const versions = await query<{ version: number }>(
5454
'SELECT version FROM schema_migrations ORDER BY version',
5555
);
56-
expect(versions.rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5]);
56+
expect(versions.rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6]);
5757

5858
const tables = await query<{ tablename: string }>(
5959
`SELECT tablename FROM pg_tables
@@ -90,7 +90,7 @@ describe.runIf(shouldRunPostgresTests)('PostgreSQL Schema Migration Integration
9090
await connect();
9191

9292
const versions = await query<{ version: number }>('SELECT version FROM schema_migrations');
93-
expect(versions.rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5]);
93+
expect(versions.rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6]);
9494
});
9595

9696
it('backfills existing JSONB repo permissions into repo_users (single, multi, same user in both roles)', async () => {
@@ -143,7 +143,7 @@ describe.runIf(shouldRunPostgresTests)('PostgreSQL Schema Migration Integration
143143
const versions = await pool.query<{ version: number }>(
144144
'SELECT version FROM schema_migrations ORDER BY version',
145145
);
146-
expect(versions.rows.map((r) => r.version)).toEqual([1, 2, 3, 4, 5]);
146+
expect(versions.rows.map((r) => r.version)).toEqual([1, 2, 3, 4, 5, 6]);
147147

148148
// The legacy JSONB column is gone, dropped by v4.
149149
const usersCol = await pool.query(

test/db/postgres/schemaMigrations.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ const makePool = (appliedRows: { version: number }[] = []) => {
3838
const sqlsOf = (query: ReturnType<typeof vi.fn>) => query.mock.calls.map((call) => String(call[0]));
3939

4040
describe('PostgreSQL - migrations', () => {
41+
it('defines the pushes hot-path indexes as version 6', () => {
42+
const v6 = MIGRATIONS.find((m) => m.version === 6);
43+
expect(v6?.name).toBe('pushes_hot_path_indexes');
44+
expect(v6?.sql).toContain('pushes_rollup_idx');
45+
expect(v6?.sql).toContain(
46+
'INCLUDE (error, rejected, canceled, authorised, blocked, allow_push)',
47+
);
48+
expect(v6?.sql).toContain('pushes_pending_idx');
49+
expect(v6?.sql).toContain('pushes_user_email_idx');
50+
expect(v6?.sql).toContain('pushes_reviewer_idx');
51+
});
52+
4153
it('exposes an ordered, append-only migration list starting at version 1', () => {
4254
expect(MIGRATIONS[0].version).toBe(1);
4355

0 commit comments

Comments
 (0)