feat(studio): logs SQL execution wiring + source-aware run gestures (#48414)

## 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 -->
This commit is contained in:
Charis authored and GitHub committed 2026-07-29 10:43:48 -04:00
1 parent 67c983caef
commit 4c8ed105d2
15 files changed
+425 -36

No files matched your search

@@ -27,7 +27,7 @@ export type EditorController = {
isReady: () => boolean
getValue: () => string | undefined
getSelectionStartLine: () => number | undefined
getSql: (snippetContent?: UntrustedSqlFragment) => UntrustedSqlFragment | undefined
getSql: (snippetContent?: string) => UntrustedSqlFragment | undefined
replaceAll: (text: string, source: string) => void
focus: () => void
revealLineInCenter: (line: number) => void
@@ -457,7 +457,7 @@ export function applyAutoLimit(
*/
export function getEditorSql(
editor: IStandaloneCodeEditor,
snippetContent?: UntrustedSqlFragment
snippetContent?: string
): UntrustedSqlFragment {
const selection = editor.getSelection()
const selectedValue = selection ? editor.getModel()?.getValueInRange(selection) : undefined
@@ -1,5 +1,4 @@
import type { Monaco } from '@monaco-editor/react'
import type { UntrustedSqlFragment } from '@supabase/pg-meta'
import {
createContext,
use,
@@ -103,7 +102,7 @@ export const SQLEditorProvider = ({
[]
)
const getSqlFromEditor = useCallback((snippetContent?: UntrustedSqlFragment) => {
const getSqlFromEditor = useCallback((snippetContent?: string) => {
const editorInstance = editorRef.current
if (!editorInstance) return undefined
return getEditorSql(editorInstance, snippetContent)
@@ -15,17 +15,25 @@ import {
} from 'react'
import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks'
import { type QuerySource } from './querySource'
import type { UtilityTab } from './SQLEditor.types'
import { useSQLEditorContext } from './SQLEditorContext'
import { useAddDefinitions } from './useAddDefinitions'
import { useEditorMount } from './useEditorMount'
import { useLogsSqlExecution } from './useLogsSqlExecution'
import { usePrettifyQuery } from './usePrettifyQuery'
import { useRunSource } from './useRunSource'
import { useSnippetIdentity } from './useSnippetIdentity'
import { useSnippetTitleGenerator } from './useSnippetTitleGenerator'
import { useSqlEditorAi } from './useSqlEditorAi'
import { useSqlEditorExecution } from './useSqlEditorExecution'
import { useSqlEditorShortcuts } from './useSqlEditorShortcuts'
import { isValidConnString } from '@/data/fetchers'
import {
untrustedLogSql,
type SafeLogSqlFragment,
type UntrustedLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
@@ -73,6 +81,9 @@ type RunContextValue = {
potentialIssues: SqlEditorExecution['potentialIssues']
resetPotentialIssues: () => void
prettifyQuery: () => void
runSource: QuerySource
executeLogsQuery: (sql: SafeLogSqlFragment) => void
readEditorLogsSql: () => UntrustedLogSqlFragment | undefined
}
/** Editor-surface UI state: selection, the active results tab, and mount. */
@@ -131,7 +142,9 @@ export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) =>
const { id, urlId, generatedNewSnippetName, isLoading } = useSnippetIdentity()
const { onMount, editorMountCount } = useEditorMount({ id })
useAddDefinitions(id, monacoRef.current)
const runSource = useRunSource(id)
useAddDefinitions(id, monacoRef.current, { enabled: runSource.type !== 'logs' })
const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery(
{
@@ -152,13 +165,31 @@ export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) =>
return editor.getSql(fallback)
}, [editor, id])
const { executeQuery, isExecuting, potentialIssues, resetPotentialIssues } =
useSqlEditorExecution({
id,
isDiffOpen,
hasSelection,
setAiTitle,
})
// Reads the SQL to run from the editor as an UntrustedLogSqlFragment — the logs
// sibling of readEditorSql. Promotion (acceptUntrustedLogsSql) happens at each
// user-action site, never here.
const readEditorLogsSql = useCallback((): UntrustedLogSqlFragment | undefined => {
const snippet = getSqlEditorV2StateSnapshot().snippets[id]?.snippet
const fallback = snippet?.type === 'log_sql' ? snippet.content?.unchecked_sql : undefined
const sql = editor.getSql(fallback)
return sql === undefined ? undefined : untrustedLogSql(sql)
}, [editor, id])
const {
executeQuery,
isExecuting: isExecutingDb,
potentialIssues,
resetPotentialIssues,
} = useSqlEditorExecution({
id,
isDiffOpen,
hasSelection,
setAiTitle,
})
const { executeLogsQuery, isExecuting: isExecutingLogs } = useLogsSqlExecution({ id })
const isExecuting = isExecutingDb || isExecutingLogs
const ai = useSqlEditorAi({ id, editorMountCount, diff, prompt })
const { acceptAiHandler, discardAiHandler } = ai
@@ -215,8 +246,21 @@ export const SQLEditorControllersProvider = ({ children }: PropsWithChildren) =>
potentialIssues,
resetPotentialIssues,
prettifyQuery,
runSource,
executeLogsQuery,
readEditorLogsSql,
}),
[executeQuery, readEditorSql, isExecuting, potentialIssues, resetPotentialIssues, prettifyQuery]
[
executeQuery,
readEditorSql,
isExecuting,
potentialIssues,
resetPotentialIssues,
prettifyQuery,
runSource,
executeLogsQuery,
readEditorLogsSql,
]
)
const uiValue = useMemo<UiContextValue>(
@@ -12,6 +12,7 @@ import {
useSqlEditorUi,
} from './SQLEditorControllers'
import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget'
import { acceptUntrustedLogsSql } from '@/data/logs/safe-analytics-sql'
import { detectOS } from '@/lib/helpers'
// Load the monaco editor client-side only (does not behave well server-side)
@@ -124,16 +125,28 @@ const SQLEditorMainView = () => {
const { diff, prompt } = useSqlEditorAssistant()
const { isDiffOpen } = diff
const { promptState, openPrompt } = prompt
const { executeQuery, readEditorSql, prettifyQuery } = useSqlEditorRun()
const {
executeQuery,
readEditorSql,
prettifyQuery,
runSource,
executeLogsQuery,
readEditorLogsSql,
} = useSqlEditorRun()
const { onMount, setHasSelection } = useSqlEditorUi()
const os = detectOS()
// Run gesture from the editor — promote here, at the user action.
const runQuery = useCallback(() => {
const sql = readEditorSql()
if (sql !== undefined) void executeQuery(acceptUntrustedSql(sql))
}, [executeQuery, readEditorSql])
if (runSource.type === 'logs') {
const sql = readEditorLogsSql()
if (sql !== undefined) void executeLogsQuery(acceptUntrustedLogsSql(sql))
} else {
const sql = readEditorSql()
if (sql !== undefined) void executeQuery(acceptUntrustedSql(sql))
}
}, [executeLogsQuery, executeQuery, readEditorLogsSql, readEditorSql, runSource.type])
return (
<div key={id} className="w-full h-full relative">
@@ -17,6 +17,7 @@ import {
import { SQLEditorEditorPanel } from './SQLEditorEditorPanel'
import { UtilityActions } from './UtilityPanel/UtilityActions'
import { UtilityPanel } from './UtilityPanel/UtilityPanel'
import { acceptUntrustedLogsSql } from '@/data/logs/safe-analytics-sql'
const SQLEditorRunWarningModal = () => {
const { refocusEditor, clearPendingRunRefocus, markRefocusAfterRun } = useSQLEditorContext()
@@ -58,16 +59,38 @@ const SQLEditorToolbar = () => {
const { clearPendingRunRefocus, markRefocusAfterRun } = useSQLEditorContext()
const { id } = useSqlEditorSnippet()
const { diff } = useSqlEditorAssistant()
const { executeQuery, readEditorSql, isExecuting, prettifyQuery } = useSqlEditorRun()
const {
executeQuery,
readEditorSql,
isExecuting,
prettifyQuery,
runSource,
executeLogsQuery,
readEditorLogsSql,
} = useSqlEditorRun()
const { hasSelection } = useSqlEditorUi()
// Run gesture from the toolbar button — promote here, at the user action.
const runQuery = useCallback(() => {
markRefocusAfterRun()
const sql = readEditorSql()
if (sql === undefined) return clearPendingRunRefocus()
void executeQuery(acceptUntrustedSql(sql))
}, [clearPendingRunRefocus, executeQuery, markRefocusAfterRun, readEditorSql])
if (runSource.type === 'logs') {
const sql = readEditorLogsSql()
if (sql === undefined) return clearPendingRunRefocus()
void executeLogsQuery(acceptUntrustedLogsSql(sql))
} else {
const sql = readEditorSql()
if (sql === undefined) return clearPendingRunRefocus()
void executeQuery(acceptUntrustedSql(sql))
}
}, [
clearPendingRunRefocus,
executeLogsQuery,
executeQuery,
markRefocusAfterRun,
readEditorLogsSql,
readEditorSql,
runSource.type,
])
return (
<UtilityActions
@@ -68,6 +68,14 @@ export const DEFAULT_LOG_DATE_RANGE: LogDateRange = {
last: { amount: 1, unit: 'hour' },
}
/**
* The runtime query source for a snippet, pairing the database/logs discriminant
* with the extra state each backend needs to run. A logs run carries the active
* time range (session state, re-resolved at every run); a database run needs
* nothing beyond the connection the execution pipeline already resolves.
*/
export type QuerySource = { type: 'database' } | { type: 'logs'; dateRange: LogDateRange }
/**
* Parse a date-picker helper's label (e.g. "Last hour", "Last 3 hours", "Last 30
* minutes") into a relative amount/unit. Covers both the static presets in
@@ -15,7 +15,11 @@ import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { formatSql } from '@/lib/formatSql'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
export const useAddDefinitions = (id: string, monaco: Monaco | null) => {
export const useAddDefinitions = (
id: string,
monaco: Monaco | null,
{ enabled = true }: { enabled?: boolean } = {}
) => {
const { data: project } = useSelectedProjectQuery()
const snapV2 = useSqlEditorV2StateSnapshot()
@@ -29,28 +33,28 @@ export const useAddDefinitions = (id: string, monaco: Monaco | null) => {
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{ enabled: intellisenseEnabled }
{ enabled: enabled && intellisenseEnabled }
)
const { data: functions, isSuccess: isFunctionsSuccess } = useDatabaseFunctionsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{ enabled: intellisenseEnabled }
{ enabled: enabled && intellisenseEnabled }
)
const { data: schemas, isSuccess: isSchemasSuccess } = useSchemasQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{ enabled: intellisenseEnabled }
{ enabled: enabled && intellisenseEnabled }
)
const { data: tableColumns, isSuccess: isTableColumnsSuccess } = useTableColumnsQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{ enabled: intellisenseEnabled }
{ enabled: enabled && intellisenseEnabled }
)
const pgInfoRef = useRef<any>(null)
@@ -58,6 +62,7 @@ export const useAddDefinitions = (id: string, monaco: Monaco | null) => {
const filteredSchemas = useSchemasFilteredForHighAvailability(schemas)
const isPgInfoReady =
enabled &&
intellisenseEnabled &&
isTableColumnsSuccess &&
isSchemasSuccess &&
@@ -0,0 +1,136 @@
import { act, waitFor } from '@testing-library/react'
import { HttpResponse } from 'msw'
import { beforeEach, describe, expect, it } from 'vitest'
import { useLogsSqlExecution } from './useLogsSqlExecution'
import {
acceptUntrustedLogsSql,
untrustedLogSql,
type SafeLogSqlFragment,
} from '@/data/logs/safe-analytics-sql'
import { sqlEditorSessionState } from '@/state/sql-editor/sql-editor-session-state'
import { addAPIMock } from '@/tests/lib/msw'
import {
renderSqlEditorHook,
resetSqlEditorStores,
seedSnippet,
setupSqlEditorMocks,
} from '@/tests/lib/sql-editor-test-utils'
const SNIPPET_ID = 'logs-execution-snippet'
/** Promote raw text to the `SafeLogSqlFragment` the run pipeline expects, exactly
* as the toolbar/editor-panel promote it right at the user action. */
const logsSql = (text: string): SafeLogSqlFragment => acceptUntrustedLogsSql(untrustedLogSql(text))
type CapturedBody = { sql: string; iso_timestamp_start: string; iso_timestamp_end: string }
function mockLogsAllOtel(rows: unknown[] = []) {
const captured: CapturedBody[] = []
addAPIMock({
method: 'post',
path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel',
response: async ({ request }) => {
const body = (await request.json()) as CapturedBody
captured.push(body)
return HttpResponse.json<any>({ result: rows })
},
})
return captured
}
/** Renders the hook with ClickHouse logs enabled by default; pass
* `{ otelLegacyLogs: false }` to exercise the not-available-yet guard. */
function renderLogsExecution(flags: Record<string, boolean> = { otelLegacyLogs: true }) {
return renderSqlEditorHook(() => useLogsSqlExecution({ id: SNIPPET_ID }), { flags })
}
beforeEach(() => {
resetSqlEditorStores()
setupSqlEditorMocks()
seedSnippet({ id: SNIPPET_ID, source: 'logs' })
})
describe('useLogsSqlExecution', () => {
it('runs a logs query and writes the result to the session store', async () => {
const rows = [{ event_message: 'hello' }]
const captured = mockLogsAllOtel(rows)
const { result } = renderLogsExecution()
act(() => {
result.current.executeLogsQuery(logsSql('select event_message from edge_logs'))
})
await waitFor(() => expect(sqlEditorSessionState.results[SNIPPET_ID]).toBeDefined())
expect(sqlEditorSessionState.results[SNIPPET_ID][0].rows).toEqual(rows)
expect(captured).toHaveLength(1)
expect(captured[0].sql).toContain('select event_message from edge_logs')
expect(captured[0].iso_timestamp_start.length).toBeGreaterThan(0)
expect(captured[0].iso_timestamp_end.length).toBeGreaterThan(0)
})
it('records a structured 200-body error on the result instead of throwing', async () => {
addAPIMock({
method: 'post',
path: '/platform/projects/:ref/analytics/endpoints/logs.all.otel',
response: async () =>
HttpResponse.json<any>({
error: {
code: 400,
errors: [{ domain: 'global', message: 'Missing column', reason: 'invalid' }],
message: 'Missing column',
status: 'INVALID_ARGUMENT',
},
}),
})
const { result } = renderLogsExecution()
act(() => {
result.current.executeLogsQuery(logsSql('select does_not_exist from edge_logs'))
})
await waitFor(() => expect(sqlEditorSessionState.results[SNIPPET_ID]?.[0]?.error).toBeDefined())
expect(sqlEditorSessionState.results[SNIPPET_ID][0].error.message).toBe('Missing column')
})
it('resolves a relative session range to a from/to window around now', async () => {
const captured = mockLogsAllOtel([])
sqlEditorSessionState.setLogRange(SNIPPET_ID, {
kind: 'relative',
last: { amount: 2, unit: 'hour' },
})
const { result } = renderLogsExecution()
act(() => {
result.current.executeLogsQuery(logsSql('select 1'))
})
await waitFor(() => expect(captured).toHaveLength(1))
const from = Date.parse(captured[0].iso_timestamp_start)
const to = Date.parse(captured[0].iso_timestamp_end)
expect(Number.isNaN(from)).toBe(false)
expect(Number.isNaN(to)).toBe(false)
expect(from).toBeLessThan(to)
})
it('records an unavailable message and fires no request when ClickHouse logs are off', async () => {
const captured = mockLogsAllOtel([])
const { result } = renderLogsExecution({ otelLegacyLogs: false })
act(() => {
result.current.executeLogsQuery(logsSql('select 1'))
})
await waitFor(() => expect(sqlEditorSessionState.results[SNIPPET_ID]?.[0]?.error).toBeDefined())
expect(sqlEditorSessionState.results[SNIPPET_ID][0].error.message).toBe(
"Querying logs from the SQL editor isn't available for this project yet."
)
// The doomed request never left the client.
expect(captured).toHaveLength(0)
})
})
@@ -0,0 +1,61 @@
import { useFlag, useParams } from 'common'
import { useCallback } from 'react'
import { DEFAULT_LOG_DATE_RANGE, resolveLogRunRange } from './querySource'
import { useExecuteLogsSqlMutation } from '@/data/logs/execute-logs-sql-mutation'
import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint'
import { type SafeLogSqlFragment } from '@/data/logs/safe-analytics-sql'
import { useTrack } from '@/lib/telemetry/track'
import {
getSqlEditorSessionSnapshot,
useSqlEditorSessionSnapshot,
} from '@/state/sql-editor/sql-editor-session-state'
type UseLogsSqlExecutionArgs = { id: string }
/**
* Logs counterpart to `useSqlEditorExecution`. Runs a promoted
* `SafeLogSqlFragment` against the analytics OTEL endpoint with the snippet's
* active time range attached as request params.
*/
export function useLogsSqlExecution({ id }: UseLogsSqlExecutionArgs) {
const { ref: projectRef } = useParams()
const isOtelLogsEnabled = useFlag('otelLegacyLogs')
const track = useTrack()
const sessionSnap = useSqlEditorSessionSnapshot()
const { mutate, isPending: isExecuting } = useExecuteLogsSqlMutation({
onSuccess: (data) => {
sessionSnap.addResult(id, data.rows)
},
onError: (error) => {
sessionSnap.addResultError(id, error)
},
})
const executeLogsQuery = useCallback(
(sql: SafeLogSqlFragment) => {
if (isExecuting || projectRef === undefined) return
if (!isOtelLogsEnabled) {
getSqlEditorSessionSnapshot().addResultError(id, {
message: "Querying logs from the SQL editor isn't available for this project yet.",
})
return
}
// Re-read imperatively so a range picked immediately before the run is
// honored; relative ranges re-resolve against `now` here.
const range = resolveLogRunRange(
getSqlEditorSessionSnapshot().logRange[id] ?? DEFAULT_LOG_DATE_RANGE
)
mutate({ projectRef, sql, range, endpoint: logsAllEndpointUrl(true) })
track('sql_editor_query_run_button_clicked', { source: 'logs' })
},
[id, isExecuting, isOtelLogsEnabled, mutate, projectRef, track]
)
return { executeLogsQuery, isExecuting }
}
@@ -0,0 +1,52 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { DEFAULT_LOG_DATE_RANGE } from './querySource'
import { useRunSource } from './useRunSource'
import { sqlEditorSessionState } from '@/state/sql-editor/sql-editor-session-state'
import {
renderSqlEditorHook,
resetSqlEditorStores,
seedSnippet,
setupSqlEditorMocks,
} from '@/tests/lib/sql-editor-test-utils'
beforeEach(() => {
resetSqlEditorStores()
setupSqlEditorMocks()
})
describe('useRunSource', () => {
it('resolves a database snippet to a database source', () => {
const id = 'database-snippet'
seedSnippet({ id, source: 'database' })
const { result } = renderSqlEditorHook(() => useRunSource(id))
expect(result.current).toEqual({ type: 'database' })
})
it('resolves a logs snippet with no session range to the default range', () => {
const id = 'logs-snippet-default-range'
seedSnippet({ id, source: 'logs' })
const { result } = renderSqlEditorHook(() => useRunSource(id))
expect(result.current).toEqual({ type: 'logs', dateRange: DEFAULT_LOG_DATE_RANGE })
})
it('resolves a logs snippet to its session-stored range when one is set', () => {
const id = 'logs-snippet-custom-range'
seedSnippet({ id, source: 'logs' })
sqlEditorSessionState.setLogRange(id, {
kind: 'relative',
last: { amount: 2, unit: 'hour' },
})
const { result } = renderSqlEditorHook(() => useRunSource(id))
expect(result.current).toEqual({
type: 'logs',
dateRange: { kind: 'relative', last: { amount: 2, unit: 'hour' } },
})
})
})
@@ -0,0 +1,27 @@
import { useMemo } from 'react'
import { DEFAULT_LOG_DATE_RANGE, type QuerySource } from './querySource'
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
/**
* Resolves where a snippet's query runs. A `log_sql` snippet targets the logs
* backend and carries its session time range (falling back to the default when
* the user hasn't picked one); every other snippet targets the database. Source
* is derived from the snippet's content type and is NOT flag-gated here — a
* URL-opened logs snippet routes correctly even with the feature flag off.
*/
export function useRunSource(id: string): QuerySource {
const snapV2 = useSqlEditorV2StateSnapshot()
const sessionSnap = useSqlEditorSessionSnapshot()
const snippetType = snapV2.snippets[id]?.snippet.type
const logRange = sessionSnap.logRange[id]
return useMemo<QuerySource>(() => {
if (snippetType === 'log_sql') {
return { type: 'logs', dateRange: logRange ?? DEFAULT_LOG_DATE_RANGE }
}
return { type: 'database' }
}, [snippetType, logRange])
}
@@ -139,7 +139,7 @@ export function useSqlEditorExecution({
},
})
track('sql_editor_query_run_button_clicked')
track('sql_editor_query_run_button_clicked', { source: 'database' })
},
[
editor,
@@ -1,6 +1,7 @@
import { untrustedSql } from '@supabase/pg-meta'
import type { QueryClient } from '@tanstack/react-query'
import { renderHook, type RenderHookOptions } from '@testing-library/react'
import { FeatureFlagContext } from 'common'
import { http, HttpResponse } from 'msw'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
import type { ReactNode } from 'react'
@@ -323,6 +324,8 @@ type RenderSqlEditorHookOptions<TProps> = {
aiAssistantState?: ReturnType<typeof createAiAssistantState>
databaseSelectorState?: ReturnType<typeof createDatabaseSelectorState>
roleImpersonationState?: ReturnType<typeof createRoleImpersonationState>
/** ConfigCat flags to expose via FeatureFlagContext (e.g. `{ otelLegacyLogs: true }`). */
flags?: Record<string, boolean>
}
export function renderSqlEditorHook<TResult, TProps = undefined>(
@@ -336,12 +339,10 @@ export function renderSqlEditorHook<TResult, TProps = undefined>(
options?.roleImpersonationState ??
createRoleImpersonationState('default', { current: async () => ({}) })
const wrapper = ({ children }: { children: ReactNode }) => (
<CustomWrapper
queryClient={options?.queryClient}
nuqs={options?.nuqs}
profileContext={options?.profileContext}
>
const flags = options?.flags
const wrapper = ({ children }: { children: ReactNode }) => {
const tree = (
<RoleImpersonationStateContext.Provider value={roleImpersonationState}>
<DatabaseSelectorStateContext.Provider value={databaseSelectorState}>
<AiAssistantStateContext.Provider value={aiAssistantState}>
@@ -351,8 +352,24 @@ export function renderSqlEditorHook<TResult, TProps = undefined>(
</AiAssistantStateContext.Provider>
</DatabaseSelectorStateContext.Provider>
</RoleImpersonationStateContext.Provider>
</CustomWrapper>
)
)
return (
<CustomWrapper
queryClient={options?.queryClient}
nuqs={options?.nuqs}
profileContext={options?.profileContext}
>
{flags ? (
<FeatureFlagContext.Provider value={{ configcat: flags, posthog: {}, hasLoaded: true }}>
{tree}
</FeatureFlagContext.Provider>
) : (
tree
)}
</CustomWrapper>
)
}
const result = renderHook(hook, {
initialProps: options?.initialProps,
+4
View File
@@ -1311,6 +1311,10 @@ export interface ImportDataAddedEvent {
*/
export interface SqlEditorQueryRunButtonClickedEvent {
action: 'sql_editor_query_run_button_clicked'
properties: {
/** Which backend the query ran against. */
source: 'database' | 'logs'
}
groups: TelemetryGroups
}