mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
## 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 --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45998) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
117 lines
4.3 KiB
TypeScript
117 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { PRESET_CONFIG } from './Reports.constants'
|
|
import { Presets } from './Reports.types'
|
|
|
|
const queries = PRESET_CONFIG[Presets.QUERY_PERFORMANCE].queries as Record<
|
|
string,
|
|
{ safeSql: (...args: any[]) => string }
|
|
>
|
|
|
|
const queryNames = [
|
|
'mostFrequentlyInvoked',
|
|
'mostTimeConsuming',
|
|
'slowestExecutionTime',
|
|
'unified',
|
|
'slowQueriesCount',
|
|
'queryMetrics',
|
|
] as const
|
|
|
|
describe('QUERY_PERFORMANCE SQL queries', () => {
|
|
describe('calls > 0 base filter', () => {
|
|
it.each(queryNames)('%s includes calls > 0 without user filters', (name) => {
|
|
const sql = queries[name].safeSql([], undefined, undefined)
|
|
expect(sql).toContain('calls > 0')
|
|
})
|
|
|
|
it.each(queryNames)('%s still includes calls > 0 when user filters are provided', (name) => {
|
|
const sql = queries[name].safeSql([], "WHERE auth.rolname in ('postgres')", undefined)
|
|
expect(sql).toContain('calls > 0')
|
|
})
|
|
})
|
|
|
|
describe('WHERE clause composition with user filters', () => {
|
|
const userWhere = "WHERE auth.rolname in ('postgres')"
|
|
|
|
it.each([
|
|
'mostFrequentlyInvoked',
|
|
'mostTimeConsuming',
|
|
'slowestExecutionTime',
|
|
'unified',
|
|
] as const)('%s: user filters appended with AND (no duplicate WHERE)', (name) => {
|
|
const sql = queries[name].safeSql([], userWhere, undefined)
|
|
// Should not have two WHERE keywords in a row / duplicate WHERE
|
|
expect(sql).not.toMatch(/WHERE\s+.*WHERE/s)
|
|
// User filter condition should be present
|
|
expect(sql).toContain("auth.rolname in ('postgres')")
|
|
// Should use AND to join base filter and user filter
|
|
expect(sql).toMatch(/calls > 0\s+AND/)
|
|
})
|
|
|
|
it('queryMetrics: user filters appended with AND (no duplicate WHERE in FROM clause)', () => {
|
|
const sql = queries.queryMetrics.safeSql([], userWhere, undefined)
|
|
// queryMetrics uses COUNT(*) FILTER (WHERE ...) which is valid SQL and not a duplicate
|
|
// Just verify the base filter + user filter are correctly composed
|
|
expect(sql).toContain("auth.rolname in ('postgres')")
|
|
expect(sql).toMatch(/calls > 0\s+AND/)
|
|
// Should not have two WHERE keywords after the FROM keyword
|
|
expect(sql).not.toMatch(/FROM[\s\S]*WHERE[\s\S]*WHERE[\s\S]*WHERE/s)
|
|
})
|
|
|
|
it.each([
|
|
'mostFrequentlyInvoked',
|
|
'mostTimeConsuming',
|
|
'slowestExecutionTime',
|
|
'unified',
|
|
'queryMetrics',
|
|
] as const)('%s: no trailing junk when no user filters', (name) => {
|
|
const sql = queries[name].safeSql([], undefined, undefined)
|
|
// Should not have a dangling undefined or 'WHERE' with nothing after the base filter
|
|
expect(sql).not.toContain('undefined')
|
|
expect(sql).not.toMatch(/calls > 0\s+AND\s+(ORDER|LIMIT|$)/im)
|
|
})
|
|
})
|
|
|
|
describe('slowQueriesCount bug fix', () => {
|
|
it('uses table alias "statements"', () => {
|
|
const sql = queries.slowQueriesCount.safeSql()
|
|
expect(sql).toContain('pg_stat_statements as statements')
|
|
})
|
|
|
|
it('filters by mean_exec_time using the alias', () => {
|
|
const sql = queries.slowQueriesCount.safeSql()
|
|
expect(sql).toContain('statements.mean_exec_time > 1000')
|
|
})
|
|
})
|
|
|
|
describe('window function elimination', () => {
|
|
it('unified uses grand_total CTE instead of OVER()', () => {
|
|
const sql = queries.unified.safeSql([], undefined, undefined)
|
|
expect(sql).toContain('grand_total')
|
|
expect(sql).not.toContain('OVER()')
|
|
})
|
|
|
|
it('mostTimeConsuming uses grand_total CTE instead of OVER()', () => {
|
|
const sql = queries.mostTimeConsuming.safeSql([], undefined, undefined)
|
|
expect(sql).toContain('grand_total')
|
|
expect(sql).not.toContain('OVER()')
|
|
})
|
|
|
|
it('grand_total CTE references calls > 0', () => {
|
|
const sql = queries.unified.safeSql([], undefined, undefined)
|
|
expect(sql).toMatch(/grand_total[\s\S]*calls > 0/)
|
|
})
|
|
})
|
|
|
|
describe('multiple user filters', () => {
|
|
it('handles multiple user filter conditions', () => {
|
|
const multiWhere = "WHERE auth.rolname in ('postgres') AND statements.calls >= 10"
|
|
const sql = queries.mostFrequentlyInvoked.safeSql([], multiWhere, undefined)
|
|
expect(sql).toContain('calls > 0')
|
|
expect(sql).toContain("auth.rolname in ('postgres')")
|
|
expect(sql).toContain('statements.calls >= 10')
|
|
expect(sql).not.toMatch(/WHERE\s+.*WHERE/s)
|
|
})
|
|
})
|
|
})
|