From 627a8498d0b2e24d74e04ecfd4c53d83c7812054 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 27 Sep 2026 02:56:14 +0000 Subject: [PATCH] feat(sql): add schema qualification completion setting Closes #9219 --- .../editor/EditorSettingsDialog.vue | 40 +++++++++ .../useQueryEditorCompletion.spec.ts | 2 +- .../editor/useQueryEditorCompletion.ts | 4 + .../i18n/__tests__/featureMessages.spec.ts | 1 + apps/desktop/src/i18n/locales/az.ts | 8 ++ apps/desktop/src/i18n/locales/en.ts | 8 ++ apps/desktop/src/i18n/locales/es.ts | 8 ++ apps/desktop/src/i18n/locales/it.ts | 8 ++ apps/desktop/src/i18n/locales/ja.ts | 8 ++ apps/desktop/src/i18n/locales/ko.ts | 8 ++ apps/desktop/src/i18n/locales/pt-BR.ts | 8 ++ apps/desktop/src/i18n/locales/ru.ts | 8 ++ apps/desktop/src/i18n/locales/tr.ts | 8 ++ apps/desktop/src/i18n/locales/zh-CN.ts | 8 ++ apps/desktop/src/i18n/locales/zh-TW.ts | 8 ++ .../__tests__/sql/semantic/completion.spec.ts | 1 + .../sql/sqlCompletion.context.spec.ts | 82 ++++++++++++++++++- .../__tests__/editorSettingsDraft.spec.ts | 19 +++++ .../settings/__tests__/settingsSearch.spec.ts | 1 + .../__tests__/settingsTransfer.spec.ts | 12 +++ .../src/lib/settings/editorSettingsDraft.ts | 3 + .../src/lib/settings/settingsSearch.ts | 1 + .../src/lib/settings/settingsTransfer.ts | 1 + apps/desktop/src/lib/sql/sqlCompletion.ts | 43 +++++++--- .../sql/sqlCompletionSchemaQualification.ts | 8 ++ .../stores/__tests__/settingsStore.spec.ts | 23 ++++++ apps/desktop/src/stores/settingsStore.ts | 25 +++++- 27 files changed, 339 insertions(+), 15 deletions(-) create mode 100644 apps/desktop/src/lib/sql/sqlCompletionSchemaQualification.ts diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index 957ee54888..83021b760f 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -108,6 +108,7 @@ import { type ClickTableNavigationTarget, type EditorSettings, type SqlCompletionTriggerMode, + type SqlTableCompletionSchemaQualification, type TableHoverLookupMode, SIDEBAR_INDENT_MIN, SIDEBAR_INDENT_MAX, @@ -623,6 +624,15 @@ const editShowLineNumbers = ref(settingsStore.editorSettings.showLineNumbers); const editShowCurrentStatementFrame = ref(settingsStore.editorSettings.showCurrentStatementFrame); const editShowInsertValueHints = ref(settingsStore.editorSettings.showInsertValueHints); const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables); +const editTableCompletionSchemaQualification = ref(settingsStore.editorSettings.tableCompletionSchemaQualification); +const tableCompletionSchemaQualificationDescription = computed(() => { + const key = { + never: "settings.tableCompletionSchemaQualificationNeverDescription", + collision: "settings.tableCompletionSchemaQualificationCollisionDescription", + always: "settings.tableCompletionSchemaQualificationAlwaysDescription", + }[editTableCompletionSchemaQualification.value]; + return t(key); +}); const editInsertSpaceAfterCompletion = ref(settingsStore.editorSettings.insertSpaceAfterCompletion); const editSqlServerSpaceConfirmsCompletion = ref(settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion); const showSqlServerSpaceConfirmsCompletion = computed(() => hasSqlServerConnection.value || settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion || editSqlServerSpaceConfirmsCompletion.value); @@ -991,6 +1001,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { showCurrentStatementFrame: editShowCurrentStatementFrame.value, showInsertValueHints: editShowInsertValueHints.value, autoAliasTables: editAutoAliasTables.value, + tableCompletionSchemaQualification: editTableCompletionSchemaQualification.value, insertSpaceAfterCompletion: editInsertSpaceAfterCompletion.value, sqlServerSpaceConfirmsCompletion: editSqlServerSpaceConfirmsCompletion.value, sortCompletionColumnsAlphabetically: editSortCompletionColumnsAlphabetically.value, @@ -1637,6 +1648,7 @@ function syncEditorSettingsDraftFromStore() { editShowCurrentStatementFrame.value = settingsStore.editorSettings.showCurrentStatementFrame; editShowInsertValueHints.value = settingsStore.editorSettings.showInsertValueHints; editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables; + editTableCompletionSchemaQualification.value = settingsStore.editorSettings.tableCompletionSchemaQualification; editInsertSpaceAfterCompletion.value = settingsStore.editorSettings.insertSpaceAfterCompletion; editSqlServerSpaceConfirmsCompletion.value = settingsStore.editorSettings.sqlServerSpaceConfirmsCompletion; editSortCompletionColumnsAlphabetically.value = settingsStore.editorSettings.sortCompletionColumnsAlphabetically; @@ -1775,6 +1787,7 @@ const editorSettingsDraftRefs: EditorSettingsDraftRefMap = { showCurrentStatementFrame: editShowCurrentStatementFrame, showInsertValueHints: editShowInsertValueHints, autoAliasTables: editAutoAliasTables, + tableCompletionSchemaQualification: editTableCompletionSchemaQualification, insertSpaceAfterCompletion: editInsertSpaceAfterCompletion, sqlServerSpaceConfirmsCompletion: editSqlServerSpaceConfirmsCompletion, sortCompletionColumnsAlphabetically: editSortCompletionColumnsAlphabetically, @@ -2263,6 +2276,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints; editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables; + editTableCompletionSchemaQualification.value = DEFAULT_EDITOR_SETTINGS.tableCompletionSchemaQualification; editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion; editSqlServerSpaceConfirmsCompletion.value = DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion; editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically; @@ -2421,6 +2435,7 @@ function resetAllDefaults() { editShowCurrentStatementFrame.value = DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame; editShowInsertValueHints.value = DEFAULT_EDITOR_SETTINGS.showInsertValueHints; editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables; + editTableCompletionSchemaQualification.value = DEFAULT_EDITOR_SETTINGS.tableCompletionSchemaQualification; editInsertSpaceAfterCompletion.value = DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion; editSqlServerSpaceConfirmsCompletion.value = DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion; editSortCompletionColumnsAlphabetically.value = DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically; @@ -2669,6 +2684,12 @@ function onCompletionTriggerModeChange(v: any) { } } +function onTableCompletionSchemaQualificationChange(v: any) { + if (v === "never" || v === "collision" || v === "always") { + editTableCompletionSchemaQualification.value = v; + } +} + function onTableHoverLookupModeChange(v: any) { if (v === "current" || v === "fallback" || v === "always") { editTableHoverLookupMode.value = v; @@ -6449,6 +6470,25 @@ onUnmounted(() => { + +
+
+ +

