mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## 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? Feature, plus a refactor of the shared logs-rewrite flow. PR 8 of the SQL editor query-source series. Stacked on #48457 — review that one first, and merge this after it. ## What is the current behavior? A `log_sql` snippet runs against the ClickHouse-backed analytics endpoint, but the SQL editor's AI still writes Postgres: inline edits get Postgres system prompts, and the result is run through `sql-formatter`, which mangles ClickHouse backticks and `log_attributes` map lookups. Legacy Logs Explorer saved queries open in the editor as `log_sql` snippets. Those are BigQuery dialect and error against the ClickHouse endpoint the editor runs them on, with no in-editor way out — only the Logs Explorer offered a rewrite. The completion route was also asymmetric. It assembled a schema/code/instruction message for Postgres but forwarded `prompt` verbatim for ClickHouse, so a client wanting ClickHouse had to hand-build the equivalent string. ## What is the new behavior? **Inline AI speaks ClickHouse for logs snippets.** `sqlSourceToDialect` maps a snippet's source to `postgres`/`clickhouse` and `buildCompletionRequestBody` threads it through. For ClickHouse, `useSqlEditorAi` strips code fences from the response and skips `formatSql`. Execution and dialect both follow the snippet type, so a snippet's valid dialect never flips. **Rewrite to ClickHouse in the editor.** A banner offers the rewrite for a logs snippet whose text trips `looksLikeLegacyLogsQuery`, and proposes the result through the editor's existing AI diff view rather than replacing the snippet, so it's accepted or discarded like any other AI edit. Gated on `otelLegacyLogs`: on a non-migrated org the BigQuery text is still correct, so rewriting it would break a working query. The offer is a state machine (`offered` / `rewriting` / `failed` / `noRewriteNeeded` / `dismissed`) with a declarative table of valid transitions, so the states are mutually exclusive by construction and dismissal is terminal. A failure keeps its message and offers a retry; a response identical to the input is reported rather than opening an empty diff. **One place assembles completion prompts.** The route now uses a single template for both dialects, branching only the schema section and — for `intent: 'rewrite'` — the instruction. `lib/ai/clickhouse-logs.ts` is the single home for ClickHouse-logs prompt content, replacing two independently maintained descriptions of the same table. Clients carry no prompt text. **The rewrite flow is shared with the Logs Explorer.** Both surfaces previously hand-rolled the same sequence and had drifted: only one detected a no-op rewrite, they sourced `log_attributes` keys differently, and the Explorer formatted errors with an `as Error` cast. Both now use `useLegacyLogsRewrite` and the same state-driven banner, so the Explorer picks up no-op detection and typed error extraction. **Attribute keys are fetched on submit, not while typing.** The detected source would otherwise feed a reactive query key, making every edit that changed it cost another network call. `useLogsAttributeKeys` is imperative and goes through `queryClient.fetchQuery`, so a source already cached — including by the Explorer header and query panel, which subscribe reactively — is reused. This also closes a gap where inline edits never received keys at all, unlike full rewrites. `getErrorMessage` gains an optional typed fallback and no longer stringifies a bare object into `'[object Object]'`; every existing caller already hand-rolled a fallback, except `QueueSettings`, which interpolated the raw result and now passes one. Nothing here is user-visible until the `sqlEditorLogsSource` flag is enabled. Tests: dialect selection and request-body shape, the ClickHouse prompt content (including that the schema section does not restate the dialect rules), the reducer's valid and invalid transitions, `shouldOfferLegacyLogsRewrite`, on-submit key discovery with cache reuse, and `getErrorMessage`. ## Additional context <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an Assistant banner to help rewrite legacy BigQuery-style logs queries into ClickHouse SQL. * SQL assistance now adapts to the selected query type, including relevant log attribute context. * Rewrite suggestions can be reviewed as editor diffs before being applied. * **Bug Fixes** * Improved rewrite failure handling, retry options, dismissal behavior, and “no rewrite needed” messaging. * Error notifications now provide a clearer fallback message when details are unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import { BASE_PATH } from '@/lib/constants'
|
|
|
|
export function stripSqlCodeFences(text: string): string {
|
|
const trimmed = text.trim()
|
|
const fenced = trimmed.match(/```(?:sql)?\s*\n?([\s\S]*?)\n?```/i)
|
|
return (fenced ? fenced[1] : trimmed).trim()
|
|
}
|
|
|
|
const SOURCE_ALIASES: Record<string, string> = {
|
|
pg_cron_logs: 'postgres_logs',
|
|
}
|
|
|
|
export function detectLogSource(sql: string): string | undefined {
|
|
// `\b` so only a standalone `source` column counts — an unanchored match reads
|
|
// the value out of `resource = '...'` or `datasource = '...'` too.
|
|
const bySource = sql.match(/\bsource\s*=\s*'([^']+)'/i)
|
|
if (bySource) {
|
|
const source = bySource[1].toLowerCase()
|
|
return SOURCE_ALIASES[source] ?? source
|
|
}
|
|
const byFrom = sql.match(/\bfrom\s+([a-z_][a-z0-9_]*)/i)
|
|
if (byFrom) {
|
|
const table = byFrom[1].toLowerCase()
|
|
if (table === 'logs') return undefined
|
|
return SOURCE_ALIASES[table] ?? table
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
export function looksLikeLegacyLogsQuery(sql: string): boolean {
|
|
const lower = sql.toLowerCase()
|
|
if (/\bunnest\s*\(/.test(lower)) return true
|
|
if (/cast\s*\(\s*timestamp\s+as\s+datetime\s*\)/.test(lower)) return true
|
|
const byFrom = lower.match(/\bfrom\s+([a-z_][a-z0-9_]*)/)
|
|
return byFrom ? byFrom[1] !== 'logs' : false
|
|
}
|
|
|
|
/**
|
|
* How long to let the query text settle before re-running the dialect check.
|
|
* Shared so every surface offering the rewrite reacts on the same cadence.
|
|
*/
|
|
export const LEGACY_LOGS_DIALECT_CHECK_DEBOUNCE_MS = 500
|
|
|
|
/**
|
|
* Whether to offer the ClickHouse rewrite for a query. Both the flag and the
|
|
* dialect check matter: on an org whose logs haven't moved to ClickHouse the
|
|
* BigQuery text is still *correct*, so offering to rewrite it would break a
|
|
* working query. Callers layer their own dismissal state on top.
|
|
*/
|
|
export function shouldOfferLegacyLogsRewrite({
|
|
sql,
|
|
isClickhouseLogsEnabled,
|
|
}: {
|
|
sql: string
|
|
isClickhouseLogsEnabled: boolean
|
|
}): boolean {
|
|
return isClickhouseLogsEnabled && looksLikeLegacyLogsQuery(sql)
|
|
}
|
|
|
|
export interface RewriteLogsSqlArgs {
|
|
sql: string
|
|
projectRef: string
|
|
connectionString?: string | null
|
|
orgSlug?: string
|
|
authorizationHeader?: string | null
|
|
availableKeys?: string[]
|
|
}
|
|
|
|
/**
|
|
* Asks the completion route to rewrite a whole BigQuery logs query as ClickHouse
|
|
* SQL. The prompt itself lives server-side (`lib/ai/clickhouse-logs.ts`) — this
|
|
* only declares the intent and hands the query over as the selection, the same
|
|
* shape an inline edit uses, so exactly one place knows how a completion prompt
|
|
* is assembled.
|
|
*/
|
|
export async function rewriteLogsSqlWithAI(args: RewriteLogsSqlArgs) {
|
|
const { sql, projectRef, connectionString, orgSlug, authorizationHeader, availableKeys } = args
|
|
|
|
const response = await fetch(`${BASE_PATH}/api/ai/code/complete`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(authorizationHeader ? { Authorization: authorizationHeader } : {}),
|
|
},
|
|
body: JSON.stringify({
|
|
projectRef,
|
|
connectionString,
|
|
language: 'sql',
|
|
dialect: 'clickhouse',
|
|
intent: 'rewrite',
|
|
orgSlug,
|
|
completionMetadata: {
|
|
// The whole query is the selection, so the rewrite replaces all of it and
|
|
// the route supplies the instruction for the `rewrite` intent.
|
|
textBeforeCursor: '',
|
|
textAfterCursor: '',
|
|
prompt: '',
|
|
selection: sql,
|
|
availableKeys,
|
|
},
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text()
|
|
throw new Error(errorText || 'Failed to rewrite the query')
|
|
}
|
|
|
|
const raw = await response.json()
|
|
const rewritten = stripSqlCodeFences(typeof raw === 'string' ? raw : String(raw))
|
|
if (!rewritten) throw new Error('The assistant returned an empty query')
|
|
return rewritten
|
|
}
|