Files
supabase/apps/studio/data/database/retrieve-index-advisor-result-query.ts
oniani1 6e580bffbd fix(studio): respect enabled prop in index advisor query hook (#44588)
Fixes #44586

The `enabled` field in `useGetIndexAdvisorResult` had an OR branch that
bypassed the `enabled` prop for queries starting with `with
pgrst_source`. This meant the query could fire even when `enabled:
false` was passed (e.g. when the index_advisor extension isn't installed
or the result is already prefetched).

Restructures the logic to match the sibling hook in
`retrieve-index-from-select-query.ts`, which correctly ANDs `enabled`
with all conditions using an extracted `isValidQueryForIndexing`
variable.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved query validation in index advisor to more reliably handle SQL
query normalization and ensure consistent query processing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-07 14:18:15 +00:00

97 lines
3.0 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
import { databaseKeys } from './keys'
import { filterProtectedSchemaIndexAdvisorResult } from '@/components/interfaces/QueryPerformance/IndexAdvisor/index-advisor.utils'
import { executeSql } from '@/data/sql/execute-sql-query'
import type { ResponseError, UseCustomQueryOptions } from '@/types'
export type GetIndexAdvisorResultVariables = {
projectRef?: string
connectionString?: string | null
query: string
}
const IndexAdvisorResultSchema = z.object({
errors: z.array(z.string()),
index_statements: z.array(z.string()),
startup_cost_before: z
.number()
.nullable()
.transform((v) => v ?? 0),
startup_cost_after: z
.number()
.nullable()
.transform((v) => v ?? 0),
total_cost_before: z
.number()
.nullable()
.transform((v) => v ?? 0),
total_cost_after: z
.number()
.nullable()
.transform((v) => v ?? 0),
})
export type GetIndexAdvisorResultResponse = z.infer<typeof IndexAdvisorResultSchema>
export async function getIndexAdvisorResult({
projectRef,
connectionString,
query,
}: GetIndexAdvisorResultVariables) {
if (!projectRef) throw new Error('Project ref is required')
const escapedQuery = query.replace(/'/g, "''")
const { result: results } = await executeSql({
projectRef,
connectionString,
sql: `set search_path to public, extensions; select * from index_advisor('${escapedQuery}');`,
})
if (!results || results.length === 0) {
console.error('[index_advisor > getIndexAdvisorResult] No results from index_advisor')
return null
}
const parsed = IndexAdvisorResultSchema.safeParse(results[0])
if (!parsed.success) {
const firstError = parsed.error.errors[0]
const errorPath = firstError.path.length > 0 ? ` at path: ${firstError.path.join('.')}` : ''
console.error(
`Invalid index advisor response${errorPath}: ${firstError.message}. Received: ${JSON.stringify(results[0])}`
)
return null
}
return filterProtectedSchemaIndexAdvisorResult(parsed.data)
}
export type GetIndexAdvisorResultData = Awaited<ReturnType<typeof getIndexAdvisorResult>>
export type GetIndexAdvisorResultError = ResponseError
export const useGetIndexAdvisorResult = <TData = GetIndexAdvisorResultData>(
{ projectRef, connectionString, query }: GetIndexAdvisorResultVariables,
{
enabled = true,
...options
}: UseCustomQueryOptions<GetIndexAdvisorResultData, GetIndexAdvisorResultError, TData> = {}
) => {
const formattedQuery = (query ?? '').trim().toLowerCase()
const isValidQueryForIndexing =
formattedQuery.startsWith('select') || formattedQuery.startsWith('with pgrst_source')
return useQuery<GetIndexAdvisorResultData, GetIndexAdvisorResultError, TData>({
queryKey: databaseKeys.indexAdvisorFromQuery(projectRef, query),
queryFn: () => getIndexAdvisorResult({ projectRef, connectionString, query }),
retry: false,
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof query !== 'undefined' &&
isValidQueryForIndexing,
...options,
})
}