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? Assistant feature and data-handling plumbing. ## Stack context This stack is based on #49352 (`chore/assistant-tool-outcomes`) and assumes #49350–#49352 merge first. Review bottom to top: 1. #49361 — assistant notebook run tool 2. #49362 — assistant notebook run UI 3. #49364 — terminal-state polish ## What is the current behavior? The Assistant can read and edit notebooks, but it cannot execute all saved query cells as one approved operation. ## What is the new behavior? - Adds a `run_notebook` tool with one approval gate for the complete notebook. - Executes database and log cells sequentially in notebook order. - Rejects stale runs when the notebook changed after the Assistant read it. - Resolves primary and read-replica connections and forwards authorization to log and replica requests. - Shares rows with the model only when the organization's AI data-sharing level permits it. - Strictly validates and sanitizes persisted notebook-run output before replaying message history. - Registers the tool in prompts, filtering, mocks, and tool construction. ## How to test manually This is the tool/data layer; use the top-of-stack preview from #49364 for the complete UI while checking these behaviors. 1. In Explorer, create and save a notebook named **Assistant run smoke test** with: - a markdown cell - a working database query - a working Logs query - a database query that returns no rows, such as `select 1 where false` 2. Open the AI Assistant and ask: **Read the “Assistant run smoke test” notebook and analyze it using its current results.** 3. Confirm the Assistant reads the notebook and requests one `run_notebook` approval for all query cells, rather than requesting one approval per cell. 4. Approve the run. Confirm database and Logs queries execute in notebook order, the markdown cell is not executed, and the Assistant responds only after the complete run finishes. 5. Start another run but do not approve it yet. In another tab, edit and save the notebook. Return to the pending approval and approve it. 6. Confirm the stale run is rejected, the Assistant reads the latest notebook version, and a new approval is required. 7. Optional privacy check: set the organization AI data-sharing level to schema-only, run a query containing a recognizable value, and confirm the value remains visible in the notebook result UI but is not repeated in the Assistant's answer. ## Automated test `mise exec node@22 -- pnpm --dir apps/studio exec vitest --run lib/ai/tool-filter.test.ts lib/ai/tools/index.test.ts lib/ai/tools/mock-tools.test.ts lib/ai/tools/notebook-tools.test.ts lib/ai/tools/tool-sanitizer.test.ts` 80 tests pass at this stack boundary. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added AI-assisted notebook execution for database and log cells, with approval, freshness checks, replica support, and per-cell error handling. - Added notebook deletion and database discovery and validation for notebook management. - Added configurable privacy controls for notebook results. - Added request header support for analytics SQL execution. - **Bug Fixes** - Improved replica lookup handling so other notebook cells can continue when one lookup fails. - Prevented invalid, unauthorized, or overly detailed notebook execution results from being exposed. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com>
180 lines
6.4 KiB
TypeScript
180 lines
6.4 KiB
TypeScript
/**
|
|
* Execution data layer for user-authored logs SQL run from the SQL editor.
|
|
*
|
|
* This is the logs-path analog of `data/sql/execute-sql-mutation.ts`. It wraps
|
|
* the analytics wire boundary (`executeAnalyticsSql`) and:
|
|
* - accepts only a `SafeLogSqlFragment` (promoted from `UntrustedLogSqlFragment`
|
|
* at an explicit user run gesture — see `safe-analytics-sql.ts`),
|
|
* - attaches the resolved time range as request params
|
|
* (`iso_timestamp_start`/`iso_timestamp_end`), never spliced into the SQL,
|
|
* - normalizes the response to `{ rows, error? }`.
|
|
*
|
|
* There are two ways a logs query can fail, and the layer keeps them distinct
|
|
* only at the pure-function level:
|
|
* - Transport failures (non-2xx) throw out of `executeAnalyticsSql` via
|
|
* `handleError`.
|
|
* - The analytics backend also returns a *200 body* carrying a structured error
|
|
* (`{ code, errors, message, status }`) — the same shape `useLogsQuery`
|
|
* reads. `mapLogsError` normalizes that into the `{ message }` shape the SQL
|
|
* editor's result pane renders, returned in `executeLogsSql`'s `error` field.
|
|
*
|
|
* `useExecuteLogsSqlMutation` then collapses both into React Query's single
|
|
* `onError` path so consumers handle one normalized error shape in one place.
|
|
*/
|
|
import { useMutation } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
|
|
import { executeAnalyticsSql, type AnalyticsSqlEndpoint } from './execute-analytics-sql'
|
|
import type { SafeLogSqlFragment } from './safe-analytics-sql'
|
|
import type { ResolvedLogDateRange } from '@/components/interfaces/Settings/Logs/logsDateRange'
|
|
import type { UseCustomMutationOptions } from '@/types'
|
|
|
|
/**
|
|
* Normalized logs query error in the shape the SQL editor result pane reads
|
|
* (`{ message }`). Produced by `mapLogsError` from either a transport error or
|
|
* the analytics backend's structured 200-body error.
|
|
*/
|
|
export interface LogsQueryError {
|
|
message: string
|
|
}
|
|
|
|
/**
|
|
* Structured error the analytics backend returns inside a 200 response body.
|
|
* Mirrors `LFResponse['error']` from the Logs types; every field is optional
|
|
* here because the value crosses the wire and must be treated defensively.
|
|
*/
|
|
interface RawLogsResponseError {
|
|
code?: number
|
|
errors?: Array<{ domain?: string; message?: string; reason?: string }>
|
|
message?: string
|
|
status?: string
|
|
}
|
|
|
|
const UNKNOWN_LOGS_ERROR_MESSAGE = 'An unexpected error occurred while running the logs query.'
|
|
|
|
/**
|
|
* Normalizes an analytics backend error into the `{ message }` shape the SQL
|
|
* editor result pane renders. Follows `useLogsQuery`'s extraction: prefer the
|
|
* top-level `message`, then the first nested `errors[].message`. Returns
|
|
* `undefined` when there is no error, and a generic fallback message when an
|
|
* error is present but carries no usable text.
|
|
*/
|
|
export function mapLogsError(error: unknown): LogsQueryError | undefined {
|
|
if (error === undefined || error === null) return undefined
|
|
|
|
if (typeof error === 'string') {
|
|
return { message: error.length > 0 ? error : UNKNOWN_LOGS_ERROR_MESSAGE }
|
|
}
|
|
|
|
if (typeof error === 'object') {
|
|
const structured = error as RawLogsResponseError
|
|
const nestedMessage = Array.isArray(structured.errors)
|
|
? structured.errors.find(
|
|
(entry) => typeof entry?.message === 'string' && entry.message.length > 0
|
|
)?.message
|
|
: undefined
|
|
const message = structured.message || nestedMessage
|
|
if (typeof message === 'string' && message.length > 0) {
|
|
return { message }
|
|
}
|
|
}
|
|
|
|
return { message: UNKNOWN_LOGS_ERROR_MESSAGE }
|
|
}
|
|
|
|
export interface ExecuteLogsSqlVariables {
|
|
projectRef: string
|
|
/** Must carry the `SafeLogSqlFragment` brand — promote at the run gesture. */
|
|
sql: SafeLogSqlFragment
|
|
/** Resolved (absolute) time range; relative ranges re-resolve at each run. */
|
|
range: ResolvedLogDateRange
|
|
/**
|
|
* Analytics endpoint to run against. The SQL editor pins this to the OTEL
|
|
* (ClickHouse) endpoint; kept as a param so callers stay explicit.
|
|
*/
|
|
endpoint: AnalyticsSqlEndpoint
|
|
signal?: AbortSignal
|
|
headers?: HeadersInit
|
|
}
|
|
|
|
export interface ExecuteLogsSqlResult {
|
|
rows: unknown[]
|
|
/** Present when the backend returned a 200 body carrying a structured error. */
|
|
error?: LogsQueryError
|
|
}
|
|
|
|
/**
|
|
* Runs a logs SQL query against the analytics backend and normalizes the
|
|
* response. Transport failures throw; a structured 200-body error is returned
|
|
* in `error` alongside empty `rows`.
|
|
*
|
|
* @throws {ResponseError} on transport failure (via `executeAnalyticsSql`).
|
|
*/
|
|
export async function executeLogsSql({
|
|
projectRef,
|
|
sql,
|
|
range,
|
|
endpoint,
|
|
signal,
|
|
headers,
|
|
}: ExecuteLogsSqlVariables): Promise<ExecuteLogsSqlResult> {
|
|
const data = await executeAnalyticsSql({
|
|
projectRef,
|
|
endpoint,
|
|
sql,
|
|
iso_timestamp_start: range.from,
|
|
iso_timestamp_end: range.to,
|
|
key: 'sql-editor',
|
|
signal,
|
|
headers,
|
|
})
|
|
|
|
const body = (data ?? {}) as { result?: unknown[]; error?: unknown }
|
|
const error = mapLogsError(body.error)
|
|
|
|
return { rows: body.result ?? [], ...(error ? { error } : {}) }
|
|
}
|
|
|
|
/**
|
|
* React Query mutation wrapping `executeLogsSql`.
|
|
*
|
|
* Collapses both failure modes into React Query's single `onError` path: a
|
|
* transport failure (thrown by `executeAnalyticsSql`) and the analytics
|
|
* backend's structured 200-body error are both normalized to `LogsQueryError`
|
|
* before they reach `onError`, so callers only ever handle one error shape in
|
|
* one place. `onSuccess` therefore always receives a genuinely successful
|
|
* result.
|
|
*/
|
|
export const useExecuteLogsSqlMutation = ({
|
|
onSuccess,
|
|
onError,
|
|
...options
|
|
}: Omit<
|
|
UseCustomMutationOptions<ExecuteLogsSqlResult, LogsQueryError, ExecuteLogsSqlVariables>,
|
|
'mutationFn'
|
|
> = {}) => {
|
|
return useMutation<ExecuteLogsSqlResult, LogsQueryError, ExecuteLogsSqlVariables>({
|
|
async mutationFn(variables) {
|
|
let result: ExecuteLogsSqlResult
|
|
try {
|
|
result = await executeLogsSql(variables)
|
|
} catch (error) {
|
|
// Transport failure — normalize to the same shape as a query error.
|
|
throw mapLogsError(error) ?? { message: UNKNOWN_LOGS_ERROR_MESSAGE }
|
|
}
|
|
// 200-body query error — route through onError alongside transport errors.
|
|
if (result.error) throw result.error
|
|
return result
|
|
},
|
|
onSuccess,
|
|
async onError(error, variables, context) {
|
|
if (onError === undefined) {
|
|
toast.error(error.message)
|
|
} else {
|
|
onError(error, variables, context)
|
|
}
|
|
},
|
|
...options,
|
|
})
|
|
}
|