Files
supabase/apps/studio/hooks/analytics/useSingleLog.tsx
Charis a7d51cdf52 feat(logs): brand legacy analytics SQL stack with SafeLogSqlFragment (#46351)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Refactor / type safety improvement

## What is the current behavior?

The legacy log query stack (`genDefaultQuery`, `genCountQuery`,
`genChartQuery`, `genWhereStatement`, `useLogsPreview`, `useSingleLog`)
builds SQL from raw strings with no type-level guarantee that values are
safely interpolated. Identifier helpers (`bqIdent`, `bqDottedIdent`,
`clickhouseIdent`, `clickhouseDottedIdent`) are duplicated across
BigQuery and ClickHouse variants, and `bqDottedIdent` wraps the entire
dotted path in one backtick pair (`` `request.pathname` ``), which
BigQuery treats as a literal column name rather than a UNNEST alias
field — causing runtime query failures on dotted filter keys.

## What is the new behavior?

- All gen functions return `SafeLogSqlFragment` and all callers route
through `executeAnalyticsSql`, enforcing compile-time SQL provenance
tracking across the legacy stack.
- `bqIdent` / `bqDottedIdent` / `clickhouseIdent` /
`clickhouseDottedIdent` are replaced by a single `quotedIdent` function
that backtick-quotes each segment individually (e.g. ``
`request`.`pathname` ``). ClickHouse natively accepts backticks, so one
function serves both engines and the dotted-path quoting bug is fixed.
- `SQL_FILTER_TEMPLATES` entries are converted to `SafeLogSqlFragment`
(static via `safeSql`, dynamic via `safeSql` + `analyticsLiteral`).
- `buildWhereClauses` is extracted as a private helper returning
`SafeLogSqlFragment[]` so the pg_cron path can merge clauses without
unsafe slice-and-cast.

## Additional context

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

* **Refactor**
* Logs query generation migrated to safer, engine-agnostic SQL
fragments, typed filter templates, and unified identifier quoting for
stronger injection protection and more consistent queries.
* Logs preview and single-log retrieval now execute analytics SQL
end-to-end using the unified executor.

* **New Features**
* Analytics SQL executor can call the backend via GET or POST and
accepts method selection.

* **Tests**
* Updated tests to validate unified identifier quoting and safe-SQL
helper behavior.

<!-- 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/46351?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-26 15:20:54 -04:00

91 lines
2.5 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { LOGS_TABLES } from '@/components/interfaces/Settings/Logs/Logs.constants'
import type {
LogData,
Logs,
LogsEndpointParams,
QueryType,
} from '@/components/interfaces/Settings/Logs/Logs.types'
import { genSingleLogQuery } from '@/components/interfaces/Settings/Logs/Logs.utils'
import { executeAnalyticsSql } from '@/data/logs/execute-analytics-sql'
import { safeSql } from '@/data/logs/safe-analytics-sql'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
interface SingleLogHook {
data: LogData | undefined
error: string | Object | null
isLoading: boolean
refresh: () => void
}
type SingleLogParams = {
id?: string
projectRef: string
queryType?: QueryType
paramsToMerge?: Partial<LogsEndpointParams>
}
function useSingleLog({
projectRef,
id,
queryType,
paramsToMerge,
}: SingleLogParams): SingleLogHook {
const table = queryType ? LOGS_TABLES[queryType] : undefined
const sql = id && table ? genSingleLogQuery(table, id) : safeSql``
const enabled = Boolean(id && table)
const { logsMetadata } = useIsFeatureEnabled(['logs:metadata'])
const {
data,
error: rcError,
isPending,
isRefetching,
refetch,
} = useQuery({
// id and queryType uniquely identify sql without having to stick the
// entire sql in the query key.
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: [
'projects',
projectRef,
'single-log',
id,
queryType,
paramsToMerge?.iso_timestamp_start,
paramsToMerge?.iso_timestamp_end,
],
queryFn: async ({ signal }) => {
const data = await executeAnalyticsSql({
projectRef,
endpoint: '/platform/projects/{ref}/analytics/endpoints/logs.all',
sql,
iso_timestamp_start: paramsToMerge?.iso_timestamp_start ?? '',
iso_timestamp_end: paramsToMerge?.iso_timestamp_end ?? '',
method: 'get',
signal,
})
return data as unknown as Logs
},
enabled,
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
})
let error: null | string | object = rcError ? (rcError as any).message : null
const result = data?.result ? data.result[0] : undefined
return {
data: !!result
? { ...result, metadata: logsMetadata ? result?.metadata : undefined }
: undefined,
isLoading: (enabled && isPending) || isRefetching,
error,
refresh: () => refetch(),
}
}
export default useSingleLog