mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
558bee9ebf
## Context Currently with the SQL Editor, the auto-completion via intellisense only works nicely with the `.` operator - e.g after keying in a schema and trying to find a table as such: <img width="500" alt="image" src="https://github.com/user-attachments/assets/86ec8455-47b9-43d3-925a-c13d0fd6ac44" /> But lacks support for finding the columns of a table after a `where` clause - so the changes here addresses that by mainly adjust the `PgSQLCompletionProvider` Also addresses a number of type fixes (replaces all the `any` types) <img width="500" alt="image" src="https://github.com/user-attachments/assets/e23672a6-2b48-4a98-b300-69f822d12b38" /> <img width="415" height="319" alt="image" src="https://github.com/user-attachments/assets/e6ee64d2-90dd-47d6-9fd2-e8b07821ebb6" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved PostgreSQL SQL editor suggestions with table, column, alias, schema, and join-aware completions. * Added context-aware support for statements, quoted identifiers, subqueries, and qualified columns. * Enhanced PostgreSQL function signature assistance. * Added safer behavior when database metadata is incomplete or unavailable. * **Bug Fixes** * Prioritized relevant columns and removed duplicate suggestions. * **Tests** * Added comprehensive coverage for SQL parsing, metadata handling, and completion behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
157 lines
5.4 KiB
TypeScript
157 lines
5.4 KiB
TypeScript
import { QueryClient } from '@tanstack/react-query'
|
|
import { waitFor } from '@testing-library/react'
|
|
import { HttpResponse } from 'msw'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { acquireSharedRegistration, useAddDefinitions } from './useAddDefinitions'
|
|
import { customRenderHook } from '@/tests/lib/custom-render'
|
|
import { addAPIMock } from '@/tests/lib/msw'
|
|
import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils'
|
|
|
|
vi.mock('@/components/ui/CodeEditor/Providers/PgSQLCompletionProvider', () => ({
|
|
getPgsqlCompletionProvider: vi.fn((_monaco: unknown, pgInfoRef: unknown) => ({
|
|
__pgInfoRef: pgInfoRef,
|
|
})),
|
|
}))
|
|
vi.mock('@/components/ui/CodeEditor/Providers/PgSQLSignatureHelpProvider', () => ({
|
|
getPgsqlSignatureHelpProvider: vi.fn((_monaco: unknown, pgInfoRef: unknown) => ({
|
|
__pgInfoRef: pgInfoRef,
|
|
})),
|
|
}))
|
|
|
|
describe('acquireSharedRegistration', () => {
|
|
it('only registers once for multiple concurrent callers sharing a key', () => {
|
|
const register = vi.fn(() => ({ dispose: vi.fn() }))
|
|
|
|
acquireSharedRegistration('test-key-1', register)
|
|
acquireSharedRegistration('test-key-1', register)
|
|
acquireSharedRegistration('test-key-1', register)
|
|
|
|
expect(register).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('registers independently per key', () => {
|
|
const register = vi.fn(() => ({ dispose: vi.fn() }))
|
|
|
|
acquireSharedRegistration('test-key-2a', register)
|
|
acquireSharedRegistration('test-key-2b', register)
|
|
|
|
expect(register).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('does not dispose while other callers are still holding the registration', () => {
|
|
const dispose = vi.fn()
|
|
const register = vi.fn(() => ({ dispose }))
|
|
|
|
const releaseA = acquireSharedRegistration('test-key-3', register)
|
|
acquireSharedRegistration('test-key-3', register)
|
|
|
|
releaseA()
|
|
|
|
expect(dispose).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('disposes once the last caller releases', () => {
|
|
const dispose = vi.fn()
|
|
const register = vi.fn(() => ({ dispose }))
|
|
|
|
const releaseA = acquireSharedRegistration('test-key-4', register)
|
|
const releaseB = acquireSharedRegistration('test-key-4', register)
|
|
|
|
releaseA()
|
|
releaseB()
|
|
|
|
expect(dispose).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('registers again after a full release cycle', () => {
|
|
const register = vi.fn(() => ({ dispose: vi.fn() }))
|
|
|
|
const release = acquireSharedRegistration('test-key-5', register)
|
|
release()
|
|
acquireSharedRegistration('test-key-5', register)
|
|
|
|
expect(register).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('is safe to release more times than acquired', () => {
|
|
const dispose = vi.fn()
|
|
const register = vi.fn(() => ({ dispose }))
|
|
|
|
const release = acquireSharedRegistration('test-key-6', register)
|
|
release()
|
|
|
|
expect(() => release()).not.toThrow()
|
|
expect(dispose).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('useAddDefinitions', () => {
|
|
const createMonaco = () =>
|
|
({
|
|
languages: {
|
|
registerCompletionItemProvider: vi.fn(() => ({ dispose: vi.fn() })),
|
|
registerSignatureHelpProvider: vi.fn(() => ({ dispose: vi.fn() })),
|
|
registerDocumentFormattingEditProvider: vi.fn(() => ({ dispose: vi.fn() })),
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
}) as any
|
|
|
|
it('keeps the registered provider reading fresh data after the registering editor unmounts, as long as a sibling editor is still active', async () => {
|
|
const { getPgsqlCompletionProvider } =
|
|
(await import('@/components/ui/CodeEditor/Providers/PgSQLCompletionProvider')) as unknown as {
|
|
getPgsqlCompletionProvider: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
let keywordWords = ['select']
|
|
setupSqlEditorMocks()
|
|
addAPIMock({
|
|
method: 'post',
|
|
path: '/platform/pg-meta/:ref/query',
|
|
response: ({ request }) => {
|
|
const key = new URL(request.url).searchParams.get('key')
|
|
if (key === 'keywords') return HttpResponse.json(keywordWords.map((word) => ({ word })))
|
|
return HttpResponse.json([])
|
|
},
|
|
})
|
|
|
|
const monaco = createMonaco()
|
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
const renderOptions = { queryClient }
|
|
|
|
// Two sibling notebook cells' editors, both mounted at once, sharing the same cache.
|
|
const editor1 = customRenderHook(
|
|
() => useAddDefinitions('', monaco, { enabled: true }),
|
|
renderOptions
|
|
)
|
|
const editor2 = customRenderHook(
|
|
() => useAddDefinitions('', monaco, { enabled: true }),
|
|
renderOptions
|
|
)
|
|
|
|
await waitFor(() =>
|
|
expect(monaco.languages.registerCompletionItemProvider).toHaveBeenCalledTimes(1)
|
|
)
|
|
|
|
// The first (and only, thanks to ref-counted registration) call registered the provider —
|
|
// capture the `pgInfoRef` it reads from.
|
|
const pgInfoRef = getPgsqlCompletionProvider.mock.calls[0][1] as {
|
|
current: { keywords: string[] }
|
|
}
|
|
await waitFor(() => expect(pgInfoRef.current.keywords).toEqual(['select']))
|
|
|
|
// Editor 1 — the one whose render happened to trigger the registration — closes. Editor 2
|
|
// is still open, so the provider must stay registered and stay current.
|
|
editor1.unmount()
|
|
expect(monaco.languages.registerCompletionItemProvider).toHaveBeenCalledTimes(1)
|
|
|
|
keywordWords = ['select', 'insert']
|
|
queryClient.invalidateQueries({ queryKey: ['projects', 'default', 'keywords'] })
|
|
editor2.rerender()
|
|
|
|
await waitFor(() => expect(pgInfoRef.current.keywords).toEqual(['select', 'insert']))
|
|
|
|
editor2.unmount()
|
|
})
|
|
})
|