mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 19:08:44 +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 (SQL editor: execution wiring for logs-source snippets). Part of the stacked SQL-editor "Database vs Logs" query-source series. ## What is the current behavior? The SQL editor only ever runs queries against the user's Postgres database. There is no execution path for a logs (`log_sql`) snippet, and the run-button telemetry event carries no backend discriminator. ## What is the new behavior? - `useRunSource(id)` derives the run backend from the snippet type; a `log_sql` snippet resolves to `{ type: 'logs', dateRange }`, pairing the run with its session time range (default: last hour). - `useLogsSqlExecution` runs a promoted `SafeLogSqlFragment` against the analytics OTEL (ClickHouse) endpoint with the resolved time range as `iso_timestamp_start`/`iso_timestamp_end` request params. The endpoint is **pinned to OTEL** — a snippet's dialect must not flip with org migration. - The run gestures (toolbar button and Cmd+Enter) branch on the source and promote with the matching `acceptUntrusted*` right at the user action, preserving the auditable promotion-at-gesture boundary. pg intellisense is gated off for logs snippets. - The `sql_editor_query_run_button_clicked` telemetry event gains a required `{ source: 'database' | 'logs' }` property, fired from both execution paths. - Capability guard: a `log_sql` snippet is reachable by direct URL regardless of the (later) entry-point flag gating, so `executeLogsQuery` short-circuits when `otelLegacyLogs` is off — recording a clear "not available yet" result message instead of firing a request that would only return an opaque backend error on a non-ClickHouse project. This is a guard on the gesture, not endpoint selection. - Tests: `useRunSource` routing, `useLogsSqlExecution` endpoint/range/structured-error/capability-guard, and a reusable `flags` option on `renderSqlEditorHook`. No UI entry points are added — the feature runs dark until the flag-gated creation/nav PRs later in the stack. ## Additional context Stacked on the query-source series; base branch is `master` now that PR 4 (log date range domain + session state, #48401) is merged. Follow-ups in the stack add the toolbar/creation UI (with a run-affordance gate on `otelLegacyLogs`), nav section, AI dialect support, and reports guard. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for running log queries directly from the SQL editor. * Log query results, errors, and time ranges are now handled within the editor session. * Added automatic selection between database and log query execution, including support for custom date ranges. * SQL assistance is disabled while editing log queries where database definitions do not apply. * **Tests** * Added coverage for log query execution, date ranges, feature availability, and execution source selection. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
import { type SafeSqlFragment } from '@supabase/pg-meta'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import { IS_PLATFORM, useParams } from 'common'
|
|
import { useCallback, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
|
|
import type { PotentialIssues } from './SQLEditor.types'
|
|
import {
|
|
analyzeQueryIssues,
|
|
buildExecuteParams,
|
|
hasBlockingIssues,
|
|
resolveConnectionString,
|
|
shouldAutoGenerateTitle,
|
|
} from './SQLEditor.utils'
|
|
import { useSQLEditorContext } from './SQLEditorContext'
|
|
import { useDatabaseEventTriggersQuery } from '@/data/database-event-triggers/database-event-triggers-query'
|
|
import { isValidConnString } from '@/data/fetchers'
|
|
import { lintKeys } from '@/data/lint/keys'
|
|
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
|
|
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
|
|
import { useOrgAiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
|
|
import { useGetImpersonatedRoleState } from '@/state/role-impersonation-state'
|
|
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
|
|
import { getSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
|
|
|
|
type UseSqlEditorExecutionArgs = {
|
|
id: string
|
|
isDiffOpen: boolean
|
|
hasSelection: boolean
|
|
setAiTitle: (id: string, sql: string) => void
|
|
}
|
|
|
|
export function useSqlEditorExecution({
|
|
id,
|
|
isDiffOpen,
|
|
hasSelection,
|
|
setAiTitle,
|
|
}: UseSqlEditorExecutionArgs) {
|
|
const { ref } = useParams()
|
|
const { editor, clearPendingRunRefocus, refocusEditorAfterRunIfNeeded } = useSQLEditorContext()
|
|
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const queryClient = useQueryClient()
|
|
const track = useTrack()
|
|
const sessionSnap = useSqlEditorSessionSnapshot()
|
|
const limit = sessionSnap.limit
|
|
const databaseSelectorState = useDatabaseSelectorStateSnapshot()
|
|
const getImpersonatedRoleState = useGetImpersonatedRoleState()
|
|
const { aiOptInLevel } = useOrgAiOptInLevel()
|
|
|
|
const { data: databases } = useReadReplicasQuery(
|
|
{ projectRef: ref },
|
|
{ enabled: isValidConnString(project?.connectionString) }
|
|
)
|
|
const { data: eventTriggers } = useDatabaseEventTriggersQuery(
|
|
{ projectRef: project?.ref, connectionString: project?.connectionString },
|
|
{ enabled: isValidConnString(project?.connectionString) }
|
|
)
|
|
|
|
const [potentialIssues, setPotentialIssues] = useState<PotentialIssues>()
|
|
|
|
const { mutate: execute, isPending: isExecuting } = useExecuteSqlMutation({
|
|
onSuccess(data, vars) {
|
|
if (id) {
|
|
sessionSnap.addResult(id, data.result, vars.autoLimit)
|
|
}
|
|
|
|
// revalidate lint query
|
|
queryClient.invalidateQueries({ queryKey: lintKeys.lint(ref) })
|
|
refocusEditorAfterRunIfNeeded()
|
|
},
|
|
onError(error: any, vars) {
|
|
if (id) {
|
|
editor.highlightErrorLine(error, hasSelection)
|
|
sessionSnap.addResultError(id, error, vars.autoLimit)
|
|
}
|
|
|
|
refocusEditorAfterRunIfNeeded()
|
|
},
|
|
})
|
|
|
|
const executeQuery = useCallback(
|
|
async (sql: SafeSqlFragment, force: boolean = false) => {
|
|
if (isDiffOpen) {
|
|
clearPendingRunRefocus()
|
|
return
|
|
}
|
|
|
|
if (!editor.isReady() || isExecuting || project === undefined) {
|
|
clearPendingRunRefocus()
|
|
return
|
|
}
|
|
|
|
const issues = analyzeQueryIssues(sql, eventTriggers)
|
|
|
|
if (hasBlockingIssues(issues, force)) {
|
|
setPotentialIssues(issues)
|
|
return
|
|
}
|
|
|
|
// use the latest state for the title-generation check
|
|
const snippet = getSqlEditorV2StateSnapshot().snippets[id]
|
|
if (
|
|
shouldAutoGenerateTitle({
|
|
aiOptInLevel,
|
|
snippetName: snippet?.snippet.name,
|
|
isPlatform: IS_PLATFORM,
|
|
})
|
|
) {
|
|
// Intentionally don't await title gen (lazy)
|
|
setAiTitle(id, sql)
|
|
}
|
|
|
|
editor.clearHighlights()
|
|
|
|
const impersonatedRoleState = getImpersonatedRoleState()
|
|
const connectionString = resolveConnectionString(
|
|
databases,
|
|
databaseSelectorState.selectedDatabaseId
|
|
)
|
|
if (!isValidConnString(connectionString)) {
|
|
clearPendingRunRefocus()
|
|
return toast.error('Unable to run query: Connection string is missing')
|
|
}
|
|
|
|
execute({
|
|
...buildExecuteParams({
|
|
sql,
|
|
limit,
|
|
connectionString,
|
|
projectRef: project.ref,
|
|
impersonatedRoleState,
|
|
}),
|
|
handleError: (error) => {
|
|
throw error
|
|
},
|
|
})
|
|
|
|
track('sql_editor_query_run_button_clicked', { source: 'database' })
|
|
},
|
|
[
|
|
editor,
|
|
clearPendingRunRefocus,
|
|
isDiffOpen,
|
|
id,
|
|
isExecuting,
|
|
project,
|
|
aiOptInLevel,
|
|
execute,
|
|
getImpersonatedRoleState,
|
|
setAiTitle,
|
|
databaseSelectorState.selectedDatabaseId,
|
|
databases,
|
|
eventTriggers,
|
|
limit,
|
|
track,
|
|
]
|
|
)
|
|
|
|
const resetPotentialIssues = useCallback(() => setPotentialIssues(undefined), [])
|
|
|
|
return { executeQuery, isExecuting, potentialIssues, resetPotentialIssues }
|
|
}
|