mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
refactor(sql-editor): extract deriveSnippetIdentity, debug/completion/diff-key helpers (#48014)
## Summary Pure-fn extraction pass across the SQL editor hooks. - Extracts `deriveSnippetIdentity` out of `useSnippetIdentity`'s inline id + `isLoading` derivation into `SQLEditor.utils.ts`. - Extracts `extractDebugContext` (shared snippet/result/error extraction) and `buildDebugChatArgs` (the `aiSnap.newChat(...)` payload builder) out of `useSqlEditorAi`'s `buildDebugPrompt`/`onDebug` into `SQLEditor.utils.ts`. - Extracts `buildCompletionRequestBody` (the AI completion endpoint's request body builder) and `planDiffRequestApplication` (the pending-diff-request application decision: replace vs. open a diff, depending on whether the editor is currently empty) out of `useSqlEditorAi` into `SQLEditor.utils.ts`. The `drainDiffRequest` effect now just applies the plan instead of branching inline. - Extracts `resolveDiffKeyAction` out of `useSqlEditorShortcuts`'s window-keydown Enter/Escape branch into `SQLEditor.utils.ts`. ## Test plan - [x] `pnpm --filter studio typecheck` - [x] `pnpm test:studio -- SQLEditor` (265 tests passing) - [x] `pnpm --filter studio run lint:ratchet`
This commit is contained in:
1 parent
cd5935d3e2
commit
cdc843dadd
5 files changed
+446
-45
No files matched your search
@@ -2,25 +2,31 @@ import { safeSql, untrustedSql } from '@supabase/pg-meta'
|
||||
import { stripIndent } from 'common-tags'
|
||||
import { describe, expect, it, test } from 'vitest'
|
||||
|
||||
import { untitledSnippetTitle } from './SQLEditor.constants'
|
||||
import type { IStandaloneCodeEditor } from './SQLEditor.types'
|
||||
import { sqlAiDisclaimerComment, untitledSnippetTitle } from './SQLEditor.constants'
|
||||
import { DiffType, type IStandaloneCodeEditor } from './SQLEditor.types'
|
||||
import {
|
||||
analyzeQueryIssues,
|
||||
appendEnableRLSStatements,
|
||||
applyAutoLimit,
|
||||
assembleCompletionDiff,
|
||||
buildCompletionRequestBody,
|
||||
buildDebugChatArgs,
|
||||
buildDebugPromptText,
|
||||
buildExecuteParams,
|
||||
checkAlterDatabaseConnection,
|
||||
checkDestructiveQuery,
|
||||
computeErrorHighlightLine,
|
||||
deriveSnippetIdentity,
|
||||
extractDebugContext,
|
||||
filterTablesCoveredByEnsureRLSTrigger,
|
||||
getCreateTablesMissingRLS,
|
||||
getEditorSql,
|
||||
hasActiveEnsureRLSTrigger,
|
||||
hasBlockingIssues,
|
||||
isUpdateWithoutWhere,
|
||||
planDiffRequestApplication,
|
||||
resolveConnectionString,
|
||||
resolveDiffKeyAction,
|
||||
shouldAutoGenerateTitle,
|
||||
trimTrailingSemicolons,
|
||||
} from './SQLEditor.utils'
|
||||
@@ -290,6 +296,257 @@ describe('SQLEditor.utils.ts:buildExecuteParams', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SQLEditor.utils.ts:deriveSnippetIdentity', () => {
|
||||
test('uses the generated id when there is no url id, loading reflects the snippets map like any other id', () => {
|
||||
const result = deriveSnippetIdentity({
|
||||
urlId: undefined,
|
||||
generatedId: 'generated-id',
|
||||
snippets: {},
|
||||
})
|
||||
expect(result).toEqual({ id: 'generated-id', isLoading: true })
|
||||
})
|
||||
test('uses the generated id and is not loading once it appears in the snippets map', () => {
|
||||
const result = deriveSnippetIdentity({
|
||||
urlId: undefined,
|
||||
generatedId: 'generated-id',
|
||||
snippets: { 'generated-id': { snippet: { content: { some: 'content' } } } },
|
||||
})
|
||||
expect(result).toEqual({ id: 'generated-id', isLoading: false })
|
||||
})
|
||||
test('uses the generated id and is never loading when the url id is "new"', () => {
|
||||
const result = deriveSnippetIdentity({
|
||||
urlId: 'new',
|
||||
generatedId: 'generated-id',
|
||||
snippets: { 'generated-id': { snippet: { content: undefined } } },
|
||||
})
|
||||
expect(result).toEqual({ id: 'generated-id', isLoading: false })
|
||||
})
|
||||
test('uses the url id and is loading when the snippet is not yet in the store', () => {
|
||||
const result = deriveSnippetIdentity({
|
||||
urlId: 'existing-id',
|
||||
generatedId: 'generated-id',
|
||||
snippets: {},
|
||||
})
|
||||
expect(result).toEqual({ id: 'existing-id', isLoading: true })
|
||||
})
|
||||
test('uses the url id and is loading when the snippet has no content yet', () => {
|
||||
const result = deriveSnippetIdentity({
|
||||
urlId: 'existing-id',
|
||||
generatedId: 'generated-id',
|
||||
snippets: { 'existing-id': { snippet: { content: undefined } } },
|
||||
})
|
||||
expect(result).toEqual({ id: 'existing-id', isLoading: true })
|
||||
})
|
||||
test('uses the url id and is not loading once the snippet content has arrived', () => {
|
||||
const result = deriveSnippetIdentity({
|
||||
urlId: 'existing-id',
|
||||
generatedId: 'generated-id',
|
||||
snippets: { 'existing-id': { snippet: { content: { some: 'content' } } } },
|
||||
})
|
||||
expect(result).toEqual({ id: 'existing-id', isLoading: false })
|
||||
})
|
||||
})
|
||||
|
||||
const buildDebugSnippet = (uncheckedSql: string) => ({
|
||||
snippet: { content: { unchecked_sql: untrustedSql(uncheckedSql) } },
|
||||
})
|
||||
|
||||
describe('SQLEditor.utils.ts:extractDebugContext', () => {
|
||||
test('strips the AI disclaimer comment and trims the sql', () => {
|
||||
const snippet = buildDebugSnippet(`${sqlAiDisclaimerComment}\n\nselect 1;`)
|
||||
const result = { error: { message: 'relation does not exist' } }
|
||||
expect(extractDebugContext(snippet, result)).toEqual({
|
||||
sql: 'select 1;',
|
||||
errorMessage: 'relation does not exist',
|
||||
})
|
||||
})
|
||||
test('falls back to an empty sql when the snippet is undefined', () => {
|
||||
expect(extractDebugContext(undefined, { error: { message: 'boom' } })).toEqual({
|
||||
sql: '',
|
||||
errorMessage: 'boom',
|
||||
})
|
||||
})
|
||||
test('falls back to an empty sql when the snippet has no content yet', () => {
|
||||
expect(
|
||||
extractDebugContext({ snippet: { content: undefined } }, { error: { message: 'boom' } })
|
||||
).toEqual({ sql: '', errorMessage: 'boom' })
|
||||
})
|
||||
test('falls back to "Unknown error" when the result is undefined', () => {
|
||||
const snippet = buildDebugSnippet('select 1;')
|
||||
expect(extractDebugContext(snippet, undefined)).toEqual({
|
||||
sql: 'select 1;',
|
||||
errorMessage: 'Unknown error',
|
||||
})
|
||||
})
|
||||
test('falls back to "Unknown error" when the result has no error message', () => {
|
||||
const snippet = buildDebugSnippet('select 1;')
|
||||
expect(extractDebugContext(snippet, {})).toEqual({
|
||||
sql: 'select 1;',
|
||||
errorMessage: 'Unknown error',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SQLEditor.utils.ts:buildDebugChatArgs', () => {
|
||||
test('builds the newChat payload from the snippet sql and error message', () => {
|
||||
const snippet = buildDebugSnippet('select 1;')
|
||||
const result = { error: { message: 'relation does not exist' } }
|
||||
expect(buildDebugChatArgs(snippet, result)).toEqual({
|
||||
name: 'Debug SQL snippet',
|
||||
sqlSnippets: ['select 1;'],
|
||||
initialInput:
|
||||
'Help me to debug the attached sql snippet which gives the following error: \n\nrelation does not exist',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SQLEditor.utils.ts:buildCompletionRequestBody', () => {
|
||||
test('builds the base request body with no extra options', () => {
|
||||
expect(
|
||||
buildCompletionRequestBody({
|
||||
projectRef: 'default',
|
||||
connectionString: 'postgresql://example',
|
||||
orgSlug: 'acme',
|
||||
})
|
||||
).toEqual({
|
||||
projectRef: 'default',
|
||||
connectionString: 'postgresql://example',
|
||||
language: 'sql',
|
||||
orgSlug: 'acme',
|
||||
})
|
||||
})
|
||||
test('merges options on top of the base fields', () => {
|
||||
expect(
|
||||
buildCompletionRequestBody({
|
||||
projectRef: 'default',
|
||||
connectionString: 'postgresql://example',
|
||||
orgSlug: 'acme',
|
||||
options: { completionMetadata: { prompt: 'add a where clause' } },
|
||||
})
|
||||
).toEqual({
|
||||
projectRef: 'default',
|
||||
connectionString: 'postgresql://example',
|
||||
language: 'sql',
|
||||
orgSlug: 'acme',
|
||||
completionMetadata: { prompt: 'add a where clause' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('SQLEditor.utils.ts:planDiffRequestApplication', () => {
|
||||
test('replaces the editor content when it is empty', () => {
|
||||
const plan = planDiffRequestApplication({
|
||||
existingValue: '',
|
||||
request: { diffType: DiffType.Modification, sql: 'select 1;' },
|
||||
})
|
||||
expect(plan).toEqual({ kind: 'replace', text: 'select 1;' })
|
||||
})
|
||||
test('opens a diff against the existing content when it is not empty', () => {
|
||||
const plan = planDiffRequestApplication({
|
||||
existingValue: 'select 0;',
|
||||
request: { diffType: DiffType.Modification, sql: 'select 1;' },
|
||||
})
|
||||
expect(plan).toEqual({
|
||||
kind: 'diff',
|
||||
diff: { original: 'select 0;', modified: 'select 1;' },
|
||||
diffType: DiffType.Modification,
|
||||
})
|
||||
})
|
||||
test('carries through the requested diff type', () => {
|
||||
const plan = planDiffRequestApplication({
|
||||
existingValue: 'select 0;',
|
||||
request: { diffType: DiffType.NewSnippet, sql: 'select 1;' },
|
||||
})
|
||||
expect(plan).toEqual({
|
||||
kind: 'diff',
|
||||
diff: { original: 'select 0;', modified: 'select 1;' },
|
||||
diffType: DiffType.NewSnippet,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const keyEvent = (overrides: Partial<Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey'>> = {}) => ({
|
||||
key: 'a',
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('SQLEditor.utils.ts:resolveDiffKeyAction', () => {
|
||||
test('does nothing when neither a diff nor the prompt is open', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Escape' }), {
|
||||
isDiffOpen: false,
|
||||
isPromptOpen: false,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'none' })
|
||||
})
|
||||
test('does nothing for keys other than Enter/Escape', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'a' }), {
|
||||
isDiffOpen: true,
|
||||
isPromptOpen: false,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'none' })
|
||||
})
|
||||
test('accepts on Cmd+Enter on macOS when a diff is open', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Enter', metaKey: true }), {
|
||||
isDiffOpen: true,
|
||||
isPromptOpen: false,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'accept' })
|
||||
})
|
||||
test('accepts on Ctrl+Enter on Windows when a diff is open', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Enter', ctrlKey: true }), {
|
||||
isDiffOpen: true,
|
||||
isPromptOpen: false,
|
||||
os: 'windows',
|
||||
})
|
||||
expect(action).toEqual({ type: 'accept' })
|
||||
})
|
||||
test('does not accept on Ctrl+Enter on macOS (wrong modifier for the OS)', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Enter', ctrlKey: true }), {
|
||||
isDiffOpen: true,
|
||||
isPromptOpen: false,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'none' })
|
||||
})
|
||||
test('does not accept on Enter without the modifier key', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Enter' }), {
|
||||
isDiffOpen: true,
|
||||
isPromptOpen: false,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'none' })
|
||||
})
|
||||
test('does not accept on Cmd+Enter when the diff is not open, even if the prompt is', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Enter', metaKey: true }), {
|
||||
isDiffOpen: false,
|
||||
isPromptOpen: true,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'none' })
|
||||
})
|
||||
test('escapes with shouldDiscard true when a diff is open', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Escape' }), {
|
||||
isDiffOpen: true,
|
||||
isPromptOpen: false,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'escape', shouldDiscard: true })
|
||||
})
|
||||
test('escapes with shouldDiscard false when only the prompt is open', () => {
|
||||
const action = resolveDiffKeyAction(keyEvent({ key: 'Escape' }), {
|
||||
isDiffOpen: false,
|
||||
isPromptOpen: true,
|
||||
os: 'macos',
|
||||
})
|
||||
expect(action).toEqual({ type: 'escape', shouldDiscard: false })
|
||||
})
|
||||
})
|
||||
|
||||
describe(`SQLEditor.utils.ts:checkDestructiveQuery`, () => {
|
||||
it('drop statement matches', () => {
|
||||
const match = checkDestructiveQuery('drop table films, distributors;')
|
||||
|
||||
@@ -15,7 +15,12 @@ import {
|
||||
untitledSnippetTitle,
|
||||
updateWithoutWhereRegex,
|
||||
} from './SQLEditor.constants'
|
||||
import { ContentDiff, type IStandaloneCodeEditor, type PotentialIssues } from './SQLEditor.types'
|
||||
import {
|
||||
ContentDiff,
|
||||
DiffType,
|
||||
type IStandaloneCodeEditor,
|
||||
type PotentialIssues,
|
||||
} from './SQLEditor.types'
|
||||
import type { SnippetWithContent } from '@/data/content/sql-folders-query'
|
||||
import type { DatabaseEventTrigger } from '@/data/database-event-triggers/database-event-triggers-query'
|
||||
import type { Database } from '@/data/read-replicas/replicas-query'
|
||||
@@ -290,6 +295,26 @@ export function buildExecuteParams({
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the stable snippet id + loading state for the editor from the URL.
|
||||
*/
|
||||
export function deriveSnippetIdentity({
|
||||
urlId,
|
||||
generatedId,
|
||||
snippets,
|
||||
}: {
|
||||
urlId: string | undefined
|
||||
generatedId: string
|
||||
snippets: Record<string, { snippet: { content?: unknown } }>
|
||||
}): { id: string; isLoading: boolean } {
|
||||
const id = !urlId || urlId === 'new' ? generatedId : urlId
|
||||
|
||||
const snippetIsLoading = !(id in snippets && snippets[id].snippet.content !== undefined)
|
||||
const isLoading = urlId === 'new' ? false : snippetIsLoading
|
||||
|
||||
return { id, isLoading }
|
||||
}
|
||||
|
||||
export const generateMigrationCliCommand = (id: string, name: string, isNpx = false) =>
|
||||
`
|
||||
${isNpx ? 'npx ' : ''}supabase snippets download ${id} |
|
||||
@@ -447,6 +472,37 @@ export function assembleCompletionDiff(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the request body sent to the AI completion endpoint. `options` is
|
||||
* the caller-provided extra fields (e.g. `completionMetadata`), merged in
|
||||
* last so it can override the defaults if it ever needs to.
|
||||
*/
|
||||
export function buildCompletionRequestBody({
|
||||
projectRef,
|
||||
connectionString,
|
||||
orgSlug,
|
||||
options,
|
||||
}: {
|
||||
projectRef: string | undefined
|
||||
connectionString: string | undefined | null
|
||||
orgSlug: string | undefined
|
||||
options?: { completionMetadata?: unknown }
|
||||
}): {
|
||||
projectRef: string | undefined
|
||||
connectionString: string | undefined | null
|
||||
language: 'sql'
|
||||
orgSlug: string | undefined
|
||||
completionMetadata?: unknown
|
||||
} {
|
||||
return {
|
||||
projectRef,
|
||||
connectionString,
|
||||
language: 'sql',
|
||||
orgSlug,
|
||||
...(options ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the prompt text used to ask the assistant to debug a failing snippet.
|
||||
*/
|
||||
@@ -454,3 +510,98 @@ export function buildDebugPromptText(sql: string, errorMessage: string): string
|
||||
const prompt = `Help me to debug the attached sql snippet which gives the following error: \n\n${errorMessage}`
|
||||
return `${prompt}\n\nSQL Query:\n\`\`\`sql\n${sql}\n\`\`\``
|
||||
}
|
||||
|
||||
type DebugSnippet = { snippet: { content?: { unchecked_sql?: UntrustedSqlFragment } } } | undefined
|
||||
type DebugResult = { error?: { message?: string } } | undefined
|
||||
|
||||
/**
|
||||
* Extracts the SQL (disclaimer stripped) and error message used to debug a
|
||||
* failing snippet, shared by `buildDebugPrompt` and `onDebug`. Falls back to
|
||||
* an empty query / `'Unknown error'` rather than throwing when the snippet or
|
||||
* its last result aren't available yet.
|
||||
*/
|
||||
export function extractDebugContext(
|
||||
snippet: DebugSnippet,
|
||||
result: DebugResult
|
||||
): { sql: string; errorMessage: string } {
|
||||
const sql = (snippet?.snippet.content?.unchecked_sql ?? '')
|
||||
.replace(sqlAiDisclaimerComment, '')
|
||||
.trim()
|
||||
const errorMessage = result?.error?.message ?? 'Unknown error'
|
||||
return { sql, errorMessage }
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the `aiSnap.newChat(...)` payload for the debug-this-snippet flow.
|
||||
*/
|
||||
export function buildDebugChatArgs(
|
||||
snippet: DebugSnippet,
|
||||
result: DebugResult
|
||||
): { name: string; sqlSnippets: string[]; initialInput: string } {
|
||||
const { sql, errorMessage } = extractDebugContext(snippet, result)
|
||||
return {
|
||||
name: 'Debug SQL snippet',
|
||||
sqlSnippets: [sql],
|
||||
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${errorMessage}`,
|
||||
}
|
||||
}
|
||||
|
||||
/** What `drainDiffRequest` should do with a pending diff request, given the editor's current value. */
|
||||
export type DiffRequestPlan =
|
||||
| { kind: 'replace'; text: string }
|
||||
| { kind: 'diff'; diff: ContentDiff; diffType: DiffType }
|
||||
|
||||
/**
|
||||
* Decides how to apply a pending diff request to the editor: if the editor is
|
||||
* empty, just copy the request's SQL straight in; otherwise open a diff
|
||||
* between what's there and the requested SQL. Pure decision only — the
|
||||
* effect (`drainDiffRequest`) is left to actually touch the editor/diff state.
|
||||
*/
|
||||
export function planDiffRequestApplication({
|
||||
existingValue,
|
||||
request,
|
||||
}: {
|
||||
existingValue: string
|
||||
request: { diffType: DiffType; sql: string }
|
||||
}): DiffRequestPlan {
|
||||
if (existingValue.length === 0) {
|
||||
return { kind: 'replace', text: request.sql }
|
||||
}
|
||||
return {
|
||||
kind: 'diff',
|
||||
diff: { original: existingValue, modified: request.sql },
|
||||
diffType: request.diffType,
|
||||
}
|
||||
}
|
||||
|
||||
/** What the window keydown handler should do for a key event, given the diff/prompt state. */
|
||||
export type DiffKeyAction =
|
||||
| { type: 'accept' }
|
||||
| { type: 'escape'; shouldDiscard: boolean }
|
||||
| { type: 'none' }
|
||||
|
||||
/**
|
||||
* Decides how the SQL editor's window-level keydown handler should react:
|
||||
* accept an open diff on Cmd/Ctrl+Enter, or discard-and-dismiss on Escape.
|
||||
* No-ops when neither a diff nor the AI prompt is open, or for any other key.
|
||||
*/
|
||||
export function resolveDiffKeyAction(
|
||||
e: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey'>,
|
||||
{
|
||||
isDiffOpen,
|
||||
isPromptOpen,
|
||||
os,
|
||||
}: { isDiffOpen: boolean; isPromptOpen: boolean; os: 'macos' | 'windows' | undefined }
|
||||
): DiffKeyAction {
|
||||
if (!isDiffOpen && !isPromptOpen) return { type: 'none' }
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
if ((os === 'macos' ? e.metaKey : e.ctrlKey) && isDiffOpen) return { type: 'accept' }
|
||||
return { type: 'none' }
|
||||
case 'Escape':
|
||||
return { type: 'escape', shouldDiscard: isDiffOpen }
|
||||
default:
|
||||
return { type: 'none' }
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useParams } from 'common'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { generateSnippetTitle } from './SQLEditor.constants'
|
||||
import { deriveSnippetIdentity } from './SQLEditor.utils'
|
||||
import { generateUuid } from '@/lib/api/snippets.browser'
|
||||
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
|
||||
|
||||
@@ -23,13 +24,11 @@ export function useSnippetIdentity() {
|
||||
return [name, generateUuid([`${name}.sql`])]
|
||||
}, [urlId])
|
||||
|
||||
// the id is stable across renders - it depends either on the url or on the memoized generated id
|
||||
const id = !urlId || urlId === 'new' ? generatedId : urlId
|
||||
|
||||
const snippetIsLoading = !(
|
||||
id in snapV2.snippets && snapV2.snippets[id].snippet.content !== undefined
|
||||
)
|
||||
const isLoading = urlId === 'new' ? false : snippetIsLoading
|
||||
const { id, isLoading } = deriveSnippetIdentity({
|
||||
urlId,
|
||||
generatedId,
|
||||
snippets: snapV2.snippets,
|
||||
})
|
||||
|
||||
return { id, urlId, generatedNewSnippetName, isLoading }
|
||||
}
|
||||
@@ -4,12 +4,15 @@ import { useCallback, useEffect, useEffectEvent, useMemo, useState } from 'react
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import type { useSqlEditorDiff, useSqlEditorPrompt } from './hooks'
|
||||
import { sqlAiDisclaimerComment } from './SQLEditor.constants'
|
||||
import { DiffType, type IStandaloneDiffEditor } from './SQLEditor.types'
|
||||
import {
|
||||
assembleCompletionDiff,
|
||||
buildCompletionRequestBody,
|
||||
buildDebugChatArgs,
|
||||
buildDebugPromptText,
|
||||
createSqlSnippetSkeletonV2,
|
||||
extractDebugContext,
|
||||
planDiffRequestApplication,
|
||||
} from './SQLEditor.utils'
|
||||
import { useSQLEditorContext } from './SQLEditorContext'
|
||||
import { useSnippetTitleGenerator } from './useSnippetTitleGenerator'
|
||||
@@ -100,10 +103,7 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi
|
||||
const buildDebugPrompt = useCallback(() => {
|
||||
const snippet = snapV2.snippets[id]
|
||||
const result = sessionSnap.results[id]?.[0]
|
||||
const sql = (snippet?.snippet.content?.unchecked_sql ?? '')
|
||||
.replace(sqlAiDisclaimerComment, '')
|
||||
.trim()
|
||||
const errorMessage = result?.error?.message ?? 'Unknown error'
|
||||
const { sql, errorMessage } = extractDebugContext(snippet, result)
|
||||
|
||||
return buildDebugPromptText(sql, errorMessage)
|
||||
}, [id, sessionSnap.results, snapV2.snippets])
|
||||
@@ -113,13 +113,7 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi
|
||||
const snippet = snapV2.snippets[id]
|
||||
const result = sessionSnap.results[id]?.[0]
|
||||
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
|
||||
aiSnap.newChat({
|
||||
name: 'Debug SQL snippet',
|
||||
sqlSnippets: [
|
||||
(snippet.snippet.content?.unchecked_sql ?? '').replace(sqlAiDisclaimerComment, '').trim(),
|
||||
],
|
||||
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${result.error.message}`,
|
||||
})
|
||||
aiSnap.newChat(buildDebugChatArgs(snippet, result))
|
||||
} catch (error: unknown) {
|
||||
// [Joshen] There's a tendency for the SQL debug to chuck a lengthy error message
|
||||
// that's not relevant for the user - so we prettify it here by avoiding to return the
|
||||
@@ -206,13 +200,14 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi
|
||||
'Content-Type': 'application/json',
|
||||
...(options?.headers ?? {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
language: 'sql',
|
||||
orgSlug: org?.slug,
|
||||
...(options?.body ?? {}),
|
||||
}),
|
||||
body: JSON.stringify(
|
||||
buildCompletionRequestBody({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
orgSlug: org?.slug,
|
||||
options: options?.body,
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -333,21 +328,19 @@ export function useSqlEditorAi({ id, editorMountCount, diff, prompt }: UseSqlEdi
|
||||
// on mount and re-runs this effect, so the request applies once mounted.
|
||||
if (!editorModel) return
|
||||
|
||||
const { diffType, sql } = request
|
||||
const existingValue = editorRef.current?.getValue() ?? ''
|
||||
if (existingValue.length === 0) {
|
||||
const plan = planDiffRequestApplication({ existingValue, request })
|
||||
if (plan.kind === 'replace') {
|
||||
// if the editor is empty, just copy over the code
|
||||
editorRef.current?.executeEdits('apply-ai-message', [
|
||||
{
|
||||
text: `${sql}`,
|
||||
text: plan.text,
|
||||
range: editorModel.getFullModelRange(),
|
||||
},
|
||||
])
|
||||
} else {
|
||||
const currentSql = editorRef.current?.getValue()
|
||||
const diff = { original: currentSql || '', modified: sql }
|
||||
setSourceSqlDiff(diff)
|
||||
setSelectedDiffType(diffType)
|
||||
setSourceSqlDiff(plan.diff)
|
||||
setSelectedDiffType(plan.diffType)
|
||||
}
|
||||
|
||||
// One-shot: drain the request so it can't re-apply to a later editor or session.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
|
||||
import { resolveDiffKeyAction } from './SQLEditor.utils'
|
||||
import { useSQLEditorContext } from './SQLEditorContext'
|
||||
import { detectOS } from '@/lib/helpers'
|
||||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||||
@@ -56,20 +57,20 @@ export function useSqlEditorShortcuts({
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (!isDiffOpen && !isPromptOpen) return
|
||||
const action = resolveDiffKeyAction(e, { isDiffOpen, isPromptOpen, os })
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
if ((os === 'macos' ? e.metaKey : e.ctrlKey) && isDiffOpen) {
|
||||
acceptAiHandler()
|
||||
resetPrompt()
|
||||
}
|
||||
switch (action.type) {
|
||||
case 'accept':
|
||||
acceptAiHandler()
|
||||
resetPrompt()
|
||||
return
|
||||
case 'Escape':
|
||||
if (isDiffOpen) discardAiHandler()
|
||||
case 'escape':
|
||||
if (action.shouldDiscard) discardAiHandler()
|
||||
resetPrompt()
|
||||
editorRef.current?.focus()
|
||||
return
|
||||
case 'none':
|
||||
return
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handler)
|
||||
|
||||
Reference in new issue
Block a user