-
-
Notifications
You must be signed in to change notification settings - Fork 161
Fix/postgres alias column autocomplete #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { useEffect } from "react"; | ||
| import type { Monaco } from "@monaco-editor/react"; | ||
| import { loader } from "@monaco-editor/react"; | ||
| import { useDatabase } from "./useDatabase"; | ||
| import { isMultiDatabaseCapable } from "../utils/database"; | ||
| import { registerSqlAutocomplete } from "../utils/autocomplete"; | ||
|
|
||
| type Options = { | ||
| monaco?: Monaco | null; | ||
| schema?: string | null; | ||
| /** When false, skips registration (e.g. inactive notebook tabs). Defaults to true. */ | ||
| enabled?: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * Keeps the global SQL completion provider in sync with the active connection. | ||
| * Pass `monaco` from the main editor when available; otherwise Monaco is loaded via loader.init (notebook). | ||
| */ | ||
| export function useSqlAutocompleteRegistration( | ||
| connectionId: string | null, | ||
| options?: Options, | ||
| ) { | ||
| const { | ||
| tables, | ||
| activeDriver, | ||
| activeSchema, | ||
| activeCapabilities, | ||
| schemaDataMap, | ||
| databaseDataMap, | ||
| selectedDatabases, | ||
| } = useDatabase(); | ||
|
|
||
| const schema = options?.schema ?? activeSchema; | ||
| const isMultiDb = | ||
| isMultiDatabaseCapable(activeCapabilities) && selectedDatabases.length > 1; | ||
|
|
||
| const enabled = options?.enabled ?? true; | ||
|
|
||
| useEffect(() => { | ||
| if (!connectionId || !enabled) return; | ||
|
|
||
| let cancelled = false; | ||
|
|
||
| const register = (monaco: Monaco) => { | ||
| if (cancelled) return; | ||
|
|
||
| let effectiveTables = tables; | ||
| if (activeCapabilities?.schemas && schema) { | ||
| effectiveTables = schemaDataMap[schema]?.tables ?? tables; | ||
| } else if (isMultiDb) { | ||
| effectiveTables = selectedDatabases.flatMap( | ||
| (db) => databaseDataMap[db]?.tables ?? [], | ||
| ); | ||
| } | ||
|
|
||
| registerSqlAutocomplete( | ||
| monaco, | ||
| connectionId, | ||
| effectiveTables, | ||
| schema, | ||
| activeDriver ?? null, | ||
| ); | ||
| }; | ||
|
|
||
| if (options?.monaco) { | ||
| register(options.monaco); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| } | ||
|
|
||
| loader.init().then((monaco) => register(monaco)); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [ | ||
| connectionId, | ||
| enabled, | ||
| options?.monaco, | ||
| schema, | ||
| tables, | ||
| activeDriver, | ||
| activeCapabilities, | ||
| schemaDataMap, | ||
| databaseDataMap, | ||
| isMultiDb, | ||
| selectedDatabases, | ||
| ]); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||
| import type { Monaco } from "@monaco-editor/react"; | ||||||||||||||||||||||||||||||||
| import { invoke } from "@tauri-apps/api/core"; | ||||||||||||||||||||||||||||||||
| import type { TableInfo } from "../contexts/DatabaseContext"; | ||||||||||||||||||||||||||||||||
| import { formatSqlIdentifier, quoteTableRef } from "./identifiers"; | ||||||||||||||||||||||||||||||||
| import { getCurrentStatement, parseTablesFromQuery } from "./sqlAnalysis"; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Lightweight column cache with TTL and size limits | ||||||||||||||||||||||||||||||||
|
|
@@ -98,11 +99,32 @@ export const clearAutocompleteCache = (connectionId?: string) => { | |||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Find a table by name in the list of tables | ||||||||||||||||||||||||||||||||
| const findTableByName = (name: string, tables: TableInfo[]) => | ||||||||||||||||||||||||||||||||
| tables.find((t) => t.name.toLowerCase() === name.toLowerCase())?.name; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const tableInsertText = ( | ||||||||||||||||||||||||||||||||
| tableName: string, | ||||||||||||||||||||||||||||||||
| driver?: string | null, | ||||||||||||||||||||||||||||||||
| schema?: string | null, | ||||||||||||||||||||||||||||||||
| ) => | ||||||||||||||||||||||||||||||||
| schema | ||||||||||||||||||||||||||||||||
| ? quoteTableRef(tableName, driver, schema) | ||||||||||||||||||||||||||||||||
| : formatSqlIdentifier(tableName, driver); | ||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The completion session is already scoped to the active schema (
Suggested change
If you want cross-schema qualification later, qualify only when |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| let sqlCompletionProvider: { dispose: () => void } | null = null; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export const disposeSqlAutocomplete = (): void => { | ||||||||||||||||||||||||||||||||
| sqlCompletionProvider?.dispose(); | ||||||||||||||||||||||||||||||||
| sqlCompletionProvider = null; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export const registerSqlAutocomplete = ( | ||||||||||||||||||||||||||||||||
| monaco: Monaco, | ||||||||||||||||||||||||||||||||
| connectionId: string | null, | ||||||||||||||||||||||||||||||||
| tables: TableInfo[], | ||||||||||||||||||||||||||||||||
| schema?: string | null, | ||||||||||||||||||||||||||||||||
| driver?: string | null, | ||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||
| const provider = monaco.languages.registerCompletionItemProvider("sql", { | ||||||||||||||||||||||||||||||||
| triggerCharacters: [".", " "], | ||||||||||||||||||||||||||||||||
|
|
@@ -141,13 +163,13 @@ export const registerSqlAutocomplete = ( | |||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Check if it's an alias or table name | ||||||||||||||||||||||||||||||||
| let actualTableName = tableAliases?.get(typedName); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (!actualTableName) { | ||||||||||||||||||||||||||||||||
| // Try direct table name match | ||||||||||||||||||||||||||||||||
| const foundTable = tables.find(t => t.name.toLowerCase() === typedName); | ||||||||||||||||||||||||||||||||
| actualTableName = foundTable?.name; | ||||||||||||||||||||||||||||||||
| actualTableName = findTableByName(typedName, tables); | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| actualTableName = findTableByName(actualTableName, tables) ?? actualTableName; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (actualTableName) { | ||||||||||||||||||||||||||||||||
| const columns = await getTableColumns(connectionId, actualTableName, schema); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
@@ -163,13 +185,15 @@ export const registerSqlAutocomplete = ( | |||||||||||||||||||||||||||||||
| label: c.label, | ||||||||||||||||||||||||||||||||
| kind: monaco.languages.CompletionItemKind.Field, | ||||||||||||||||||||||||||||||||
| detail: c.detail, | ||||||||||||||||||||||||||||||||
| insertText: c.label, | ||||||||||||||||||||||||||||||||
| insertText: formatSqlIdentifier(c.label, driver), | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Columns get the same always-quote treatment —
Suggested change
|
||||||||||||||||||||||||||||||||
| range: columnRange, | ||||||||||||||||||||||||||||||||
| sortText: `0_${c.label}`, | ||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return { suggestions }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return { suggestions: [] }; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // ============================================ | ||||||||||||||||||||||||||||||||
|
|
@@ -188,7 +212,7 @@ export const registerSqlAutocomplete = ( | |||||||||||||||||||||||||||||||
| // User is inside a query with FROM/JOIN - suggest columns from those tables | ||||||||||||||||||||||||||||||||
| const tableNames = Array.from(new Set(tableAliases.values())); | ||||||||||||||||||||||||||||||||
| const matchingTables = tableNames | ||||||||||||||||||||||||||||||||
| .map(name => tables.find(t => t.name.toLowerCase() === name.toLowerCase())) | ||||||||||||||||||||||||||||||||
| .map((name) => tables.find((t) => t.name.toLowerCase() === name.toLowerCase())) | ||||||||||||||||||||||||||||||||
| .filter(Boolean) as TableInfo[]; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Limit parallel fetches to prevent memory spikes | ||||||||||||||||||||||||||||||||
|
|
@@ -223,7 +247,7 @@ export const registerSqlAutocomplete = ( | |||||||||||||||||||||||||||||||
| label: col.label, | ||||||||||||||||||||||||||||||||
| kind: monaco.languages.CompletionItemKind.Field, | ||||||||||||||||||||||||||||||||
| detail: `${col.detail} — ${table.name}${aliasHint}`, | ||||||||||||||||||||||||||||||||
| insertText: col.label, | ||||||||||||||||||||||||||||||||
| insertText: formatSqlIdentifier(col.label, driver), | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same for the context-column insert path:
Suggested change
|
||||||||||||||||||||||||||||||||
| range, | ||||||||||||||||||||||||||||||||
| sortText: `0_${col.label}`, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
@@ -259,7 +283,7 @@ export const registerSqlAutocomplete = ( | |||||||||||||||||||||||||||||||
| label: t.name, | ||||||||||||||||||||||||||||||||
| kind: monaco.languages.CompletionItemKind.Class, | ||||||||||||||||||||||||||||||||
| detail: "Table", | ||||||||||||||||||||||||||||||||
| insertText: t.name, | ||||||||||||||||||||||||||||||||
| insertText: tableInsertText(t.name, driver, schema), | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Match the simplified
Suggested change
|
||||||||||||||||||||||||||||||||
| range, | ||||||||||||||||||||||||||||||||
| sortText: `1_${t.name}` | ||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||
|
|
@@ -274,5 +298,7 @@ export const registerSqlAutocomplete = ( | |||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| sqlCompletionProvider?.dispose(); | ||||||||||||||||||||||||||||||||
| sqlCompletionProvider = provider; | ||||||||||||||||||||||||||||||||
| return provider; | ||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the root cause of point #1 above. The global provider mirrors the active editor, so only dispose it when the connection being torn down is actually the active one — otherwise disconnecting a background connection kills autocomplete in the editor you're using:
The health-check handler at L786 has the same issue, but there
activeConnectionIdis a stale closure — you'd need anactiveConnectionIdRef(or move disposal into the active-connection-change path). Cleanest long-term: letuseSqlAutocompleteRegistrationown disposal in its cleanup and drop both calls from here entirely.