From 6f12a3ba1d0c09a303b78523d165a98161e403cb Mon Sep 17 00:00:00 2001 From: Jaydeep Pipaliya Date: Wed, 24 Jun 2026 13:21:21 +0530 Subject: [PATCH] fix(db): index two more nullable FK columns missing covering indexes Two gateway-related FK columns shipped without covering indexes, so the per-row referential-integrity trigger seq-scans the child table on every parent delete: - dynamic_secrets.gatewayPoolId (RESTRICT to gateway_pools, nullable). Set only when a dynamic secret is pinned to a pool, most rows are NULL. - pam_sessions.gatewayId (SET NULL to gateways_v2, nullable). Set when a pool-backed PAM session resolves to a specific gateway, most rows are NULL. Both get partial indexes filtered to the non-NULL rows, same shape as the indexes added in #6965 and #6966. The partial form keeps the index tiny and imposes near-zero write overhead on the live insert path. --- ...fk-indexes-gateway-pool-and-pam-session.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 backend/src/db/migrations/20260624075011_add-fk-indexes-gateway-pool-and-pam-session.ts diff --git a/backend/src/db/migrations/20260624075011_add-fk-indexes-gateway-pool-and-pam-session.ts b/backend/src/db/migrations/20260624075011_add-fk-indexes-gateway-pool-and-pam-session.ts new file mode 100644 index 00000000000..fecb58e1d6f --- /dev/null +++ b/backend/src/db/migrations/20260624075011_add-fk-indexes-gateway-pool-and-pam-session.ts @@ -0,0 +1,53 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +// Two nullable FK columns from gateway-related migrations shipped without covering indexes, +// so the per-row referential-integrity trigger seq-scans the child table on every parent delete. +// +// - dynamic_secrets.gatewayPoolId (RESTRICT to gateway_pools, nullable) +// Set only when a dynamic secret is pinned to a pool. Most rows are NULL → partial index. +// +// - pam_sessions.gatewayId (SET NULL to gateways_v2, nullable) +// Set when a pool-backed PAM session resolves to a specific gateway. Most rows are NULL → partial index. +const FK_INDEXES = [ + { + table: TableName.DynamicSecret, + column: "gatewayPoolId", + name: "dynamic_secrets_gateway_pool_id_idx" + }, + { + table: TableName.PamSession, + column: "gatewayId", + name: "pam_sessions_gateway_id_idx" + } +]; + +const indexExists = async (knex: Knex, indexName: string): Promise => { + const result = await knex.raw(`SELECT 1 FROM pg_indexes WHERE indexname = ?`, [indexName]); + return result.rows.length > 0; +}; + +export async function up(knex: Knex): Promise { + for await (const idx of FK_INDEXES) { + if ( + (await knex.schema.hasTable(idx.table)) && + (await knex.schema.hasColumn(idx.table, idx.column)) && + !(await indexExists(knex, idx.name)) + ) { + await knex.schema.alterTable(idx.table, (t) => { + t.index([idx.column], idx.name, { predicate: knex.whereNotNull(idx.column) }); + }); + } + } +} + +export async function down(knex: Knex): Promise { + for await (const idx of FK_INDEXES) { + if ((await knex.schema.hasTable(idx.table)) && (await indexExists(knex, idx.name))) { + await knex.schema.alterTable(idx.table, (t) => { + t.dropIndex([idx.column], idx.name); + }); + } + } +}