index-advisor.utils.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import { joinSqlFragments, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
  2. import { toast } from 'sonner'
  3. import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query'
  4. import { GetIndexAdvisorResultResponse } from '@/data/database/retrieve-index-advisor-result-query'
  5. import { executeSql } from '@/data/sql/execute-sql-query'
  6. import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
  7. /**
  8. * Gets the required extensions for index advisor
  9. * @param extensions Array of database extensions
  10. * @returns Object containing hypopg, index_advisor, and test_extension extensions if they exist
  11. */
  12. export function getIndexAdvisorExtensions(extensions: DatabaseExtension[] = []) {
  13. const hypopg = extensions.find((ext) => ext.name === 'hypopg')
  14. const indexAdvisor = extensions.find((ext) => ext.name === 'index_advisor')
  15. return { hypopg, indexAdvisor }
  16. }
  17. /**
  18. * Calculates the percentage improvement between before and after costs
  19. *
  20. * @param costBefore Cost before optimization
  21. * @param costAfter Cost after optimization
  22. * @returns Percentage improvement, or 0 if inputs are invalid
  23. */
  24. export function calculateImprovement(
  25. costBefore: number | undefined,
  26. costAfter: number | undefined
  27. ): number {
  28. if (costBefore === undefined || costAfter === undefined) {
  29. return 0
  30. }
  31. const before = Number(costBefore)
  32. const after = Number(costAfter)
  33. if (before <= 0 || before <= after) {
  34. return 0
  35. }
  36. return ((before - after) / before) * 100
  37. }
  38. interface CreateIndexParams {
  39. projectRef?: string
  40. connectionString?: string | null
  41. indexStatements: SafeSqlFragment[]
  42. onSuccess?: () => void
  43. onError?: (error: any) => void
  44. }
  45. /**
  46. * Creates database indexes using the provided SQL statements
  47. *
  48. * @param params Parameters for index creation
  49. * @returns Promise that resolves when the index creation completes
  50. */
  51. export async function createIndexes({
  52. projectRef,
  53. connectionString,
  54. indexStatements,
  55. onSuccess,
  56. onError,
  57. }: CreateIndexParams): Promise<void> {
  58. if (!projectRef) {
  59. const error = new Error('Project ref is required')
  60. if (onError) onError(error)
  61. return Promise.reject(error)
  62. }
  63. if (indexStatements.length === 0) {
  64. const error = new Error('No index statements provided')
  65. if (onError) onError(error)
  66. return Promise.reject(error)
  67. }
  68. try {
  69. await executeSql({
  70. projectRef,
  71. connectionString,
  72. sql: safeSql`${joinSqlFragments(indexStatements, ';\n')};`,
  73. })
  74. toast.success('Successfully created index')
  75. if (onSuccess) onSuccess()
  76. return Promise.resolve()
  77. } catch (error: any) {
  78. toast.error(`Failed to create index: ${error.message}`)
  79. if (onError) onError(error)
  80. return Promise.reject(error)
  81. }
  82. }
  83. /**
  84. * Checks if the index advisor result contains recommendations
  85. *
  86. * @param result The index advisor result object
  87. * @param isSuccess Whether the query was successful
  88. * @returns Whether there are index recommendations available
  89. */
  90. export function hasIndexRecommendations(
  91. result: GetIndexAdvisorResultResponse | undefined | null,
  92. isSuccess: boolean
  93. ): boolean {
  94. return Boolean(isSuccess && result?.index_statements && result.index_statements.length > 0)
  95. }
  96. /**
  97. * Filters out index statements that reference protected schemas
  98. * Index statements are typically in the format: "CREATE INDEX ON schema.table USING ..."
  99. *
  100. * @param indexStatements Array of index statement strings
  101. * @returns Filtered array excluding statements referencing protected schemas
  102. */
  103. export function filterProtectedSchemaIndexStatements(
  104. indexStatements: SafeSqlFragment[]
  105. ): SafeSqlFragment[] {
  106. if (!indexStatements || indexStatements.length === 0) {
  107. return []
  108. }
  109. return indexStatements.filter((statement) => {
  110. // Match patterns like "CREATE INDEX ON schema.table" or "CREATE INDEX ON "schema"."table""
  111. // Handle both quoted and unquoted schema names
  112. const schemaMatch = statement.match(/ON\s+(?:"?(\w+)"?\.|(\w+)\.)/i)
  113. if (!schemaMatch) {
  114. // If we can't parse the schema, keep it (safer to show than hide)
  115. return true
  116. }
  117. // Extract schema name (handle both quoted and unquoted)
  118. const schemaName = schemaMatch[1] || schemaMatch[2]
  119. if (!schemaName) {
  120. return true
  121. }
  122. // Check if schema is in the protected schemas list
  123. return !INTERNAL_SCHEMAS.includes(schemaName.toLowerCase())
  124. })
  125. }
  126. /**
  127. * Filters an index advisor result to remove recommendations for protected schemas
  128. *
  129. * @param result The index advisor result object
  130. * @returns Filtered result with protected schema recommendations removed
  131. */
  132. export function filterProtectedSchemaIndexAdvisorResult(
  133. result: GetIndexAdvisorResultResponse | null | undefined
  134. ): GetIndexAdvisorResultResponse | null {
  135. if (!result || !result.index_statements) {
  136. return result ?? null
  137. }
  138. const filteredStatements = filterProtectedSchemaIndexStatements(result.index_statements)
  139. // If all statements were filtered out, return null
  140. if (filteredStatements.length === 0) {
  141. return null
  142. }
  143. return {
  144. ...result,
  145. index_statements: filteredStatements,
  146. }
  147. }
  148. /**
  149. * Checks if a query involves protected schemas by examining the SQL query text
  150. *
  151. * @param query The SQL query string
  152. * @returns Whether the query involves protected schemas
  153. */
  154. export function queryInvolvesProtectedSchemas(query: string | undefined | null): boolean {
  155. if (!query) return false
  156. const queryLower = query.toLowerCase()
  157. // Check if the query references any protected schemas
  158. // Match patterns like "schema.table", "FROM schema.table", "JOIN schema.table", etc.
  159. return INTERNAL_SCHEMAS.some((schema) => {
  160. // Match schema.table patterns (with or without quotes)
  161. const schemaPattern = new RegExp(
  162. `(?:from|join|update|insert\\s+into|delete\\s+from)\\s+(?:${schema}\\.|"${schema}"\\.)`,
  163. 'i'
  164. )
  165. return schemaPattern.test(queryLower)
  166. })
  167. }