feat(query-editor): 为 Postgres 兼容方言补全增加标识符自动引用

- 在 SQL 编辑器补全中识别需要保留大小写的对象名
- 自动为大写表名和字段名插入双引号标识符
- 保持 MySQL 等其它方言现有补全行为不变
- 补充 QueryEditor 相关测试覆盖

Close #562
This commit is contained in:
Syngnat
2026-06-14 15:54:00 +08:00
parent 6cb5998cd6
commit 5310ec7c44
2 changed files with 122 additions and 8 deletions

View File

@@ -994,6 +994,108 @@ describe('QueryEditor external SQL save', () => {
});
});
it('quotes uppercase postgres table names in FROM completion insert text', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
storeState.connections[0].config.type = 'postgres';
storeState.connections[0].config.database = 'main';
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Table: 'public.MyTable' }] });
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: '', dbName: 'main' })} />);
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});
const sqlProvider = editorState.providers.find((provider) => Array.isArray(provider.triggerCharacters) && provider.triggerCharacters.includes('.'));
expect(sqlProvider).toBeTruthy();
editorState.value = 'SELECT * FROM My';
editorState.latestOnChange?.(editorState.value);
const result = await sqlProvider.provideCompletionItems(editorState.editor.getModel(), { lineNumber: 1, column: editorState.value.length + 1 });
const match = result.suggestions.find((item: any) => item.label === 'MyTable');
expect(match?.insertText).toBe('"MyTable"');
await act(async () => {
renderer.unmount();
});
});
it('quotes uppercase postgres table names after schema qualifiers in completion insert text', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
storeState.connections[0].config.type = 'postgres';
storeState.connections[0].config.database = 'main';
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Table: 'public.MyTable' }] });
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: '', dbName: 'main' })} />);
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});
const sqlProvider = editorState.providers.find((provider) => Array.isArray(provider.triggerCharacters) && provider.triggerCharacters.includes('.'));
expect(sqlProvider).toBeTruthy();
editorState.value = 'SELECT * FROM public.';
editorState.latestOnChange?.(editorState.value);
const result = await sqlProvider.provideCompletionItems(editorState.editor.getModel(), { lineNumber: 1, column: editorState.value.length + 1 });
const match = result.suggestions.find((item: any) => item.label === 'MyTable');
expect(match?.insertText).toBe('"MyTable"');
await act(async () => {
renderer.unmount();
});
});
it('quotes uppercase postgres column names in completion insert text', async () => {
let renderer!: ReactTestRenderer;
autoFetchState.visible = true;
storeState.connections[0].config.type = 'postgres';
storeState.connections[0].config.database = 'main';
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [{ Table: 'public.MyTable' }] });
backendApp.DBGetAllColumns.mockResolvedValueOnce({
success: true,
data: [{ tableName: 'public.MyTable', name: 'DisplayName', type: 'text' }],
});
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ query: '', dbName: 'main' })} />);
});
await act(async () => {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
});
const sqlProvider = editorState.providers.find((provider) => Array.isArray(provider.triggerCharacters) && provider.triggerCharacters.includes('.'));
expect(sqlProvider).toBeTruthy();
editorState.value = 'SELECT Dis FROM public."MyTable"';
editorState.latestOnChange?.(editorState.value);
const result = await sqlProvider.provideCompletionItems(editorState.editor.getModel(), { lineNumber: 1, column: 'SELECT Dis'.length + 1 });
const match = result.suggestions.find((item: any) => item.label === 'DisplayName');
expect(match?.insertText).toBe('"DisplayName"');
await act(async () => {
renderer.unmount();
});
});
it('resolves database and table targets for ctrl/cmd navigation', () => {
const tables = [
{ dbName: 'main', tableName: 'users' },