+ {{ tableCompletionSchemaQualificationDescription }} +

+
+ +
diff --git a/apps/desktop/src/components/editor/__tests__/useQueryEditorCompletion.spec.ts b/apps/desktop/src/components/editor/__tests__/useQueryEditorCompletion.spec.ts index 43e7908709..fde3032a77 100644 --- a/apps/desktop/src/components/editor/__tests__/useQueryEditorCompletion.spec.ts +++ b/apps/desktop/src/components/editor/__tests__/useQueryEditorCompletion.spec.ts @@ -63,7 +63,7 @@ function createHarness(overrides: Partial = {}) { listElasticsearchCompletionFields: vi.fn(async () => []), }; const connectionStore = store as unknown as Options["connectionStore"]; - const settings = reactive({ editorSettings: { completionTriggerMode: "positional", snippets: [], sqlFormatter: { keywordCase: "upper", functionCase: "upper" }, autoAliasTables: false, generateSqlQuoteIdentifiers: false } }); + const settings = reactive({ editorSettings: { completionTriggerMode: "positional", snippets: [], sqlFormatter: { keywordCase: "upper", functionCase: "upper" }, autoAliasTables: false, tableCompletionSchemaQualification: "collision", generateSqlQuoteIdentifiers: false } }); const metadata = useQueryEditorCompletionMetadata({ props, view, connectionStore, sqlBehaviorDialect: () => props.dialect, remoteLatencyBudgetMs: 40, maxCompletionTables: 100, onDemandMinPrefix: 2, semanticCompletionEnabled: false }); const startCompletion = vi.fn(() => true); const runtime: Options["runtime"] = { codeMirrorStartCompletion: startCompletion, codeMirrorInsertCompletionText: insertCompletionText, codeMirrorSnippetCompletion: snippetCompletion, codeMirrorCompletionStatus: () => null, imeCompositionActive: false }; diff --git a/apps/desktop/src/components/editor/useQueryEditorCompletion.ts b/apps/desktop/src/components/editor/useQueryEditorCompletion.ts index db41d3ec05..52edf14045 100644 --- a/apps/desktop/src/components/editor/useQueryEditorCompletion.ts +++ b/apps/desktop/src/components/editor/useQueryEditorCompletion.ts @@ -811,6 +811,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions) keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase, functionCase: settingsStore.editorSettings.sqlFormatter.functionCase, autoAliasTables: settingsStore.editorSettings.autoAliasTables, + tableCompletionSchemaQualification: settingsStore.editorSettings.tableCompletionSchemaQualification, quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, }); return buildSqlCompletionResult(items, completionContext, fullDoc, position); @@ -873,6 +874,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions) keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase, functionCase: settingsStore.editorSettings.sqlFormatter.functionCase, autoAliasTables: settingsStore.editorSettings.autoAliasTables, + tableCompletionSchemaQualification: settingsStore.editorSettings.tableCompletionSchemaQualification, quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, }); return buildSqlCompletionResult(items, completionContext, fullDoc, position); @@ -1194,6 +1196,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions) keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase, functionCase: settingsStore.editorSettings.sqlFormatter.functionCase, autoAliasTables: settingsStore.editorSettings.autoAliasTables, + tableCompletionSchemaQualification: settingsStore.editorSettings.tableCompletionSchemaQualification, quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, }); @@ -1655,6 +1658,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions) keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase, functionCase: settingsStore.editorSettings.sqlFormatter.functionCase, autoAliasTables: settingsStore.editorSettings.autoAliasTables, + tableCompletionSchemaQualification: settingsStore.editorSettings.tableCompletionSchemaQualification, quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, }); diff --git a/apps/desktop/src/i18n/__tests__/featureMessages.spec.ts b/apps/desktop/src/i18n/__tests__/featureMessages.spec.ts index 6d9f80f559..155a2d5ab4 100644 --- a/apps/desktop/src/i18n/__tests__/featureMessages.spec.ts +++ b/apps/desktop/src/i18n/__tests__/featureMessages.spec.ts @@ -72,6 +72,7 @@ const FEATURE_MESSAGES: FeatureMessages[] = [ { feature: "sidebar search skip hint", keys: ["sidebar.searchConnectionSkipped"] }, { feature: "PostgreSQL legacy TLS", keys: ["connection.postgresLegacyTls", "connection.postgresLegacyTlsHint"] }, { feature: "settings search sections", keys: ["settings.syncWebDavWebDescription", "settings.performanceSection"] }, + { feature: "SQL table completion schema qualification (#9219)", keys: ["settings.tableCompletionSchemaQualification*"], translated: true }, { feature: "Redis batch expiration", keys: under("redis", ["batchExpiry", "batchExpiryTitle", "batchExpirySelected", "batchExpiryApply", "batchExpirySuccess", "batchExpiryPartial"]) }, { feature: "plugin AI tools and data access", diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 27fcfeae2c..a8be671ccd 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -7740,6 +7740,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "SQL Server üslublu yer tutucuları əvəz etmək üçün dəyər soruş (sessiya dəyişənləri ilə toqquşa bilər).", sqlVariableSyntax_atSet: "Sətirdaxili dəyişənlərin açılması", sqlVariableSyntax_atSetDescription: "İşlətməzdən əvvəl sətirdaxili dəyişən elanlarını aç və dəyərlərini yerinə qoy.", + tableCompletionSchemaQualification: "Cədvəl tamamlamasında sxem ixtisaslaşdırması", + tableCompletionSchemaQualificationDescription: "Tamamlanan cədvəl və görünüş adlarına sxemin nə vaxt əlavə ediləcəyini seçin.", + tableCompletionSchemaQualificationNever: "Heç vaxt", + tableCompletionSchemaQualificationNeverDescription: "Yalnız cədvəl və ya görünüş adını daxil edir.", + tableCompletionSchemaQualificationCollision: "Ad toqquşduqda", + tableCompletionSchemaQualificationCollisionDescription: "Cari avtomatik davranışdan istifadə edir və sxemi yalnız lazım olduqda əlavə edir.", + tableCompletionSchemaQualificationAlways: "Həmişə", + tableCompletionSchemaQualificationAlwaysDescription: "Sxem metadata-da olduqda həmişə əlavə edir.", autoAliasTables: "Cədvəl ləqəblərini avtomatik əlavə et", autoAliasTablesDescription: "FROM və ya JOIN daxilində cədvəl tamamlaması seçilərkən order_items AS oi kimi yaradılmış ləqəb daxil et.", sqlServerSpaceConfirmsCompletion: "Boşluqla tamamlamanı təsdiqlə (SQL Server üslubu)", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 54560667c0..8c51b3e6c0 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -8414,6 +8414,14 @@ export default { sqlVariableSyntax_sqlserverDescription: "Prompt for a value to substitute SQL Server-style placeholders (may collide with session variables).", sqlVariableSyntax_atSet: "Inline variable expansion", sqlVariableSyntax_atSetDescription: "Expand inline variable declarations and substitute their values before running.", + tableCompletionSchemaQualification: "Schema qualification for table completion", + tableCompletionSchemaQualificationDescription: "Choose when completed table and view names include their schema.", + tableCompletionSchemaQualificationNever: "Never", + tableCompletionSchemaQualificationNeverDescription: "Insert only the table or view name.", + tableCompletionSchemaQualificationCollision: "On collision", + tableCompletionSchemaQualificationCollisionDescription: "Use the current automatic behavior and include a schema only when needed.", + tableCompletionSchemaQualificationAlways: "Always", + tableCompletionSchemaQualificationAlwaysDescription: "Include the schema whenever it is available in metadata.", autoAliasTables: "Automatically add table aliases", autoAliasTablesDescription: "When selecting a table completion in FROM or JOIN, insert a generated alias such as order_items AS oi.", sqlServerSpaceConfirmsCompletion: "Space confirms completion (SQL Server style)", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 98707a2f0d..54d9ecb5b2 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -7749,6 +7749,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "Solicita un valor para sustituir los marcadores de estilo SQL Server (pueden entrar en conflicto con variables de sesión).", sqlVariableSyntax_atSet: "Expansión de variables en línea", sqlVariableSyntax_atSetDescription: "Expande las declaraciones de variables en línea y sustituye sus valores antes de ejecutar.", + tableCompletionSchemaQualification: "Calificación de esquema al completar tablas", + tableCompletionSchemaQualificationDescription: "Elige cuándo los nombres de tablas y vistas completados incluyen su esquema.", + tableCompletionSchemaQualificationNever: "Nunca", + tableCompletionSchemaQualificationNeverDescription: "Inserta solo el nombre de la tabla o vista.", + tableCompletionSchemaQualificationCollision: "Si hay conflicto", + tableCompletionSchemaQualificationCollisionDescription: "Usa el comportamiento automático actual e incluye el esquema solo cuando sea necesario.", + tableCompletionSchemaQualificationAlways: "Siempre", + tableCompletionSchemaQualificationAlwaysDescription: "Incluye el esquema siempre que esté disponible en los metadatos.", autoAliasTables: "Agregar alias de tabla automáticamente", autoAliasTablesDescription: "Al seleccionar una tabla en FROM o JOIN, inserta un alias generado como order_items AS oi.", sqlServerSpaceConfirmsCompletion: "Confirmar la sugerencia con espacio (estilo SQL Server)", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 358e54326d..c53178a939 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -7674,6 +7674,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "Richiede un valore per sostituire i segnaposto in stile SQL Server (possono entrare in conflitto con le variabili di sessione).", sqlVariableSyntax_atSet: "Espansione di variabili inline", sqlVariableSyntax_atSetDescription: "Espande le dichiarazioni di variabili inline e ne sostituisce i valori prima dell'esecuzione.", + tableCompletionSchemaQualification: "Qualificazione dello schema nel completamento tabelle", + tableCompletionSchemaQualificationDescription: "Scegli quando i nomi completati di tabelle e viste includono lo schema.", + tableCompletionSchemaQualificationNever: "Mai", + tableCompletionSchemaQualificationNeverDescription: "Inserisce solo il nome della tabella o della vista.", + tableCompletionSchemaQualificationCollision: "In caso di conflitto", + tableCompletionSchemaQualificationCollisionDescription: "Usa il comportamento automatico attuale e include lo schema solo quando necessario.", + tableCompletionSchemaQualificationAlways: "Sempre", + tableCompletionSchemaQualificationAlwaysDescription: "Include lo schema ogni volta che è disponibile nei metadati.", autoAliasTables: "Aggiungi alias tabella automaticamente", autoAliasTablesDescription: "Quando scegli una tabella in FROM o JOIN, inserisce un alias generato come order_items AS oi.", sqlServerSpaceConfirmsCompletion: "Conferma il completamento con lo spazio (stile SQL Server)", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index e1ec59bb54..abc27f0cc9 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -7679,6 +7679,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "SQL Server 形式プレースホルダーを置き換える値の入力を求めます(セッション変数と競合する場合があります)。", sqlVariableSyntax_atSet: "インライン変数展開", sqlVariableSyntax_atSetDescription: "実行前にインライン変数宣言を展開し、その値をインライン化します。", + tableCompletionSchemaQualification: "テーブル補完のスキーマ修飾", + tableCompletionSchemaQualificationDescription: "補完したテーブル名とビュー名にスキーマを含めるタイミングを選択します。", + tableCompletionSchemaQualificationNever: "しない", + tableCompletionSchemaQualificationNeverDescription: "テーブル名またはビュー名だけを挿入します。", + tableCompletionSchemaQualificationCollision: "名前が重複する場合", + tableCompletionSchemaQualificationCollisionDescription: "現在の自動動作を使用し、必要な場合だけスキーマを挿入します。", + tableCompletionSchemaQualificationAlways: "常に", + tableCompletionSchemaQualificationAlwaysDescription: "メタデータにスキーマがある場合は常に挿入します。", autoAliasTables: "テーブル別名を自動追加", autoAliasTablesDescription: "FROM または JOIN でテーブル補完を選択すると、order_items AS oi のような生成済み別名を挿入します。", sqlServerSpaceConfirmsCompletion: "スペースで補完を確定(SQL Server 方式)", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 867be5871a..8d289d871e 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -7454,6 +7454,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "SQL Server 스타일 자리표시자를 치환할 값을 입력하라는 메시지를 표시합니다 (세션 변수와 충돌할 수 있음).", sqlVariableSyntax_atSet: "인라인 변수 확장", sqlVariableSyntax_atSetDescription: "실행 전에 인라인 변수 선언을 확장하고 값을 치환합니다.", + tableCompletionSchemaQualification: "테이블 완성 스키마 한정", + tableCompletionSchemaQualificationDescription: "완성된 테이블 및 뷰 이름에 스키마를 포함할 시점을 선택합니다.", + tableCompletionSchemaQualificationNever: "안 함", + tableCompletionSchemaQualificationNeverDescription: "테이블 또는 뷰 이름만 삽입합니다.", + tableCompletionSchemaQualificationCollision: "이름 충돌 시", + tableCompletionSchemaQualificationCollisionDescription: "현재 자동 동작을 사용하여 필요한 경우에만 스키마를 삽입합니다.", + tableCompletionSchemaQualificationAlways: "항상", + tableCompletionSchemaQualificationAlwaysDescription: "메타데이터에 스키마가 있으면 항상 삽입합니다.", autoAliasTables: "자동으로 테이블 별칭 추가", autoAliasTablesDescription: "FROM이나 JOIN에서 테이블 완성을 선택할 때 order_items AS oi 같은 생성된 별칭을 삽입합니다.", sqlServerSpaceConfirmsCompletion: "공백으로 완성 확정(SQL Server 방식)", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 5558122e22..a7aa697bde 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -7675,6 +7675,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "Solicita um valor para substituir os espaços reservados de estilo SQL Server (podem colidir com variáveis de sessão).", sqlVariableSyntax_atSet: "Expansão de variáveis inline", sqlVariableSyntax_atSetDescription: "Expande as declarações de variáveis inline e substitui seus valores antes de executar.", + tableCompletionSchemaQualification: "Qualificação de schema no autocompletar de tabelas", + tableCompletionSchemaQualificationDescription: "Escolha quando nomes de tabelas e views autocompletados incluem o schema.", + tableCompletionSchemaQualificationNever: "Nunca", + tableCompletionSchemaQualificationNeverDescription: "Insere apenas o nome da tabela ou view.", + tableCompletionSchemaQualificationCollision: "Em caso de conflito", + tableCompletionSchemaQualificationCollisionDescription: "Usa o comportamento automático atual e inclui o schema apenas quando necessário.", + tableCompletionSchemaQualificationAlways: "Sempre", + tableCompletionSchemaQualificationAlwaysDescription: "Inclui o schema sempre que ele estiver disponível nos metadados.", autoAliasTables: "Adicionar alias de tabela automaticamente", autoAliasTablesDescription: "Ao escolher uma tabela em FROM ou JOIN, insere um alias gerado como order_items AS oi.", sqlServerSpaceConfirmsCompletion: "Confirmar a conclusão com espaço (estilo SQL Server)", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index f287a8cb81..888f9f87f6 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -8934,6 +8934,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "Запрашивать значение для подстановки заполнителей в стиле SQL Server (может конфликтовать с переменными сеанса).", sqlVariableSyntax_atSet: "Раскрытие встроенных переменных", sqlVariableSyntax_atSetDescription: "Раскрывать встроенные объявления переменных и подставлять их значения перед выполнением.", + tableCompletionSchemaQualification: "Указание схемы при автодополнении таблиц", + tableCompletionSchemaQualificationDescription: "Выберите, когда дополненные имена таблиц и представлений должны включать схему.", + tableCompletionSchemaQualificationNever: "Никогда", + tableCompletionSchemaQualificationNeverDescription: "Вставлять только имя таблицы или представления.", + tableCompletionSchemaQualificationCollision: "При совпадении имён", + tableCompletionSchemaQualificationCollisionDescription: "Использовать текущее автоматическое поведение и добавлять схему только при необходимости.", + tableCompletionSchemaQualificationAlways: "Всегда", + tableCompletionSchemaQualificationAlwaysDescription: "Добавлять схему всегда, когда она доступна в метаданных.", autoAliasTables: "Автоматически добавлять псевдонимы таблиц", autoAliasTablesDescription: "При выборе автодополнения таблицы в FROM или JOIN вставлять сгенерированный псевдоним, например order_items AS oi.", sqlServerSpaceConfirmsCompletion: "Пробел подтверждает автодополнение (стиль SQL Server)", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index b4313186a3..8386258d12 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -7636,6 +7636,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "SQL Server biçemli yer tutucuları değiştirmek için değer sor (oturum değişkenleriyle çakışabilir).", sqlVariableSyntax_atSet: "Satır içi değişken genişletme", sqlVariableSyntax_atSetDescription: "Satır içi değişken bildirimlerini genişlet ve çalıştırmadan önce değerlerini yerine koy.", + tableCompletionSchemaQualification: "Tablo tamamlamasında şema nitelemesi", + tableCompletionSchemaQualificationDescription: "Tamamlanan tablo ve görünüm adlarının ne zaman şema içereceğini seçin.", + tableCompletionSchemaQualificationNever: "Hiçbir zaman", + tableCompletionSchemaQualificationNeverDescription: "Yalnızca tablo veya görünüm adını ekler.", + tableCompletionSchemaQualificationCollision: "Ad çakışmasında", + tableCompletionSchemaQualificationCollisionDescription: "Geçerli otomatik davranışı kullanır ve şemayı yalnızca gerektiğinde ekler.", + tableCompletionSchemaQualificationAlways: "Her zaman", + tableCompletionSchemaQualificationAlwaysDescription: "Şema meta verilerde bulunduğunda her zaman ekler.", autoAliasTables: "Tablo takma adlarını otomatik ekle", autoAliasTablesDescription: "FROM veya JOIN içinde bir tablo tamamlaması seçildiğinde order_items AS oi gibi üretilmiş bir takma ad ekle.", sqlServerSpaceConfirmsCompletion: "Boşlukla tamamlamayı onayla (SQL Server tarzı)", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index bb3b884c83..1b0d02a331 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -8376,6 +8376,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "为 SQL Server 风格占位符提示输入替换值(可能与会话变量冲突)。", sqlVariableSyntax_atSet: "内联变量展开", sqlVariableSyntax_atSetDescription: "执行前展开内联变量声明并就地代入其值。", + tableCompletionSchemaQualification: "SQL 表补全的模式限定", + tableCompletionSchemaQualificationDescription: "选择表和视图补全插入时是否带模式名。", + tableCompletionSchemaQualificationNever: "从不", + tableCompletionSchemaQualificationNeverDescription: "仅插入表名或视图名。", + tableCompletionSchemaQualificationCollision: "同名冲突时", + tableCompletionSchemaQualificationCollisionDescription: "保持当前自动行为,仅在需要时插入模式名。", + tableCompletionSchemaQualificationAlways: "始终", + tableCompletionSchemaQualificationAlwaysDescription: "元数据提供模式名时始终插入。", autoAliasTables: "自动添加表别名", autoAliasTablesDescription: "补全 FROM 或 JOIN 表名时自动添加别名。", sqlServerSpaceConfirmsCompletion: "空格确认补全(SQL Server 习惯)", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index c3b33ffcaf..59d7c60fa3 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -6983,6 +6983,14 @@ export default withEnglishFallback({ sqlVariableSyntax_sqlserverDescription: "為 SQL Server 風格佔位符提示輸入替換值(可能與工作階段變數衝突)。", sqlVariableSyntax_atSet: "內聯變數展開", sqlVariableSyntax_atSetDescription: "執行前展開內聯變數宣告並就地代入其值。", + tableCompletionSchemaQualification: "SQL 資料表補全的結構描述限定", + tableCompletionSchemaQualificationDescription: "選擇資料表和檢視補全插入時是否帶結構描述名稱。", + tableCompletionSchemaQualificationNever: "永不", + tableCompletionSchemaQualificationNeverDescription: "僅插入資料表或檢視名稱。", + tableCompletionSchemaQualificationCollision: "同名衝突時", + tableCompletionSchemaQualificationCollisionDescription: "保持目前自動行為,僅在需要時插入結構描述名稱。", + tableCompletionSchemaQualificationAlways: "一律", + tableCompletionSchemaQualificationAlwaysDescription: "中繼資料提供結構描述名稱時一律插入。", autoAliasTables: "自動加入資料表別名", autoAliasTablesDescription: "在 FROM 或 JOIN 中選擇資料表補全時,自動插入類似 order_items AS oi 的資料表別名。", sqlServerSpaceConfirmsCompletion: "空格確認補全(SQL Server 習慣)", diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts index c267556bdb..e801f8096d 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -30,6 +30,7 @@ function semanticCompletion(markedSql: string, input: Partial { expect(items.findIndex((item) => item.label === "ORDERS_10K")).toBeLessThan(items.findIndex((item) => item.label === "TABLE")); }); - it("qualifies same-name PostgreSQL tables from different schemas", () => { + it("uses on-collision qualification by default for same-name PostgreSQL tables", () => { const sql = "SELECT * FROM shared"; const items = buildSqlCompletionItems(sql, sql.length, { databaseType: "postgres", @@ -1381,6 +1381,86 @@ describe("sqlCompletion scoped metadata ranking", () => { expect(items.map((item) => item.apply).sort()).toEqual(["public.shared", "reporting.shared"]); }); + it("never qualifies table names while keeping colliding candidates distinct", () => { + const sql = "SELECT * FROM shared"; + const items = buildSqlCompletionItems(sql, sql.length, { + databaseType: "postgres", + dialect: "postgres", + tables: [ + { name: "shared", schema: "public", type: "table", applyName: "public.shared" }, + { name: "shared", schema: "reporting", type: "view", applyName: "reporting.shared" }, + ], + columnsByTable: new Map(), + tableCompletionSchemaQualification: "never", + }).filter((item) => item.type === "table"); + + expect(items).toHaveLength(2); + expect(items.map((item) => item.apply)).toEqual(["shared", "shared"]); + expect(items.map((item) => item.detail).sort()).toEqual(["public.shared", "reporting.shared"]); + }); + + it("always qualifies tables and views when schema metadata is available", () => { + const sql = "SELECT * FROM "; + const items = buildSqlCompletionItems(sql, sql.length, { + databaseType: "postgres", + dialect: "postgres", + tables: [ + { name: "Order Details", schema: "Sales Data", type: "view" }, + { name: "scratch", type: "table" }, + ], + columnsByTable: new Map(), + tableCompletionSchemaQualification: "always", + }).filter((item) => item.type === "table"); + + expect(items.find((item) => item.label === "Order Details")?.apply).toBe('"Sales Data"."Order Details"'); + expect(items.find((item) => item.label === "scratch")?.apply).toBe("scratch"); + }); + + it.each([ + ["never", "customers"], + ["always", "sales.customers"], + ] as const)("applies the %s policy to foreign-key table suggestions", (tableCompletionSchemaQualification, expectedApply) => { + const sql = "SELECT * FROM sales.orders o JOIN cus"; + const items = buildSqlCompletionItems(sql, sql.length, { + databaseType: "postgres", + dialect: "postgres", + tables: [ + { name: "orders", schema: "sales", type: "table" }, + { name: "customers", schema: "sales", type: "table" }, + ], + columnsByTable: new Map(), + foreignKeysByTable: new Map([["sales.orders", [{ name: "orders_customer_id_fkey", column: "customer_id", ref_table: "customers", ref_schema: "sales", ref_column: "id" }]]]), + tableCompletionSchemaQualification, + }); + const customers = items.filter((item) => item.type === "table" && item.label === "customers"); + + expect(customers).toHaveLength(1); + expect(customers[0]?.apply).toBe(expectedApply); + expect(customers[0]?.detail).toContain("related by"); + }); + + it("does not repeat an already typed quoted schema or SQL Server database/schema qualifier", () => { + const postgresSql = 'SELECT * FROM "Sales Data".Ord'; + const postgresItems = buildSqlCompletionItems(postgresSql, postgresSql.length, { + databaseType: "postgres", + dialect: "postgres", + tables: [{ name: "Order Details", schema: "Sales Data", type: "table" }], + columnsByTable: new Map(), + tableCompletionSchemaQualification: "always", + }); + const sqlServerSql = "SELECT * FROM Reporting.dbo.Ord"; + const sqlServerItems = buildSqlCompletionItems(sqlServerSql, sqlServerSql.length, { + databaseType: "sqlserver", + dialect: "sqlserver", + tables: [{ name: "Order Details", database: "Reporting", schema: "dbo", type: "table" }], + columnsByTable: new Map(), + tableCompletionSchemaQualification: "always", + }); + + expect(postgresItems.find((item) => item.label === "Order Details")?.apply).toBe('"Order Details"'); + expect(sqlServerItems.find((item) => item.label === "Order Details")?.apply).toBe("[Order Details]"); + }); + it("qualifies same-name tables for generic metadata providers", () => { const sql = "SELECT * FROM orders"; const items = buildSqlCompletionItems(sql, sql.length, { diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index 95ba0681c2..247c3dd45a 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -136,6 +136,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode"); }); + it("includes tableCompletionSchemaQualification", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("tableCompletionSchemaQualification"); + }); + it("includes tableHoverLookupMode", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("tableHoverLookupMode"); }); @@ -231,6 +235,11 @@ describe("editorSettingsDraftFromSettings", () => { const draft = editorSettingsDraftFromSettings(makeSettings({ completionTriggerMode: "always" as unknown } as Partial)); expect(draft.completionTriggerMode).toBe("positional"); }); + + it("maps and normalizes tableCompletionSchemaQualification", () => { + expect(editorSettingsDraftFromSettings(makeSettings({ tableCompletionSchemaQualification: "always" })).tableCompletionSchemaQualification).toBe("always"); + expect(editorSettingsDraftFromSettings(makeSettings({ tableCompletionSchemaQualification: "invalid" } as any)).tableCompletionSchemaQualification).toBe("collision"); + }); }); describe("normalizeTableOpenPageSizeDraft", () => { @@ -406,6 +415,16 @@ describe("editorSettingsPatchFromDraft", () => { const patch = editorSettingsPatchFromDraft(draft, base); expect(patch.completionTriggerMode).toBeUndefined(); }); + + it("includes tableCompletionSchemaQualification only when changed", () => { + const settings = makeSettings({ tableCompletionSchemaQualification: "collision" }); + const draft = editorSettingsDraftFromSettings(settings); + const base = editorSettingsDraftFromSettings(settings); + + expect(editorSettingsPatchFromDraft(draft, base).tableCompletionSchemaQualification).toBeUndefined(); + draft.tableCompletionSchemaQualification = "never"; + expect(editorSettingsPatchFromDraft(draft, base).tableCompletionSchemaQualification).toBe("never"); + }); }); describe("EDITOR_SETTINGS_DRAFT_KEYS - tabLayout", () => { diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index a935949271..e33ef957f0 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -295,6 +295,7 @@ describe("settings search", () => { { titleKey: "settings.sidebarObjectInfoMode", category: "navigation", targetId: "navigation" }, { titleKey: "settings.insertSpaceAfterCompletion", category: "editor", targetId: "editor" }, { titleKey: "settings.completionTriggerMode", category: "editor", targetId: "editor" }, + { titleKey: "settings.tableCompletionSchemaQualification", category: "editor", targetId: "editor" }, { titleKey: "settings.autoAliasTables", category: "editor", targetId: "editor" }, { titleKey: "settings.clickTableNavigationTarget", category: "navigation", targetId: "navigation" }, { titleKey: "settings.prefillNewQueryWithSelect", category: "navigation", targetId: "navigation" }, diff --git a/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts index 34e5145dde..a150908fff 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsTransfer.spec.ts @@ -68,6 +68,18 @@ describe("settingsTransfer", () => { expect(result.error.detail).toContain("updateDownloadSource"); }); + it("round-trips table completion schema qualification as an editor setting", () => { + const result = parseSettingsTransferFile(fileWith({ tableCompletionSchemaQualification: "always" })); + expect(result.ok).toBe(true); + if (!result.ok) return; + expect(result.value.editorSettings.tableCompletionSchemaQualification).toBe("always"); + expect(result.value.categories).toEqual(["editor"]); + + const invalid = parseSettingsTransferFile(fileWith({ tableCompletionSchemaQualification: "sometimes" })); + expect(invalid.ok).toBe(false); + if (!invalid.ok) expect(invalid.error.detail).toContain("tableCompletionSchemaQualification"); + }); + it("rejects out-of-range numbers that the normalizer clamps", () => { const result = parseSettingsTransferFile(fileWith({ sidebarIndent: 99999 })); expect(result.ok).toBe(false); diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 35c6e79b45..ce991b14f9 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -4,6 +4,7 @@ import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize"; import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit"; import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen"; import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy"; +import { normalizeSqlTableCompletionSchemaQualification } from "@/lib/sql/sqlCompletionSchemaQualification"; import { normalizeTableHoverLookupMode } from "@/lib/editor/hoverTableLookup"; import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates"; @@ -25,6 +26,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "showCurrentStatementFrame", "showInsertValueHints", "autoAliasTables", + "tableCompletionSchemaQualification", "insertSpaceAfterCompletion", "sqlServerSpaceConfirmsCompletion", "sortCompletionColumnsAlphabetically", @@ -158,6 +160,7 @@ function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unkn if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value); if (key === "externalSqlEditorMaxMb") return normalizeExternalSqlEditorMaxMb(value); if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value); + if (key === "tableCompletionSchemaQualification") return normalizeSqlTableCompletionSchemaQualification(value); if (key === "tableHoverLookupMode") return normalizeTableHoverLookupMode(value); if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value); if (key === "backgroundImage") return normalizeBackgroundImageSettings(value); diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 8a22456f71..f9b6f10774 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -145,6 +145,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-completion-spacing", category: "editor", titleKey: "settings.insertSpaceAfterCompletion", descriptionKey: "settings.insertSpaceAfterCompletionDescription", targetId: "editor" }, { id: "editor-sqlserver-space-completion", category: "editor", titleKey: "settings.sqlServerSpaceConfirmsCompletion", descriptionKey: "settings.sqlServerSpaceConfirmsCompletionDescription", targetId: "editor", visible: sqlServerSpaceCompletionVisible }, { id: "editor-completion-trigger-mode", category: "editor", titleKey: "settings.completionTriggerMode", descriptionKey: "settings.completionTriggerModeDescription", targetId: "editor" }, + { id: "editor-table-completion-schema-qualification", category: "editor", titleKey: "settings.tableCompletionSchemaQualification", descriptionKey: "settings.tableCompletionSchemaQualificationDescription", targetId: "editor" }, { id: "editor-auto-alias", category: "editor", titleKey: "settings.autoAliasTables", descriptionKey: "settings.autoAliasTablesDescription", targetId: "editor" }, { id: "editor-unsaved-close", category: "editor", titleKey: "settings.confirmUnsavedSqlClose", descriptionKey: "settings.confirmUnsavedSqlCloseDescription", targetId: "editor" }, { id: "editor-app-close-unsaved-tabs", category: "editor", titleKey: "settings.appCloseUnsavedTabsMode", descriptionKey: "settings.appCloseUnsavedTabsModeDescription", targetId: "editor" }, diff --git a/apps/desktop/src/lib/settings/settingsTransfer.ts b/apps/desktop/src/lib/settings/settingsTransfer.ts index 629092739f..f8e1ee5925 100644 --- a/apps/desktop/src/lib/settings/settingsTransfer.ts +++ b/apps/desktop/src/lib/settings/settingsTransfer.ts @@ -83,6 +83,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record>, + schemaQualificationMode: SqlTableCompletionSchemaQualification, ): { ambiguousTableName: boolean; schemaQualification: boolean; defaultApplyName: string } { const oracleSchemaQualification = isOracleCompletionDatabase(databaseType) && table.schema && table.schema.toUpperCase() !== "PUBLIC" && (!currentSchema || normalizeIdentifierPart(table.schema) !== normalizeIdentifierPart(currentSchema)); // A bare table name is ambiguous when metadata contains the same name in multiple schemas. - // Keep Oracle's current-schema behavior, but qualify the generic/PostgreSQL/SQL Server paths. + // Collision mode keeps Oracle's current-schema behavior and qualifies the + // generic/PostgreSQL/SQL Server paths only when metadata is ambiguous. const ambiguousTableName = !isOracleCompletionDatabase(databaseType) && (schemasByTableName.get(normalizeIdentifierPart(table.name))?.size ?? 0) > 1; - const schemaQualification = !!table.schema && (oracleSchemaQualification || ambiguousTableName); + const schemaQualification = !!table.schema && (schemaQualificationMode === "always" || (schemaQualificationMode === "collision" && (oracleSchemaQualification || ambiguousTableName))); const defaultApplyName = schemaQualification ? `${quoteCompletionApplyIdentifier(table.schema!, dialect)}.${quoteCompletionApplyIdentifier(table.name, dialect)}` : quoteCompletionApplyIdentifier(table.name, dialect); return { ambiguousTableName, schemaQualification, defaultApplyName }; } function buildTableItems( - context: Pick, + context: Pick, tables: SqlCompletionTable[], dialect?: SqlCompletionApplyDialect, autoAliasTables = false, referencedTables: SqlCompletionReferencedTable[] = [], databaseType?: DatabaseType, currentSchema?: string, + schemaQualificationMode: SqlTableCompletionSchemaQualification = "collision", ): SqlCompletionItem[] { const { prefix } = context; - const qualifierSchema = context.qualifier?.split(".").filter(Boolean).pop(); + const qualifierSchema = context.qualifierParts?.[context.qualifierParts.length - 1] ?? context.qualifier?.split(".").filter(Boolean).pop(); const existingAliases = new Set(referencedTables.map((ref) => ref.alias?.toLowerCase()).filter((alias): alias is string => !!alias)); const matchingTables = tables.filter((table) => matchesPrefix(table.name, prefix)); // Ambiguity is decided among prefix-matching tables only, matching prior behavior. @@ -3960,11 +3967,19 @@ function buildTableItems( return matchingTables .map((table) => { const qualifiedByContext = !!qualifierSchema && !!table.schema && normalizeIdentifierPart(qualifierSchema) === normalizeIdentifierPart(table.schema); - const { ambiguousTableName, defaultApplyName } = resolveTableSchemaQualification(table, dialect, databaseType, currentSchema, schemasByTableName); + const { ambiguousTableName, defaultApplyName } = resolveTableSchemaQualification(table, dialect, databaseType, currentSchema, schemasByTableName, schemaQualificationMode); + const unqualifiedApplyName = quoteCompletionApplyIdentifier(table.name, dialect); const rawSuppliedApplyName = table.applyName?.trim(); const suppliedApplyName = dialect === "sqlserver" && rawSuppliedApplyName ? quoteCompletionApplyName(rawSuppliedApplyName, dialect) : rawSuppliedApplyName; const suppliedApplyNameIsQualified = suppliedApplyName?.includes(".") === true; - const applyName = qualifiedByContext ? quoteCompletionApplyIdentifier(table.name, dialect) : ambiguousTableName && !!table.schema && (!suppliedApplyName || !suppliedApplyNameIsQualified) ? defaultApplyName : (suppliedApplyName ?? defaultApplyName); + const applyName = + qualifiedByContext || schemaQualificationMode === "never" + ? unqualifiedApplyName + : schemaQualificationMode === "always" && table.schema + ? defaultApplyName + : ambiguousTableName && table.schema && (!suppliedApplyName || !suppliedApplyNameIsQualified) + ? defaultApplyName + : (suppliedApplyName ?? defaultApplyName); const alias = autoAliasTables ? generateTableCompletionAlias(table.name, existingAliases) : ""; const schemaDetail = ambiguousTableName && table.schema ? `${table.schema}.${table.name}` : undefined; const detail = table.detail && schemaDetail ? `${schemaDetail} ${table.detail}` : (table.detail ?? schemaDetail ?? (table.type === "table" ? undefined : table.type)); @@ -3974,7 +3989,7 @@ function buildTableItems( detail, apply: formatTableAliasApply(applyName, alias), boost: computeBoost(table.name, prefix) + 1000 + (table.boost ?? 0), - dedupeKey: table.applyName || ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined, + dedupeKey: schemaQualificationMode === "never" && table.schema ? `${quoteCompletionApplyIdentifier(table.schema, dialect)}.${unqualifiedApplyName}` : table.applyName || ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined, }; }) .sort(compareCompletionItems) @@ -3989,6 +4004,7 @@ function buildForeignKeyRelatedTableItems( autoAliasTables = false, databaseType?: DatabaseType, currentSchema?: string, + schemaQualificationMode: SqlTableCompletionSchemaQualification = "collision", ): SqlCompletionItem[] { if (!foreignKeysByTable || context.referencedTables.length === 0) return []; const candidates = new Map(); @@ -4016,11 +4032,14 @@ function buildForeignKeyRelatedTableItems( // `schema.table` exactly when regular candidates would. Built from the same // prefix-matching table set buildTableItems uses, keeping the two in lockstep. const schemasByTableName = collectSchemasByTableName(tables.filter((table) => matchesPrefix(table.name, context.prefix))); + const qualifierSchema = context.qualifierParts?.[context.qualifierParts.length - 1] ?? context.qualifier?.split(".").filter(Boolean).pop(); return [...candidates.values()] .map(({ table, detail }) => { - const { ambiguousTableName, defaultApplyName } = resolveTableSchemaQualification(table, dialect, databaseType, currentSchema, schemasByTableName); - const applyName = defaultApplyName; + const qualifiedByContext = !!qualifierSchema && !!table.schema && normalizeIdentifierPart(qualifierSchema) === normalizeIdentifierPart(table.schema); + const { ambiguousTableName, defaultApplyName } = resolveTableSchemaQualification(table, dialect, databaseType, currentSchema, schemasByTableName, schemaQualificationMode); + const unqualifiedApplyName = quoteCompletionApplyIdentifier(table.name, dialect); + const applyName = qualifiedByContext || schemaQualificationMode === "never" ? unqualifiedApplyName : defaultApplyName; const alias = autoAliasTables ? generateTableCompletionAlias(table.name, existingAliases) : ""; return { label: table.name, @@ -4030,7 +4049,7 @@ function buildForeignKeyRelatedTableItems( boost: computeBoost(table.name, context.prefix) + 3600, // Mirror buildTableItems' dedupeKey so an FK candidate and the regular // candidate for the same schema-qualified table collapse to one entry. - dedupeKey: ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined, + dedupeKey: schemaQualificationMode === "never" && table.schema ? `${quoteCompletionApplyIdentifier(table.schema, dialect)}.${unqualifiedApplyName}` : ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined, }; }) .sort(compareCompletionItems); diff --git a/apps/desktop/src/lib/sql/sqlCompletionSchemaQualification.ts b/apps/desktop/src/lib/sql/sqlCompletionSchemaQualification.ts new file mode 100644 index 0000000000..0509fc9200 --- /dev/null +++ b/apps/desktop/src/lib/sql/sqlCompletionSchemaQualification.ts @@ -0,0 +1,8 @@ +export type SqlTableCompletionSchemaQualification = "never" | "collision" | "always"; + +export const DEFAULT_SQL_TABLE_COMPLETION_SCHEMA_QUALIFICATION: SqlTableCompletionSchemaQualification = "collision"; + +export function normalizeSqlTableCompletionSchemaQualification(value: unknown): SqlTableCompletionSchemaQualification { + if (value === "never" || value === "collision" || value === "always") return value; + return DEFAULT_SQL_TABLE_COMPLETION_SCHEMA_QUALIFICATION; +} diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts index 6e6f0fa6d2..0bcfeb0b46 100644 --- a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts +++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts @@ -224,6 +224,13 @@ describe("normalizeEditorSettings", () => { expect(normalizeEditorSettings({ autoAliasTables: false }).autoAliasTables).toBe(false); }); + it("defaults table completion schema qualification to collision and preserves valid modes", () => { + expect(normalizeEditorSettings({}).tableCompletionSchemaQualification).toBe("collision"); + expect(normalizeEditorSettings({ tableCompletionSchemaQualification: "never" }).tableCompletionSchemaQualification).toBe("never"); + expect(normalizeEditorSettings({ tableCompletionSchemaQualification: "always" }).tableCompletionSchemaQualification).toBe("always"); + expect(normalizeEditorSettings({ tableCompletionSchemaQualification: "invalid" } as any).tableCompletionSchemaQualification).toBe("collision"); + }); + it("enables a trailing space after completion by default and preserves the opt-out", () => { expect(normalizeEditorSettings({}).insertSpaceAfterCompletion).toBe(true); expect(normalizeEditorSettings({ insertSpaceAfterCompletion: false }).insertSpaceAfterCompletion).toBe(false); @@ -1134,6 +1141,22 @@ describe("settingsStore persisted settings initialization", () => { expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ dataGridKeepFilterEditorExpanded: false })); }); + it("persists table completion schema qualification updates", async () => { + const loadEditorSettings = vi.fn().mockResolvedValue({}); + const saveEditorSettings = vi.fn().mockResolvedValue(undefined); + vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings })); + + const { useSettingsStore } = await import("@/stores/settingsStore"); + const store = useSettingsStore(); + await store.initEditorSettings(); + saveEditorSettings.mockClear(); + + await store.updateEditorSettingsAndPersist({ tableCompletionSchemaQualification: "always" }); + + expect(store.editorSettings.tableCompletionSchemaQualification).toBe("always"); + expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ tableCompletionSchemaQualification: "always" })); + }); + it("loads and persists the substitution switch without discarding syntax overrides", async () => { const loadEditorSettings = vi.fn().mockResolvedValue({ sqlVariableSubstitutionEnabled: false, diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index 84f5def8ca..9411267e06 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -33,12 +33,31 @@ import { DEFAULT_TABLE_COLUMN_TEMPLATE_FIELDS, normalizeTableColumnTemplateField import { type DataTabReuseMode, DEFAULT_DATA_TAB_REUSE_MODE, normalizeDataTabReuseMode } from "@/lib/tabs/dataTabReuseMode"; import { normalizeTableHoverLookupMode, type TableHoverLookupMode } from "@/lib/editor/hoverTableLookup"; import { normalizeCompletionTriggerMode, type SqlCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy"; +import { DEFAULT_SQL_TABLE_COMPLETION_SCHEMA_QUALIFICATION, normalizeSqlTableCompletionSchemaQualification, type SqlTableCompletionSchemaQualification } from "@/lib/sql/sqlCompletionSchemaQualification"; import { DEFAULT_CSV_QUOTE_MODE, normalizeCsvQuoteMode, type CsvQuoteMode } from "@/lib/export/csvQuoteMode"; import { configureMetadataRuntimeCache, METADATA_CACHE_DEFAULT_MEMORY_MB, normalizeMetadataCacheMemoryMb } from "@/lib/metadata/metadataRuntimeCache"; import type { AiApiStyle, AiAssistantMode, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiModelEffortPreference, AiProvider, AiReasoningLevel, AiTestConnectionResult } from "@/types/ai"; import type { SqlShortcutAction, SqlSnippet, TableInfoTab } from "@/types/database"; -export type { AiApiStyle, AiAuthMethod, AiChatSelectionState, AiConfig, AiConfigItem, AiConfiguredModel, AiEffortLevel, AiEffortSelection, AiProvider, AiReasoningLevel, AiTestConnectionResult, CsvQuoteMode, DataTabReuseMode, SavedSqlOpenTargetMode, SqlCompletionTriggerMode, TableHoverLookupMode }; +export type { + AiApiStyle, + AiAuthMethod, + AiChatSelectionState, + AiConfig, + AiConfigItem, + AiConfiguredModel, + AiEffortLevel, + AiEffortSelection, + AiProvider, + AiReasoningLevel, + AiTestConnectionResult, + CsvQuoteMode, + DataTabReuseMode, + SavedSqlOpenTargetMode, + SqlCompletionTriggerMode, + SqlTableCompletionSchemaQualification, + TableHoverLookupMode, +}; export interface DesktopSettings { show_tray_icon: boolean; @@ -830,6 +849,7 @@ export interface EditorSettings { showCurrentStatementFrame: boolean; showInsertValueHints: boolean; autoAliasTables: boolean; + tableCompletionSchemaQualification: SqlTableCompletionSchemaQualification; insertSpaceAfterCompletion: boolean; sqlServerSpaceConfirmsCompletion: boolean; sortCompletionColumnsAlphabetically: boolean; @@ -1114,6 +1134,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { showCurrentStatementFrame: true, showInsertValueHints: true, autoAliasTables: true, + tableCompletionSchemaQualification: DEFAULT_SQL_TABLE_COMPLETION_SCHEMA_QUALIFICATION, insertSpaceAfterCompletion: true, sqlServerSpaceConfirmsCompletion: false, sortCompletionColumnsAlphabetically: true, @@ -1676,6 +1697,7 @@ export function normalizeEditorSettings(settings: Partial, exist showCurrentStatementFrame: typeof settings.showCurrentStatementFrame === "boolean" ? settings.showCurrentStatementFrame : DEFAULT_EDITOR_SETTINGS.showCurrentStatementFrame, showInsertValueHints: typeof settings.showInsertValueHints === "boolean" ? settings.showInsertValueHints : DEFAULT_EDITOR_SETTINGS.showInsertValueHints, autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables, + tableCompletionSchemaQualification: normalizeSqlTableCompletionSchemaQualification(settings.tableCompletionSchemaQualification), insertSpaceAfterCompletion: typeof settings.insertSpaceAfterCompletion === "boolean" ? settings.insertSpaceAfterCompletion : DEFAULT_EDITOR_SETTINGS.insertSpaceAfterCompletion, sqlServerSpaceConfirmsCompletion: typeof settings.sqlServerSpaceConfirmsCompletion === "boolean" ? settings.sqlServerSpaceConfirmsCompletion : DEFAULT_EDITOR_SETTINGS.sqlServerSpaceConfirmsCompletion, sortCompletionColumnsAlphabetically: typeof settings.sortCompletionColumnsAlphabetically === "boolean" ? settings.sortCompletionColumnsAlphabetically : DEFAULT_EDITOR_SETTINGS.sortCompletionColumnsAlphabetically, @@ -2505,6 +2527,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.showCurrentStatementFrame !== undefined) editorSettings.value.showCurrentStatementFrame = partial.showCurrentStatementFrame === true; if (partial.showInsertValueHints !== undefined) editorSettings.value.showInsertValueHints = partial.showInsertValueHints === true; if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables; + if (partial.tableCompletionSchemaQualification !== undefined) editorSettings.value.tableCompletionSchemaQualification = normalizeSqlTableCompletionSchemaQualification(partial.tableCompletionSchemaQualification); if (partial.insertSpaceAfterCompletion !== undefined) editorSettings.value.insertSpaceAfterCompletion = partial.insertSpaceAfterCompletion === true; if (partial.sqlServerSpaceConfirmsCompletion !== undefined) editorSettings.value.sqlServerSpaceConfirmsCompletion = partial.sqlServerSpaceConfirmsCompletion === true; if (partial.sortCompletionColumnsAlphabetically !== undefined) editorSettings.value.sortCompletionColumnsAlphabetically = partial.sortCompletionColumnsAlphabetically === true;