Files
supabase/apps/studio/components/interfaces/QueryInsights/hooks/useQueryInsightsIssues.utils.test.ts
Charis 2d4e87f579 studio: SafeSql for reports, query performance, privileges (4/7) (#45998)
## Summary

Part 4 of the SafeSql migration stack
([#45897](https://github.com/supabase/supabase/pull/45897),
[#45903](https://github.com/supabase/supabase/pull/45903),
[#45990](https://github.com/supabase/supabase/pull/45990), this PR, …).

Converts the remaining reports, query performance, observability, index
advisor, and privileges call sites of `executeSql` to produce
`SafeSqlFragment` values. The `ReportQuery.sql` field flips from
`string` to `SafeSqlFragment`, which cascades into every consumer —
landed here atomically so each branch typechecks cleanly.

Touched areas:

- `interfaces/Reports/*` — `ReportQuery.sql: SafeSqlFragment`, plus all
report definitions/utilities updated
- `interfaces/QueryPerformance/useQueryPerformanceQuery.ts`
- `interfaces/Database/IndexAdvisor/*` and
`data/database/{table-index-advisor,retrieve-index-advisor-result}-query.ts`
-
`data/privileges/{table-api-access,update-exposed-entities}-mutation.ts`
- `interfaces/Storage/StoragePolicies/StoragePolicies.tsx`
- `hooks/analytics/useDbQuery.tsx`
- `Observability/useSlowQueriesCount.ts` +
`useQueryInsightsIssues.utils.test.ts`

## Test plan

- [x] `pnpm typecheck` passes
- [x] `useQueryInsightsIssues.utils.test.ts` passes
- [x] Dev-server smoke test: reports pages, query performance, index
advisor, storage policies

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

* **Refactor**
* Reworked SQL construction and typings across reporting, query
performance, index advisor, and privilege features to use safer SQL
fragments, improving reliability and preventing query composition
issues.
* **Types**
* Reporting query types were split to distinguish database vs. logs
queries, enabling correct handling and validation.
* **Docs/Utils**
  * Added a helper to consistently generate logs SQL for report hooks.
* **Tests**
  * Updated tests to exercise the new SQL-building API.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45998)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-15 14:50:38 -04:00

102 lines
3.1 KiB
TypeScript

import { safeSql } from '@supabase/pg-meta'
import { describe, expect, it, vi } from 'vitest'
import { hasIndexRecommendations } from '../../QueryPerformance/IndexAdvisor/index-advisor.utils'
import type { QueryPerformanceRow } from '../../QueryPerformance/QueryPerformance.types'
import { classifyQuery } from './useQueryInsightsIssues.utils'
vi.mock('../../QueryPerformance/IndexAdvisor/index-advisor.utils', () => ({
hasIndexRecommendations: vi.fn(),
}))
const baseRow: QueryPerformanceRow = {
query: safeSql`SELECT * FROM users`,
calls: 10,
mean_time: 50,
min_time: 10,
max_time: 200,
total_time: 500,
prop_total_time: 5,
rows_read: 100,
cache_hit_rate: 1,
rolname: 'postgres',
application_name: 'test',
index_advisor_result: null,
_total_cache_hits: 0,
_total_cache_misses: 0,
}
describe('classifyQuery', () => {
it('returns error when index_advisor_result has errors', () => {
const row = {
...baseRow,
index_advisor_result: {
errors: ['some error'],
index_statements: [],
startup_cost_before: 0,
startup_cost_after: 0,
total_cost_before: 0,
total_cost_after: 0,
},
}
const result = classifyQuery(row)
expect(result.issueType).toBe('error')
expect(result.hint).toBe('some error')
})
it('returns index when hasIndexRecommendations is true', () => {
vi.mocked(hasIndexRecommendations).mockReturnValue(true)
const row = {
...baseRow,
index_advisor_result: {
errors: [],
index_statements: [safeSql`CREATE INDEX ...`],
startup_cost_before: 0,
startup_cost_after: 0,
total_cost_before: 0,
total_cost_after: 0,
},
}
const result = classifyQuery(row)
expect(result.issueType).toBe('index')
expect(result.hint).toContain('Missing index')
vi.mocked(hasIndexRecommendations).mockReset()
})
it('returns slow when mean_time exceeds threshold', () => {
vi.mocked(hasIndexRecommendations).mockReturnValue(false)
const row = { ...baseRow, mean_time: 300 }
const result = classifyQuery(row)
expect(result.issueType).toBe('slow')
expect(result.hint).toBe('Abnormally slow query detected')
vi.mocked(hasIndexRecommendations).mockReset()
})
it('returns null issue for healthy queries', () => {
vi.mocked(hasIndexRecommendations).mockReturnValue(false)
const row = { ...baseRow, mean_time: 50 }
const result = classifyQuery(row)
expect(result.issueType).toBeNull()
expect(result.hint).toBe('')
vi.mocked(hasIndexRecommendations).mockReset()
})
it('errors take priority over index recommendations', () => {
vi.mocked(hasIndexRecommendations).mockReturnValue(true)
const row = {
...baseRow,
index_advisor_result: {
errors: ['critical error'],
index_statements: [safeSql`CREATE INDEX ...`],
startup_cost_before: 0,
startup_cost_after: 0,
total_cost_before: 0,
total_cost_after: 0,
},
}
const result = classifyQuery(row)
expect(result.issueType).toBe('error')
vi.mocked(hasIndexRecommendations).mockReset()
})
})