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 -->
This commit is contained in:
Charis
2026-05-15 14:50:38 -04:00
committed by GitHub
parent e925385415
commit 2d4e87f579
17 changed files with 244 additions and 132 deletions

View File

@@ -1,4 +1,11 @@
import { ident, literal } from '@supabase/pg-meta/src/pg-format'
import {
ident,
joinSqlFragments,
keyword,
literal,
safeSql,
type SafeSqlFragment,
} from '@supabase/pg-meta'
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'
import { PRESET_CONFIG } from '../Reports/Reports.constants'
@@ -54,36 +61,47 @@ export function generateQueryPerformanceSql({
(orderBy.order === 'asc' || orderBy.order === 'desc')
const orderBySql = isValidOrderBy
? `ORDER BY ${ident(orderBy!.column)} ${orderBy!.order}`
? safeSql`ORDER BY ${ident(orderBy!.column)} ${keyword(orderBy!.order)}`
: undefined
const whereConditions = []
const whereConditions: SafeSqlFragment[] = []
if (roles.length > 0) {
whereConditions.push(`auth.rolname in (${roles.map((r) => `${literal(r)}`).join(', ')})`)
whereConditions.push(
safeSql`auth.rolname in (${joinSqlFragments(
roles.map((r) => literal(r)),
', '
)})`
)
}
if (searchQuery.length > 0) {
whereConditions.push(`statements.query ~* ${literal(searchQuery)}`)
whereConditions.push(safeSql`statements.query ~* ${literal(searchQuery)}`)
}
if (sources.includes('dashboard') && !sources.includes('non-dashboard')) {
whereConditions.push(`statements.query ~* 'source: dashboard'`)
whereConditions.push(safeSql`statements.query ~* 'source: dashboard'`)
}
if (sources.includes('non-dashboard') && !sources.includes('dashboard')) {
whereConditions.push(`statements.query !~* 'source: dashboard'`)
whereConditions.push(safeSql`statements.query !~* 'source: dashboard'`)
}
if (Number.isFinite(minCalls) && minCalls > 0) {
whereConditions.push(`statements.calls >= ${minCalls}`)
whereConditions.push(safeSql`statements.calls >= ${literal(minCalls)}`)
}
if (Number.isFinite(minTotalTime) && minTotalTime > 0) {
whereConditions.push(
`(statements.total_exec_time + statements.total_plan_time) >= ${minTotalTime}`
safeSql`(statements.total_exec_time + statements.total_plan_time) >= ${literal(minTotalTime)}`
)
}
const whereSql = whereConditions.join(' AND ')
const whereSql = joinSqlFragments(whereConditions, ' AND ')
const sql = baseSQL.sql(
if (baseSQL.queryType !== 'db') {
throw new Error(
`Query performance presets must be db queries; got ${baseSQL.queryType} for preset ${preset}`
)
}
const sql = baseSQL.safeSql(
[],
whereSql.length > 0 ? `WHERE ${whereSql}` : undefined,
whereSql.length > 0 ? safeSql`WHERE ${whereSql}` : undefined,
orderBySql,
runIndexAdvisor,
filterIndexAdvisor,