import { joinSqlFragments, safeSql, type SafeSqlFragment } from '@supabase/pg-meta' import { toast } from 'sonner' import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query' import { GetIndexAdvisorResultResponse } from '@/data/database/retrieve-index-advisor-result-query' import { executeSql } from '@/data/sql/execute-sql-query' import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas' /** * Gets the required extensions for index advisor * @param extensions Array of database extensions * @returns Object containing hypopg, index_advisor, and test_extension extensions if they exist */ export function getIndexAdvisorExtensions(extensions: DatabaseExtension[] = []) { const hypopg = extensions.find((ext) => ext.name === 'hypopg') const indexAdvisor = extensions.find((ext) => ext.name === 'index_advisor') return { hypopg, indexAdvisor } } /** * Calculates the percentage improvement between before and after costs * * @param costBefore Cost before optimization * @param costAfter Cost after optimization * @returns Percentage improvement, or 0 if inputs are invalid */ export function calculateImprovement( costBefore: number | undefined, costAfter: number | undefined ): number { if (costBefore === undefined || costAfter === undefined) { return 0 } const before = Number(costBefore) const after = Number(costAfter) if (before <= 0 || before <= after) { return 0 } return ((before - after) / before) * 100 } interface CreateIndexParams { projectRef?: string connectionString?: string | null indexStatements: SafeSqlFragment[] onSuccess?: () => void onError?: (error: any) => void } /** * Creates database indexes using the provided SQL statements * * @param params Parameters for index creation * @returns Promise that resolves when the index creation completes */ export async function createIndexes({ projectRef, connectionString, indexStatements, onSuccess, onError, }: CreateIndexParams): Promise { if (!projectRef) { const error = new Error('Project ref is required') if (onError) onError(error) return Promise.reject(error) } if (indexStatements.length === 0) { const error = new Error('No index statements provided') if (onError) onError(error) return Promise.reject(error) } try { await executeSql({ projectRef, connectionString, sql: safeSql`${joinSqlFragments(indexStatements, ';\n')};`, }) toast.success('Successfully created index') if (onSuccess) onSuccess() return Promise.resolve() } catch (error: any) { toast.error(`Failed to create index: ${error.message}`) if (onError) onError(error) return Promise.reject(error) } } /** * Checks if the index advisor result contains recommendations * * @param result The index advisor result object * @param isSuccess Whether the query was successful * @returns Whether there are index recommendations available */ export function hasIndexRecommendations( result: GetIndexAdvisorResultResponse | undefined | null, isSuccess: boolean ): boolean { return Boolean(isSuccess && result?.index_statements && result.index_statements.length > 0) } /** * Filters out index statements that reference protected schemas * Index statements are typically in the format: "CREATE INDEX ON schema.table USING ..." * * @param indexStatements Array of index statement strings * @returns Filtered array excluding statements referencing protected schemas */ export function filterProtectedSchemaIndexStatements( indexStatements: SafeSqlFragment[] ): SafeSqlFragment[] { if (!indexStatements || indexStatements.length === 0) { return [] } return indexStatements.filter((statement) => { // Match patterns like "CREATE INDEX ON schema.table" or "CREATE INDEX ON "schema"."table"" // Handle both quoted and unquoted schema names const schemaMatch = statement.match(/ON\s+(?:"?(\w+)"?\.|(\w+)\.)/i) if (!schemaMatch) { // If we can't parse the schema, keep it (safer to show than hide) return true } // Extract schema name (handle both quoted and unquoted) const schemaName = schemaMatch[1] || schemaMatch[2] if (!schemaName) { return true } // Check if schema is in the protected schemas list return !INTERNAL_SCHEMAS.includes(schemaName.toLowerCase()) }) } /** * Filters an index advisor result to remove recommendations for protected schemas * * @param result The index advisor result object * @returns Filtered result with protected schema recommendations removed */ export function filterProtectedSchemaIndexAdvisorResult( result: GetIndexAdvisorResultResponse | null | undefined ): GetIndexAdvisorResultResponse | null { if (!result || !result.index_statements) { return result ?? null } const filteredStatements = filterProtectedSchemaIndexStatements(result.index_statements) // If all statements were filtered out, return null if (filteredStatements.length === 0) { return null } return { ...result, index_statements: filteredStatements, } } /** * Checks if a query involves protected schemas by examining the SQL query text * * @param query The SQL query string * @returns Whether the query involves protected schemas */ export function queryInvolvesProtectedSchemas(query: string | undefined | null): boolean { if (!query) return false const queryLower = query.toLowerCase() // Check if the query references any protected schemas // Match patterns like "schema.table", "FROM schema.table", "JOIN schema.table", etc. return INTERNAL_SCHEMAS.some((schema) => { // Match schema.table patterns (with or without quotes) const schemaPattern = new RegExp( `(?:from|join|update|insert\\s+into|delete\\s+from)\\s+(?:${schema}\\.|"${schema}"\\.)`, 'i' ) return schemaPattern.test(queryLower) }) }