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,9 +1,10 @@
import { safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
import { useMemo } from 'react'
import useDbQuery from '@/hooks/analytics/useDbQuery'
export function buildSlowQueriesCountSql(): string {
return `
export function buildSlowQueriesCountSql(): SafeSqlFragment {
return safeSql`
-- observability-slow-queries-count
set search_path to public, extensions;

View File

@@ -50,7 +50,7 @@ describe('classifyQuery', () => {
...baseRow,
index_advisor_result: {
errors: [],
index_statements: ['CREATE INDEX ...'],
index_statements: [safeSql`CREATE INDEX ...`],
startup_cost_before: 0,
startup_cost_after: 0,
total_cost_before: 0,
@@ -87,7 +87,7 @@ describe('classifyQuery', () => {
...baseRow,
index_advisor_result: {
errors: ['critical error'],
index_statements: ['CREATE INDEX ...'],
index_statements: [safeSql`CREATE INDEX ...`],
startup_cost_before: 0,
startup_cost_after: 0,
total_cost_before: 0,

View File

@@ -1,3 +1,4 @@
import { joinSqlFragments, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
import { toast } from 'sonner'
import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query'
@@ -44,7 +45,7 @@ export function calculateImprovement(
interface CreateIndexParams {
projectRef?: string
connectionString?: string | null
indexStatements: string[]
indexStatements: SafeSqlFragment[]
onSuccess?: () => void
onError?: (error: any) => void
}
@@ -78,7 +79,7 @@ export async function createIndexes({
await executeSql({
projectRef,
connectionString,
sql: indexStatements.join(';\n') + ';',
sql: safeSql`${joinSqlFragments(indexStatements, ';\n')};`,
})
toast.success('Successfully created index')
@@ -112,7 +113,9 @@ export function hasIndexRecommendations(
* @param indexStatements Array of index statement strings
* @returns Filtered array excluding statements referencing protected schemas
*/
export function filterProtectedSchemaIndexStatements(indexStatements: string[]): string[] {
export function filterProtectedSchemaIndexStatements(
indexStatements: SafeSqlFragment[]
): SafeSqlFragment[] {
if (!indexStatements || indexStatements.length === 0) {
return []
}

View File

@@ -1,3 +1,4 @@
import { safeSql } from '@supabase/pg-meta/src/pg-format'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { RefreshCw, RotateCcw, X } from 'lucide-react'
import { parseAsString, useQueryStates } from 'nuqs'
@@ -297,7 +298,7 @@ export const WithStatements = ({
await executeSql({
projectRef: project?.ref,
connectionString,
sql: `SELECT pg_stat_statements_reset();`,
sql: safeSql`SELECT pg_stat_statements_reset();`,
})
handleRefresh()
setShowResetgPgStatStatements(false)

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,

View File

@@ -1,3 +1,4 @@
import { literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta'
import dayjs from 'dayjs'
import type { DatetimeHelper } from '../Settings/Logs/Logs.types'
@@ -78,6 +79,10 @@ export const DEFAULT_QUERY_PARAMS = {
iso_timestamp_end: REPORTS_DATEPICKER_HELPERS[0].calcTo(),
}
function rewriteWhereToAnd(sql: SafeSqlFragment): SafeSqlFragment {
return sql.replace(/^WHERE/, 'AND') as SafeSqlFragment
}
export const generateRegexpWhere = (filters: ReportFilterItem[], prepend = true) => {
if (filters.length === 0) return ''
const conditions = filters
@@ -380,7 +385,13 @@ limit 12
queries: {
mostFrequentlyInvoked: {
queryType: 'db',
sql: (_params, where, orderBy, runIndexAdvisor = false, _filterIndexAdvisor = false) => `
safeSql: (
_params,
where,
orderBy,
runIndexAdvisor = false,
_filterIndexAdvisor = false
) => safeSql`
-- reports-query-performance-most-frequently-invoked
set search_path to public, extensions;
@@ -410,7 +421,7 @@ select
else 0
end as cache_hit_rate${
runIndexAdvisor
? `,
? safeSql`,
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
@@ -426,18 +437,24 @@ select
)
else null
end as index_advisor_result`
: ''
: safeSql``
}
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
-- skip queries that were never actually executed
WHERE statements.calls > 0 ${where ? where.replace(/^WHERE/, 'AND') : ''}
${orderBy || 'order by statements.calls desc'}
WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
${orderBy || safeSql`order by statements.calls desc`}
limit 20`,
},
mostTimeConsuming: {
queryType: 'db',
sql: (_, where, orderBy, runIndexAdvisor = false, _filterIndexAdvisor = false) => `
safeSql: (
_,
where,
orderBy,
runIndexAdvisor = false,
_filterIndexAdvisor = false
) => safeSql`
-- reports-query-performance-most-time-consuming
set search_path to public, extensions;
@@ -459,7 +476,7 @@ select
0
) as prop_total_time${
runIndexAdvisor
? `,
? safeSql`,
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
@@ -475,18 +492,24 @@ select
)
else null
end as index_advisor_result`
: ''
: safeSql``
}
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
-- skip queries that were never actually executed
WHERE statements.calls > 0 ${where ? where.replace(/^WHERE/, 'AND') : ''}
${orderBy || 'order by total_time desc'}
WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
${orderBy || safeSql`order by total_time desc`}
limit 20`,
},
slowestExecutionTime: {
queryType: 'db',
sql: (_params, where, orderBy, runIndexAdvisor = false, _filterIndexAdvisor = false) => `
safeSql: (
_params,
where,
orderBy,
runIndexAdvisor = false,
_filterIndexAdvisor = false
) => safeSql`
-- reports-query-performance-slowest-execution-time
set search_path to public, extensions;
@@ -506,7 +529,7 @@ select
-- mean_time,
coalesce(statements.rows::numeric / nullif(statements.calls, 0), 0) as avg_rows${
runIndexAdvisor
? `,
? safeSql`,
case
when (lower(statements.query) like 'select%' or lower(statements.query) like 'with pgrst%')
then (
@@ -522,18 +545,18 @@ select
)
else null
end as index_advisor_result`
: ''
: safeSql``
}
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
-- skip queries that were never actually executed
WHERE statements.calls > 0 ${where ? where.replace(/^WHERE/, 'AND') : ''}
${orderBy || 'order by max_time desc'}
WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
${orderBy || safeSql`order by max_time desc`}
limit 20`,
},
queryHitRate: {
queryType: 'db',
sql: (_params) => `-- reports-query-performance-cache-and-index-hit-rate
safeSql: (_params) => safeSql`-- reports-query-performance-cache-and-index-hit-rate
select
'index hit rate' as name,
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read),0) as ratio
@@ -546,7 +569,7 @@ select
},
unified: {
queryType: 'db',
sql: (
safeSql: (
_params,
where,
orderBy,
@@ -565,7 +588,7 @@ select
const baseCteLimit = runIndexAdvisor
? Math.min(baseScanTarget, INDEX_ADVISOR_SCAN_CAP)
: baseScanTarget
const baseQuery = `
const baseQuery = safeSql`
-- reports-query-performance-unified
set search_path to public, extensions;
@@ -602,15 +625,15 @@ select
from pg_stat_statements as statements
inner join pg_authid as auth on statements.userid = auth.oid
-- skip queries that were never actually executed
WHERE statements.calls > 0 ${where ? where.replace(/^WHERE/, 'AND') : ''}
${orderBy || 'order by total_time desc'}
${baseCteLimit !== null ? `limit ${baseCteLimit}` : ''}
WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
${orderBy || safeSql`order by total_time desc`}
${baseCteLimit !== null ? safeSql`limit ${literal(baseCteLimit)}` : safeSql``}
),
query_results as (
select
base.*${
runIndexAdvisor
? `,
? safeSql`,
case
when (lower(base.query) like 'select%' or lower(base.query) like 'with pgrst%')
then (
@@ -626,22 +649,22 @@ select
)
else null
end as index_advisor_result`
: ''
: safeSql``
}
from base
)
select *
from query_results
${filterIndexAdvisor && runIndexAdvisor ? `where (index_advisor_result->>'has_suggestion')::boolean = true` : ''}
${orderBy || 'order by total_time desc'}
limit ${pageSize} offset ${offset}`
${filterIndexAdvisor && runIndexAdvisor ? safeSql`where (index_advisor_result->>'has_suggestion')::boolean = true` : safeSql``}
${orderBy || safeSql`order by total_time desc`}
limit ${literal(pageSize)} offset ${literal(offset)}`
return baseQuery
},
},
slowQueriesCount: {
queryType: 'db',
sql: () => `
safeSql: () => safeSql`
-- reports-query-performance-slow-queries-count
set search_path to public, extensions;
@@ -654,7 +677,13 @@ select
},
queryMetrics: {
queryType: 'db',
sql: (_params, where, orderBy, _runIndexAdvisor = false, _filterIndexAdvisor = false) => `
safeSql: (
_params,
where,
orderBy,
_runIndexAdvisor = false,
_filterIndexAdvisor = false
) => safeSql`
-- reports-query-performance-metrics
set search_path to public, extensions;
@@ -670,8 +699,8 @@ select
) || '%' as cache_hit_rate
FROM pg_stat_statements as statements
-- skip queries that were never actually executed
WHERE statements.calls > 0 ${where ? where.replace(/^WHERE/, 'AND') : ''}
${orderBy || ''}`,
WHERE statements.calls > 0 ${where ? rewriteWhereToAnd(where) : safeSql``}
${orderBy || safeSql``}`,
},
},
},
@@ -680,7 +709,7 @@ select
queries: {
largeObjects: {
queryType: 'db',
sql: (_) => `-- reports-database-large-objects
safeSql: (_) => safeSql`-- reports-database-large-objects
SELECT
SCHEMA_NAME,
relname,

View File

@@ -5,7 +5,7 @@ import { Presets } from './Reports.types'
const queries = PRESET_CONFIG[Presets.QUERY_PERFORMANCE].queries as Record<
string,
{ sql: (...args: any[]) => string }
{ safeSql: (...args: any[]) => string }
>
const queryNames = [
@@ -20,12 +20,12 @@ const queryNames = [
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].sql([], undefined, undefined)
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].sql([], "WHERE auth.rolname in ('postgres')", undefined)
const sql = queries[name].safeSql([], "WHERE auth.rolname in ('postgres')", undefined)
expect(sql).toContain('calls > 0')
})
})
@@ -39,7 +39,7 @@ describe('QUERY_PERFORMANCE SQL queries', () => {
'slowestExecutionTime',
'unified',
] as const)('%s: user filters appended with AND (no duplicate WHERE)', (name) => {
const sql = queries[name].sql([], userWhere, undefined)
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
@@ -49,7 +49,7 @@ describe('QUERY_PERFORMANCE SQL queries', () => {
})
it('queryMetrics: user filters appended with AND (no duplicate WHERE in FROM clause)', () => {
const sql = queries.queryMetrics.sql([], userWhere, undefined)
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')")
@@ -65,7 +65,7 @@ describe('QUERY_PERFORMANCE SQL queries', () => {
'unified',
'queryMetrics',
] as const)('%s: no trailing junk when no user filters', (name) => {
const sql = queries[name].sql([], undefined, undefined)
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)
@@ -74,31 +74,31 @@ describe('QUERY_PERFORMANCE SQL queries', () => {
describe('slowQueriesCount bug fix', () => {
it('uses table alias "statements"', () => {
const sql = queries.slowQueriesCount.sql()
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.sql()
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.sql([], undefined, undefined)
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.sql([], undefined, undefined)
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.sql([], undefined, undefined)
const sql = queries.unified.safeSql([], undefined, undefined)
expect(sql).toMatch(/grand_total[\s\S]*calls > 0/)
})
})
@@ -106,7 +106,7 @@ describe('QUERY_PERFORMANCE SQL queries', () => {
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.sql([], multiWhere, undefined)
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')

View File

@@ -1,3 +1,5 @@
import type { SafeSqlFragment } from '@supabase/pg-meta'
import type { ResponseError } from '@/types'
export enum Presets {
@@ -19,8 +21,8 @@ export interface PresetConfig {
}
export type BaseQueries<Keys extends string> = Record<Keys, ReportQuery>
export interface ReportQuery {
queryType: ReportQueryType
export interface ReportQueryLogs {
queryType: 'logs'
sql: (
filters: ReportFilterItem[],
where?: string,
@@ -32,7 +34,22 @@ export interface ReportQuery {
) => string
}
export type ReportQueryType = 'db' | 'logs'
export interface ReportQueryDb {
queryType: 'db'
safeSql: (
filters: ReportFilterItem[],
where?: SafeSqlFragment,
orderBy?: SafeSqlFragment,
runIndexAdvisor?: boolean,
filterIndexAdvisor?: boolean,
page?: number,
pageSize?: number
) => SafeSqlFragment
}
export type ReportQuery = ReportQueryLogs | ReportQueryDb
export type ReportQueryType = ReportQuery['queryType']
export interface StatusCodesDatum {
timestamp: number

View File

@@ -1,6 +1,11 @@
import dayjs from 'dayjs'
import { type BaseQueries, type PresetConfig, type ReportQuery } from './Reports.types'
import {
type BaseQueries,
type PresetConfig,
type ReportFilterItem,
type ReportQuery,
} from './Reports.types'
import {
isUnixMicro,
unixMicroToIsoTimestamp,
@@ -28,25 +33,29 @@ export const queriesFactory = <T extends string>(
queries: BaseQueries<T>,
projectRef: string
): PresetHooks => {
const hooks: PresetHooks = Object.entries<ReportQuery>(queries).reduce(
(acc, [k, { sql, queryType }]) => {
if (queryType === 'db') {
return {
...acc,
[k]: () => useDbQuery({ sql }),
}
} else {
return {
...acc,
[k]: () => useLogsQuery(projectRef),
}
const hooks: PresetHooks = Object.entries<ReportQuery>(queries).reduce((acc, [k, query]) => {
if (query.queryType === 'db') {
return {
...acc,
[k]: () => useDbQuery({ sql: query.safeSql }),
}
},
{}
)
} else {
return {
...acc,
[k]: () => useLogsQuery(projectRef),
}
}
}, {})
return hooks
}
export function getLogsSql(query: ReportQuery, filters: ReportFilterItem[]): string {
if (query.queryType !== 'logs') {
throw new Error(`Expected logs query, got ${query.queryType}`)
}
return query.sql(filters)
}
/**
* Formats a timestamp to a human readable format in UTC
*

View File

@@ -19,6 +19,10 @@ import { formatPoliciesForStorage, UNGROUPED_POLICY_SYMBOL } from '../Storage.ut
import { StoragePoliciesBucketRow } from './StoragePoliciesBucketRow'
import { BucketsPolicies, type SelectBucketPolicyForAction } from './StoragePoliciesBucketsSection'
import { StoragePoliciesEditPolicyModal } from './StoragePoliciesEditPolicyModal'
import type {
PostgresPolicyCreatePayload,
PostgresPolicyUpdatePayload,
} from '@/components/interfaces/Auth/Policies/Policies.types'
import { PolicyEditorModal } from '@/components/interfaces/Auth/Policies/PolicyEditorModal'
import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
@@ -155,7 +159,7 @@ export const StoragePolicies = () => {
Functions that involve the CRUD for policies
For each API call within the Promise.all, return true if an error occurred, else return false
*/
const onCreatePolicies = async (payloads: any[]) => {
const onCreatePolicies = async (payloads: PostgresPolicyCreatePayload[]) => {
if (!project) {
console.error('Project is required')
return true
@@ -181,7 +185,7 @@ export const StoragePolicies = () => {
}
}
const onCreatePolicy = async (payload: any) => {
const onCreatePolicy = async (payload: PostgresPolicyCreatePayload) => {
if (!project) {
console.error('Project is required')
return true
@@ -200,7 +204,7 @@ export const StoragePolicies = () => {
}
}
const onUpdatePolicy = async (payload: any) => {
const onUpdatePolicy = async (payload: PostgresPolicyUpdatePayload) => {
if (!project) {
console.error('Project is required')
return true

View File

@@ -1,3 +1,4 @@
import { literal, safeSql, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
@@ -33,7 +34,15 @@ const IndexAdvisorResultSchema = z.object({
.transform((v) => v ?? 0),
})
export type GetIndexAdvisorResultResponse = z.infer<typeof IndexAdvisorResultSchema>
export type GetIndexAdvisorResultResponse = z.infer<typeof IndexAdvisorResultSchema> & {
index_statements: SafeSqlFragment[]
}
function markDatabaseSqlSafe(
indexAdvisorResult: z.infer<typeof IndexAdvisorResultSchema>
): GetIndexAdvisorResultResponse {
return indexAdvisorResult as GetIndexAdvisorResultResponse
}
export async function getIndexAdvisorResult({
projectRef,
@@ -42,12 +51,10 @@ export async function getIndexAdvisorResult({
}: GetIndexAdvisorResultVariables) {
if (!projectRef) throw new Error('Project ref is required')
const escapedQuery = query.replace(/'/g, "''")
const { result: results } = await executeSql({
projectRef,
connectionString,
sql: `set search_path to public, extensions; select * from index_advisor('${escapedQuery}');`,
sql: safeSql`set search_path to public, extensions; select * from index_advisor(${literal(query)});`,
})
if (!results || results.length === 0) {
@@ -65,7 +72,7 @@ export async function getIndexAdvisorResult({
return null
}
return filterProtectedSchemaIndexAdvisorResult(parsed.data)
return filterProtectedSchemaIndexAdvisorResult(markDatabaseSqlSafe(parsed.data))
}
export type GetIndexAdvisorResultData = Awaited<ReturnType<typeof getIndexAdvisorResult>>

View File

@@ -18,7 +18,7 @@ export type IndexAdvisorSuggestion = {
calls: number
total_time: number
mean_time: number
index_statements: string[]
index_statements: SafeSqlFragment[]
startup_cost_before: number
startup_cost_after: number
total_cost_before: number
@@ -66,6 +66,18 @@ function extractColumnsFromIndexStatements(indexStatements: string[]): string[]
return Array.from(columns)
}
interface GetTableIndexAdvisorSuggestionsResponse {
query: SafeSqlFragment
calls: number
total_time: number
mean_time: number
index_statements: SafeSqlFragment[]
startup_cost_before: number
startup_cost_after: number
total_cost_before: number
total_cost_after: number
}
export async function getTableIndexAdvisorSuggestions({
projectRef,
connectionString,
@@ -78,19 +90,7 @@ export async function getTableIndexAdvisorSuggestions({
const sql = getTableIndexAdvisorSql(schema, table)
const { result } = await executeSql<
Array<{
query: string
calls: number
total_time: number
mean_time: number
index_statements: string[]
startup_cost_before: number
startup_cost_after: number
total_cost_before: number
total_cost_after: number
}>
>({
const { result } = await executeSql<Array<GetTableIndexAdvisorSuggestionsResponse>>({
projectRef,
connectionString,
sql,
@@ -113,7 +113,7 @@ export async function getTableIndexAdvisorSuggestions({
: 0
return {
query: row.query as SafeSqlFragment,
query: row.query,
calls: row.calls,
total_time: row.total_time,
mean_time: row.mean_time,

View File

@@ -1,4 +1,5 @@
import pgMeta from '@supabase/pg-meta'
import { joinSqlFragments, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -25,7 +26,7 @@ export async function updateTableApiAccessPrivileges({
relationId,
privileges,
}: TableApiAccessPrivilegesVariables) {
const sqlStatements: string[] = []
const sqlStatements: Array<SafeSqlFragment> = []
for (const role of API_ACCESS_ROLES) {
const rolePrivileges = privileges[role]
@@ -41,7 +42,7 @@ export async function updateTableApiAccessPrivileges({
privilegeType,
relationId,
}))
const revokeSql = pgMeta.tablePrivileges.revoke(revokeGrants).sql.trim()
const revokeSql = pgMeta.tablePrivileges.revoke(revokeGrants).sql
if (revokeSql) sqlStatements.push(revokeSql)
}
@@ -52,7 +53,7 @@ export async function updateTableApiAccessPrivileges({
privilegeType,
relationId,
}))
const grantSql = pgMeta.tablePrivileges.grant(grantGrants).sql.trim()
const grantSql = pgMeta.tablePrivileges.grant(grantGrants).sql
if (grantSql) sqlStatements.push(grantSql)
}
}
@@ -64,7 +65,7 @@ export async function updateTableApiAccessPrivileges({
const { result } = await executeSql<[]>({
projectRef,
connectionString,
sql: sqlStatements.join('\n'),
sql: joinSqlFragments(sqlStatements, '\n'),
queryKey: ['table-api-access', 'update-privileges'],
})

View File

@@ -1,4 +1,5 @@
import { buildFunctionPrivilegesSql, buildTablePrivilegesSql } from '@supabase/pg-meta'
import { joinSqlFragments, type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useMutation } from '@tanstack/react-query'
import { toast } from 'sonner'
@@ -23,7 +24,7 @@ export async function updateExposedEntities({
}: UpdateExposedEntitiesVariables): Promise<void> {
if (!projectRef) throw new Error('projectRef is required')
const sqlParts: string[] = []
const sqlParts: Array<SafeSqlFragment> = []
if (tableIdsToAdd.length > 0) {
sqlParts.push(buildTablePrivilegesSql(tableIdsToAdd, 'grant'))
@@ -46,7 +47,7 @@ export async function updateExposedEntities({
await executeSql({
projectRef,
connectionString,
sql: sqlParts.join('\n'),
sql: joinSqlFragments(sqlParts, '\n'),
queryKey: ['update-exposed-entities'],
})
}

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { PRESET_CONFIG } from '@/components/interfaces/Reports/Reports.constants'
import { ReportFilterItem } from '@/components/interfaces/Reports/Reports.types'
import { queriesFactory } from '@/components/interfaces/Reports/Reports.utils'
import { getLogsSql, queriesFactory } from '@/components/interfaces/Reports/Reports.utils'
import type { LogsEndpointParams } from '@/components/interfaces/Settings/Logs/Logs.types'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
@@ -73,32 +73,42 @@ export const useApiReport = () => {
useEffect(() => {
// update sql for each query
if (totalRequests.changeQuery) {
totalRequests.changeQuery(PRESET_CONFIG.api.queries.totalRequests.sql(formattedFilters))
totalRequests.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.totalRequests, formattedFilters)
)
}
if (topRoutes.changeQuery) {
topRoutes.changeQuery(PRESET_CONFIG.api.queries.topRoutes.sql(formattedFilters))
topRoutes.changeQuery(getLogsSql(PRESET_CONFIG.api.queries.topRoutes, formattedFilters))
}
if (errorCounts.changeQuery) {
errorCounts.changeQuery(PRESET_CONFIG.api.queries.errorCounts.sql(formattedFilters))
errorCounts.changeQuery(getLogsSql(PRESET_CONFIG.api.queries.errorCounts, formattedFilters))
}
if (topErrorRoutes.changeQuery) {
topErrorRoutes.changeQuery(PRESET_CONFIG.api.queries.topErrorRoutes.sql(formattedFilters))
topErrorRoutes.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.topErrorRoutes, formattedFilters)
)
}
if (responseSpeed.changeQuery) {
responseSpeed.changeQuery(PRESET_CONFIG.api.queries.responseSpeed.sql(formattedFilters))
responseSpeed.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.responseSpeed, formattedFilters)
)
}
if (topSlowRoutes.changeQuery) {
topSlowRoutes.changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(formattedFilters))
topSlowRoutes.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.topSlowRoutes, formattedFilters)
)
}
if (networkTraffic.changeQuery) {
networkTraffic.changeQuery(PRESET_CONFIG.api.queries.networkTraffic.sql(formattedFilters))
networkTraffic.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.networkTraffic, formattedFilters)
)
}
if (requestsByCountry.changeQuery) {
requestsByCountry.changeQuery(
PRESET_CONFIG.api.queries.requestsByCountry.sql(formattedFilters)
getLogsSql(PRESET_CONFIG.api.queries.requestsByCountry, formattedFilters)
)
}
}, [JSON.stringify(formattedFilters)])

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'
import { PRESET_CONFIG } from '@/components/interfaces/Reports/Reports.constants'
import { ReportFilterItem } from '@/components/interfaces/Reports/Reports.types'
import { queriesFactory } from '@/components/interfaces/Reports/Reports.utils'
import { getLogsSql, queriesFactory } from '@/components/interfaces/Reports/Reports.utils'
import type { LogsEndpointParams } from '@/components/interfaces/Settings/Logs/Logs.types'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
@@ -86,36 +86,46 @@ export const useStorageReport = () => {
useEffect(() => {
if (totalRequests.changeQuery) {
totalRequests.changeQuery(PRESET_CONFIG.api.queries.totalRequests.sql(formattedFilters))
totalRequests.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.totalRequests, formattedFilters)
)
}
if (topRoutes.changeQuery) {
topRoutes.changeQuery(PRESET_CONFIG.api.queries.topRoutes.sql(formattedFilters))
topRoutes.changeQuery(getLogsSql(PRESET_CONFIG.api.queries.topRoutes, formattedFilters))
}
if (errorCounts.changeQuery) {
errorCounts.changeQuery(PRESET_CONFIG.api.queries.errorCounts.sql(formattedFilters))
errorCounts.changeQuery(getLogsSql(PRESET_CONFIG.api.queries.errorCounts, formattedFilters))
}
if (topErrorRoutes.changeQuery) {
topErrorRoutes.changeQuery(PRESET_CONFIG.api.queries.topErrorRoutes.sql(formattedFilters))
topErrorRoutes.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.topErrorRoutes, formattedFilters)
)
}
if (responseSpeed.changeQuery) {
responseSpeed.changeQuery(PRESET_CONFIG.api.queries.responseSpeed.sql(formattedFilters))
responseSpeed.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.responseSpeed, formattedFilters)
)
}
if (topSlowRoutes.changeQuery) {
topSlowRoutes.changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(formattedFilters))
topSlowRoutes.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.topSlowRoutes, formattedFilters)
)
}
if (networkTraffic.changeQuery) {
networkTraffic.changeQuery(PRESET_CONFIG.api.queries.networkTraffic.sql(formattedFilters))
networkTraffic.changeQuery(
getLogsSql(PRESET_CONFIG.api.queries.networkTraffic, formattedFilters)
)
}
if (cacheHitRate.changeQuery) {
cacheHitRate.changeQuery(PRESET_CONFIG.storage.queries.cacheHitRate.sql([]))
cacheHitRate.changeQuery(getLogsSql(PRESET_CONFIG.storage.queries.cacheHitRate, []))
}
if (topCacheMisses.changeQuery) {
topCacheMisses.changeQuery(PRESET_CONFIG.storage.queries.topCacheMisses.sql([]))
topCacheMisses.changeQuery(getLogsSql(PRESET_CONFIG.storage.queries.topCacheMisses, []))
}
}, [JSON.stringify(formattedFilters)])

View File

@@ -1,10 +1,11 @@
import { type SafeSqlFragment } from '@supabase/pg-meta/src/pg-format'
import { useQuery } from '@tanstack/react-query'
import { DEFAULT_QUERY_PARAMS } from '@/components/interfaces/Reports/Reports.constants'
import {
BaseReportParams,
MetaQueryResponse,
ReportQuery,
ReportQueryDb,
} from '@/components/interfaces/Reports/Reports.types'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { executeSql } from '@/data/sql/execute-sql-query'
@@ -31,7 +32,7 @@ const useDbQuery = ({
where,
orderBy,
}: {
sql: ReportQuery['sql'] | string
sql: ReportQueryDb['safeSql'] | SafeSqlFragment
params?: BaseReportParams
where?: string
orderBy?: string