Files
supabase/apps/studio/data/logs/execute-analytics-sql.ts
T
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

70 lines
2.2 KiB
TypeScript

/**
* Wire-boundary for analytics SQL execution.
*
* This is the analytics-path analog of pg-meta's `executeSql`. It accepts only
* `SafeLogSqlFragment` — plain strings are rejected at compile time — so any
* value flowing from URL parameters, UI inputs, or LLM output must pass through
* a sanitization helper in safe-analytics-sql.ts before reaching the wire.
*
* See .claude/skills/safe-sql-execution/SKILL.md for the full security model.
*/
import type { SafeLogSqlFragment } from './safe-analytics-sql'
import { get, handleError, post } from '@/data/fetchers'
/**
* Analytics endpoints that accept `{ sql, iso_timestamp_start, iso_timestamp_end }`
* either as a POST body or GET query string. Extend this union as additional
* endpoints are migrated to the safe-analytics-sql pattern.
*/
export type AnalyticsSqlEndpoint =
| '/platform/projects/{ref}/analytics/endpoints/logs.all'
| '/platform/projects/{ref}/analytics/endpoints/logs.all.otel'
export interface ExecuteAnalyticsSqlVariables {
projectRef: string
endpoint: AnalyticsSqlEndpoint
/** Must carry the `SafeLogSqlFragment` brand — plain strings are rejected at compile time. */
sql: SafeLogSqlFragment
iso_timestamp_start: string
iso_timestamp_end: string
/** Defaults to 'post'. Use 'get' to preserve wire behavior when migrating legacy GET callers. */
method?: 'get' | 'post'
signal?: AbortSignal
headers?: HeadersInit
}
export async function executeAnalyticsSql({
projectRef,
endpoint,
sql,
iso_timestamp_start,
iso_timestamp_end,
method = 'post',
signal,
headers: headersInit,
}: ExecuteAnalyticsSqlVariables) {
const headers = headersInit !== undefined ? new Headers(headersInit) : undefined
if (method === 'get') {
const { data, error } = await get(endpoint, {
params: {
path: { ref: projectRef },
query: { sql, iso_timestamp_start, iso_timestamp_end },
},
signal,
headers,
})
if (error) handleError(error)
return data
}
const { data, error } = await post(endpoint, {
params: { path: { ref: projectRef } },
body: { sql, iso_timestamp_start, iso_timestamp_end },
signal,
headers,
})
if (error) handleError(error)
return data
}