From 2eaf7f9914bd4c91eabf452edc6d102f0d20c818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E4=B8=AB=E8=AE=B2=E6=A2=B5?= Date: Mon, 7 Sep 2026 01:11:42 +0800 Subject: [PATCH] feat(editor): add toggle for SQL identifier quoting --- apps/desktop/src/App.vue | 1 + .../editor/EditorSettingsDialog.vue | 15 +++++++ .../sidebar/SidebarTreeRuntimeHost.vue | 9 ++++ apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/ko.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/tr.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../lib/__tests__/sql/newQueryContext.spec.ts | 17 +++++++ .../__tests__/table/tableSqlTemplates.spec.ts | 16 +++++++ .../__tests__/editorSettingsDraft.spec.ts | 4 ++ .../settings/__tests__/settingsSearch.spec.ts | 1 + .../src/lib/settings/editorSettingsDraft.ts | 1 + .../src/lib/settings/settingsSearch.ts | 1 + apps/desktop/src/lib/sql/newQueryContext.ts | 7 +-- apps/desktop/src/lib/table/tableSelectSql.ts | 44 ++++++++++--------- .../src/lib/table/tableSqlTemplates.ts | 3 ++ .../stores/__tests__/settingsStore.spec.ts | 7 +++ apps/desktop/src/stores/settingsStore.ts | 4 ++ 23 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/table/tableSqlTemplates.spec.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 6fbe5a7095..6ff4eb920e 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -2251,6 +2251,7 @@ async function newQuery() { driverProfile: conn.driver_profile, identifierQuote: connectionStore.connectionIdentifierQuote?.(target.connectionId), includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, }); const tabId = queryStore.createTab(conn.id, target.database, undefined, "query", target.schema, initialSql, target.catalog); if (initialSql) { diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index a497fd8ab2..1d818190c6 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -538,6 +538,7 @@ const editDataTabReuseMode = ref(settingsStore.editorSettings. const editOpenDataTabsNextToActive = ref(settingsStore.editorSettings.openDataTabsNextToActive); const editPrefillNewQueryWithSelect = ref(settingsStore.editorSettings.prefillNewQueryWithSelect); const editGenerateSqlIncludeDatabaseName = ref(settingsStore.editorSettings.generateSqlIncludeDatabaseName); +const editGenerateSqlQuoteIdentifiers = ref(settingsStore.editorSettings.generateSqlQuoteIdentifiers); const editFormatSqlOnSqlFileSave = ref(settingsStore.editorSettings.formatSqlOnSqlFileSave); const editShowTableDdlHoverPreview = ref(settingsStore.editorSettings.showTableDdlHoverPreview); const editClickTableNavigationTarget = ref(settingsStore.editorSettings.clickTableNavigationTarget); @@ -690,6 +691,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { openDataTabsNextToActive: editOpenDataTabsNextToActive.value, prefillNewQueryWithSelect: editPrefillNewQueryWithSelect.value, generateSqlIncludeDatabaseName: editGenerateSqlIncludeDatabaseName.value, + generateSqlQuoteIdentifiers: editGenerateSqlQuoteIdentifiers.value, formatSqlOnSqlFileSave: editFormatSqlOnSqlFileSave.value, showTableDdlHoverPreview: editShowTableDdlHoverPreview.value, updateNotificationsEnabled: editUpdateNotificationsEnabled.value, @@ -1094,6 +1096,7 @@ function syncEditorSettingsDraftFromStore() { editOpenDataTabsNextToActive.value = settingsStore.editorSettings.openDataTabsNextToActive; editPrefillNewQueryWithSelect.value = settingsStore.editorSettings.prefillNewQueryWithSelect; editGenerateSqlIncludeDatabaseName.value = settingsStore.editorSettings.generateSqlIncludeDatabaseName; + editGenerateSqlQuoteIdentifiers.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers; editFormatSqlOnSqlFileSave.value = settingsStore.editorSettings.formatSqlOnSqlFileSave; editShowTableDdlHoverPreview.value = settingsStore.editorSettings.showTableDdlHoverPreview; editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget; @@ -1364,6 +1367,7 @@ function resetDefaultsForTab(tab: SettingsCategory) { editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive; editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect; editGenerateSqlIncludeDatabaseName.value = DEFAULT_EDITOR_SETTINGS.generateSqlIncludeDatabaseName; + editGenerateSqlQuoteIdentifiers.value = DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers; editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave; editClickTableNavigationTarget.value = DEFAULT_EDITOR_SETTINGS.clickTableNavigationTarget; editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled; @@ -1502,6 +1506,7 @@ function resetAllDefaults() { editOpenDataTabsNextToActive.value = DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive; editPrefillNewQueryWithSelect.value = DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect; editGenerateSqlIncludeDatabaseName.value = DEFAULT_EDITOR_SETTINGS.generateSqlIncludeDatabaseName; + editGenerateSqlQuoteIdentifiers.value = DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers; editFormatSqlOnSqlFileSave.value = DEFAULT_EDITOR_SETTINGS.formatSqlOnSqlFileSave; editShowTableDdlHoverPreview.value = DEFAULT_EDITOR_SETTINGS.showTableDdlHoverPreview; editUpdateNotificationsEnabled.value = DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled; @@ -5024,6 +5029,16 @@ onUnmounted(() => { +
+
+ +

+ {{ t("settings.generateSqlQuoteIdentifiersDescription") }} +

+
+ +
+
diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index c7171b9075..7a2e3da330 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -1808,6 +1808,7 @@ async function newQuery() { database: node.database, schema: node.schema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: node.label, columns: [], }); @@ -1907,6 +1908,7 @@ async function newSelectTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, }), @@ -1925,6 +1927,7 @@ async function newSelectTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, }); @@ -1949,6 +1952,7 @@ async function newInsertTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, tableType: context.tableType, @@ -1968,6 +1972,7 @@ async function newInsertTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, tableType: context.tableType, @@ -1993,6 +1998,7 @@ async function newUpdateTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, }), @@ -2011,6 +2017,7 @@ async function newUpdateTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, }); @@ -2035,6 +2042,7 @@ async function newDeleteTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, }), @@ -2053,6 +2061,7 @@ async function newDeleteTemplate() { database: context.node.database, schema: context.tableSchema, includeDatabaseName: settingsStore.editorSettings.generateSqlIncludeDatabaseName, + quoteIdentifiers: settingsStore.editorSettings.generateSqlQuoteIdentifiers, tableName: context.node.label, columns: context.columns, }); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 7ccfb6ce35..81a334ac91 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -6971,6 +6971,8 @@ export default { prefillNewQueryWithSelectDescription: "When creating a new query, prefill the editor with SELECT * FROM based on the active table tab or the table selected in the sidebar.", generateSqlIncludeDatabaseName: "Include database name in generated SQL", generateSqlIncludeDatabaseNameDescription: "When supported by the database, generated table SQL uses database.table instead of only the table name.", + generateSqlQuoteIdentifiers: "Quote identifiers in generated SQL", + generateSqlQuoteIdentifiersDescription: "When disabled, new query table references omit dialect-specific quotes. Reserved words, special characters, and case-sensitive names may require quotes to run.", formatSqlOnSqlFileSave: "Format SQL when saving SQL files", formatSqlOnSqlFileSaveDescription: "Automatically format SQL when saving a SQL file, including saved SQL library files and queries saved as .sql files.", clickTableNavigationTarget: "Ctrl+Click Table Opens DDL", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index d700763656..b3316a43d1 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -6532,6 +6532,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "Al crear una nueva consulta, rellena el editor con SELECT * FROM según la pestaña de tabla activa o la tabla seleccionada en la barra lateral.", generateSqlIncludeDatabaseName: "Incluir el nombre de la base de datos en el SQL generado", generateSqlIncludeDatabaseNameDescription: "Cuando la base de datos lo admite, el SQL de tabla generado usa base_de_datos.tabla en lugar de solo el nombre de la tabla.", + generateSqlQuoteIdentifiers: "Citar identificadores en el SQL generado", + generateSqlQuoteIdentifiersDescription: "Al desactivarlo, las referencias de tabla de las consultas nuevas omiten las comillas del dialecto. Las palabras reservadas, los caracteres especiales y los nombres sensibles a mayúsculas pueden necesitar comillas.", formatSqlOnSqlFileSave: "Formatear SQL al guardar archivos SQL", formatSqlOnSqlFileSaveDescription: "Formatea automáticamente el SQL al guardar un archivo SQL, incluidos los archivos guardados en la biblioteca SQL y las consultas guardadas como archivos .sql.", clickTableNavigationTarget: "Ctrl+Clic abre DDL de tabla", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index b447375c29..00a7389944 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -6532,6 +6532,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "Alla creazione di una nuova query, precompila l'editor con SELECT * FROM in base alla scheda tabella attiva o alla tabella selezionata nella barra laterale.", generateSqlIncludeDatabaseName: "Includi il nome del database nell'SQL generato", generateSqlIncludeDatabaseNameDescription: "Quando supportato dal database, l'SQL della tabella generato usa database.tabella anziché solo il nome della tabella.", + generateSqlQuoteIdentifiers: "Racchiudi gli identificatori nell'SQL generato", + generateSqlQuoteIdentifiersDescription: "Se disattivato, i riferimenti alle tabelle nelle nuove query omettono le virgolette del dialetto. Parole riservate, caratteri speciali e nomi sensibili alle maiuscole potrebbero richiederle.", formatSqlOnSqlFileSave: "Formatta SQL durante il salvataggio dei file SQL", formatSqlOnSqlFileSaveDescription: "Formatta automaticamente l'SQL durante il salvataggio di un file SQL, inclusi i file salvati nella libreria SQL e le query salvate come file .sql.", clickTableNavigationTarget: "Ctrl+Clic apre DDL tabella", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 0d9ed1f63f..59b8dbaee8 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -6547,6 +6547,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "新規クエリ作成時、アクティブなテーブルタブまたはサイドバーで選択したテーブルに基づき、エディタに SELECT * FROM <テーブル名> を自動入力します。", generateSqlIncludeDatabaseName: "生成 SQL にデータベース名を含める", generateSqlIncludeDatabaseNameDescription: "データベースが対応している場合、生成するテーブル SQL でテーブル名のみではなく database.table を使用します。", + generateSqlQuoteIdentifiers: "生成 SQL の識別子を引用符で囲む", + generateSqlQuoteIdentifiersDescription: "無効にすると、新規クエリのテーブル参照では方言固有の引用符を省略します。予約語、特殊文字、大文字小文字を区別する名前では引用符が必要な場合があります。", formatSqlOnSqlFileSave: "SQL ファイル保存時に SQL をフォーマット", formatSqlOnSqlFileSaveDescription: "SQL ファイル保存時に SQL を自動フォーマットします。SQL ライブラリに保存されたファイルや、新規クエリを .sql ファイルとして保存する場合も含みます。", clickTableNavigationTarget: "Ctrl+クリックでテーブル構造を開く", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 4251dc8aa2..168f276e52 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -6285,6 +6285,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "새 쿼리를 만들 때 활성 테이블 탭이나 사이드바에서 선택한 테이블을 기반으로 편집기에 SELECT * FROM
을 미리 채웁니다.", generateSqlIncludeDatabaseName: "생성된 SQL에 데이터베이스 이름 포함", generateSqlIncludeDatabaseNameDescription: "데이터베이스가 지원하는 경우 생성된 테이블 SQL에서 테이블 이름만이 아니라 database.table을 사용합니다.", + generateSqlQuoteIdentifiers: "생성된 SQL에서 식별자 인용", + generateSqlQuoteIdentifiersDescription: "끄면 새 쿼리의 테이블 참조에서 데이터베이스 방언별 인용을 생략합니다. 예약어, 특수 문자 또는 대소문자 구분 이름에는 인용이 필요할 수 있습니다.", formatSqlOnSqlFileSave: "SQL 파일 저장 시 SQL 서식 지정", formatSqlOnSqlFileSaveDescription: "SQL 파일을 저장할 때 SQL을 자동으로 서식 지정합니다. SQL 라이브러리에 저장된 파일과 새 쿼리를 .sql 파일로 저장하는 경우도 포함합니다.", sqlVariableSyntax: "SQL 변수 및 자리표시자 치환", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 6306234762..c761619969 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -6534,6 +6534,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "Ao criar uma nova consulta, preenche o editor com SELECT * FROM com base na aba de tabela ativa ou na tabela selecionada na barra lateral.", generateSqlIncludeDatabaseName: "Incluir nome do banco de dados no SQL gerado", generateSqlIncludeDatabaseNameDescription: "Quando compatível com o banco de dados, o SQL de tabela gerado usa banco.tabela em vez de somente o nome da tabela.", + generateSqlQuoteIdentifiers: "Usar aspas em identificadores no SQL gerado", + generateSqlQuoteIdentifiersDescription: "Quando desativado, as referências de tabela em novas consultas omitem as aspas específicas do dialeto. Palavras reservadas, caracteres especiais e nomes sensíveis a maiúsculas podem exigir aspas.", formatSqlOnSqlFileSave: "Formatar SQL ao salvar arquivos SQL", formatSqlOnSqlFileSaveDescription: "Formata automaticamente o SQL ao salvar um arquivo SQL, incluindo arquivos salvos na biblioteca SQL e consultas salvas como arquivos .sql.", clickTableNavigationTarget: "Ctrl+Clique abre DDL da tabela", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 84595eeb5d..a3866cc572 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -6883,6 +6883,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "Yeni bir sorgu oluşturulurken düzenleyiciyi, etkin tablo sekmesine ya da kenar çubuğunda seçili tabloya göre SELECT * FROM ile doldur.", generateSqlIncludeDatabaseName: "Üretilen SQL'e veritabanı adını dâhil et", generateSqlIncludeDatabaseNameDescription: "Veritabanı destekliyorsa üretilen tablo SQL'i yalnızca tablo adı yerine veritabanı.tablo biçimini kullanır.", + generateSqlQuoteIdentifiers: "Üretilen SQL'de tanımlayıcıları tırnakla", + generateSqlQuoteIdentifiersDescription: "Devre dışı bırakıldığında yeni sorgulardaki tablo başvuruları lehçeye özgü tırnakları kullanmaz. Ayrılmış sözcükler, özel karakterler ve büyük/küçük harfe duyarlı adlar tırnak gerektirebilir.", formatSqlOnSqlFileSave: "SQL dosyalarını kaydederken SQL'i biçimlendir", formatSqlOnSqlFileSaveDescription: "Bir SQL dosyası kaydedilirken SQL'i otomatik biçimlendir; kayıtlı SQL kitaplığı dosyaları ve .sql olarak kaydedilen sorgular dâhil.", clickTableNavigationTarget: "Ctrl+Tıklama Tabloda DDL Açar", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 554e008a0d..7b1802ef26 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -6952,6 +6952,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "新建查询时,根据当前激活的数据表标签页或侧边栏选中的表,自动在编辑器中填充 SELECT * FROM <表名>。", generateSqlIncludeDatabaseName: "生成 SQL 时包含数据库名", generateSqlIncludeDatabaseNameDescription: "数据库支持时,生成的表 SQL 将使用“数据库名.表名”,而不只包含表名。", + generateSqlQuoteIdentifiers: "生成 SQL 时为标识符添加引号", + generateSqlQuoteIdentifiersDescription: "关闭后,新建查询中的表引用将不再使用数据库方言引号。保留字、特殊字符或大小写敏感名称可能仍需手动添加引号。", formatSqlOnSqlFileSave: "保存 SQL 文件时格式化 SQL", formatSqlOnSqlFileSaveDescription: "保存 SQL 文件时自动格式化 SQL,包括通过 SQL 库保存的文件以及将新查询保存为 .sql 文件等场景。", clickTableNavigationTarget: "Ctrl+点击表名打开 DDL", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 663d19fd2e..423ab7b30a 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -5845,6 +5845,8 @@ export default withEnglishFallback({ prefillNewQueryWithSelectDescription: "新建查詢時,根據目前啟用的資料表分頁或側邊欄選取的資料表,自動在編輯器中填入 SELECT * FROM <資料表名稱>。", generateSqlIncludeDatabaseName: "產生 SQL 時包含資料庫名稱", generateSqlIncludeDatabaseNameDescription: "資料庫支援時,產生的資料表 SQL 會使用「資料庫名稱.資料表名稱」,而非只包含資料表名稱。", + generateSqlQuoteIdentifiers: "產生 SQL 時為識別碼加上引號", + generateSqlQuoteIdentifiersDescription: "關閉後,新建查詢中的資料表參考不再使用資料庫方言引號。保留字、特殊字元或大小寫敏感名稱可能仍需手動加上引號。", formatSqlOnSqlFileSave: "儲存 SQL 檔案時格式化 SQL", formatSqlOnSqlFileSaveDescription: "儲存 SQL 檔案時自動格式化 SQL,包括透過 SQL 庫儲存的檔案,以及將新查詢儲存為 .sql 檔案等場景。", clickTableNavigationTarget: "Ctrl+點擊表名開啟 DDL", diff --git a/apps/desktop/src/lib/__tests__/sql/newQueryContext.spec.ts b/apps/desktop/src/lib/__tests__/sql/newQueryContext.spec.ts index fef4782f1c..50cc793279 100644 --- a/apps/desktop/src/lib/__tests__/sql/newQueryContext.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/newQueryContext.spec.ts @@ -208,6 +208,10 @@ describe("buildSelectAllSql", () => { expect(buildSelectAllSql("postgres", { schema: "public", tableName: "users" })).toBe('SELECT * FROM "public"."users"'); }); + it("can omit identifier quotes while preserving schema qualification", () => { + expect(buildSelectAllSql("postgres", { schema: "public", tableName: "users" }, undefined, undefined, false, false)).toBe("SELECT * FROM public.users"); + }); + it("preserves the Phoenix schema for new-query prefill", () => { expect(buildSelectAllSql("jdbc", { schema: "APP", tableName: "USERS" }, '"', "phoenix")).toBe('SELECT * FROM "APP"."USERS"'); }); @@ -280,6 +284,19 @@ describe("resolveNewQueryInitialSql", () => { ).toBe('SELECT * FROM "public"."users"'); }); + it("omits identifier quotes when configured for new-query prefill", () => { + expect( + resolveNewQueryInitialSql({ + activeTab: dataTab(), + prefillEnabled: true, + targetConnectionId: "conn-1", + targetDatabase: "app_db", + databaseType: "postgres", + quoteIdentifiers: false, + }), + ).toBe("SELECT * FROM public.users"); + }); + it("passes the Phoenix driver profile into the initial SQL builder", () => { expect( resolveNewQueryInitialSql({ diff --git a/apps/desktop/src/lib/__tests__/table/tableSqlTemplates.spec.ts b/apps/desktop/src/lib/__tests__/table/tableSqlTemplates.spec.ts new file mode 100644 index 0000000000..0ab64f3c5f --- /dev/null +++ b/apps/desktop/src/lib/__tests__/table/tableSqlTemplates.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import { buildTableSelectTemplate } from "@/lib/table/tableSqlTemplates"; + +describe("buildTableSelectTemplate", () => { + it("can omit identifier quotes for a new-query table reference", () => { + expect( + buildTableSelectTemplate({ + databaseType: "postgres", + schema: "public", + tableName: "dbx_smoke", + quoteIdentifiers: false, + }), + ).toBe("SELECT *\nFROM public.dbx_smoke;"); + }); +}); diff --git a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts index 1fc358e1b0..2823329ee9 100644 --- a/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/editorSettingsDraft.spec.ts @@ -68,6 +68,10 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataTabReuseMode"); }); + it("includes generated SQL identifier quote preference", () => { + expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("generateSqlQuoteIdentifiers"); + }); + it("includes adjacent data-tab opening", () => { expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("openDataTabsNextToActive"); }); diff --git a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts index 904af27c59..58f4d42cd4 100644 --- a/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts +++ b/apps/desktop/src/lib/settings/__tests__/settingsSearch.spec.ts @@ -240,6 +240,7 @@ describe("settings search", () => { { titleKey: "settings.clickTableNavigationTarget", category: "navigation", targetId: "navigation" }, { titleKey: "settings.prefillNewQueryWithSelect", category: "navigation", targetId: "navigation" }, { titleKey: "settings.generateSqlIncludeDatabaseName", category: "editor", targetId: "editor" }, + { titleKey: "settings.generateSqlQuoteIdentifiers", category: "editor", targetId: "editor" }, { titleKey: "settings.formatSqlOnSqlFileSave", category: "editor", targetId: "editor" }, { titleKey: "settings.showTableDdlHoverPreview", category: "editor", targetId: "editor" }, { titleKey: "settings.sqlFormatterKeywordCase", category: "formatter", targetId: "formatter" }, diff --git a/apps/desktop/src/lib/settings/editorSettingsDraft.ts b/apps/desktop/src/lib/settings/editorSettingsDraft.ts index 7162fe790a..2f621aebf1 100644 --- a/apps/desktop/src/lib/settings/editorSettingsDraft.ts +++ b/apps/desktop/src/lib/settings/editorSettingsDraft.ts @@ -75,6 +75,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [ "openDataTabsNextToActive", "prefillNewQueryWithSelect", "generateSqlIncludeDatabaseName", + "generateSqlQuoteIdentifiers", "formatSqlOnSqlFileSave", "showTableDdlHoverPreview", "updateNotificationsEnabled", diff --git a/apps/desktop/src/lib/settings/settingsSearch.ts b/apps/desktop/src/lib/settings/settingsSearch.ts index 508ce38129..890715c93a 100644 --- a/apps/desktop/src/lib/settings/settingsSearch.ts +++ b/apps/desktop/src/lib/settings/settingsSearch.ts @@ -123,6 +123,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] = { id: "editor-app-close-unsaved-tabs", category: "editor", titleKey: "settings.appCloseUnsavedTabsMode", descriptionKey: "settings.appCloseUnsavedTabsModeDescription", targetId: "editor" }, { id: "editor-prefill-query", category: "navigation", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "navigation" }, { id: "editor-generate-sql-include-database", category: "editor", titleKey: "settings.generateSqlIncludeDatabaseName", descriptionKey: "settings.generateSqlIncludeDatabaseNameDescription", targetId: "editor" }, + { id: "editor-generate-sql-quote-identifiers", category: "editor", titleKey: "settings.generateSqlQuoteIdentifiers", descriptionKey: "settings.generateSqlQuoteIdentifiersDescription", targetId: "editor" }, { id: "editor-format-sql-on-sql-file-save", category: "editor", titleKey: "settings.formatSqlOnSqlFileSave", descriptionKey: "settings.formatSqlOnSqlFileSaveDescription", targetId: "editor" }, { id: "editor-table-ddl-hover-preview", category: "editor", titleKey: "settings.showTableDdlHoverPreview", descriptionKey: "settings.showTableDdlHoverPreviewDescription", targetId: "editor" }, { id: "editor-diagnostics", category: "editor", titleKey: "settings.sqlSemanticDiagnosticsEnabled", descriptionKey: "settings.sqlSemanticDiagnosticsEnabledDescription", targetId: "editor" }, diff --git a/apps/desktop/src/lib/sql/newQueryContext.ts b/apps/desktop/src/lib/sql/newQueryContext.ts index a4a0fbbdb1..9357adf00f 100644 --- a/apps/desktop/src/lib/sql/newQueryContext.ts +++ b/apps/desktop/src/lib/sql/newQueryContext.ts @@ -93,6 +93,7 @@ export interface ResolveNewQueryInitialSqlInput extends ResolveNewQueryTableInpu driverProfile?: string; identifierQuote?: string; includeDatabaseName?: boolean; + quoteIdentifiers?: boolean; } // Database types whose "table" view does not use standard SQL `SELECT * FROM
` @@ -156,9 +157,9 @@ export function resolveNewQueryTable(input: ResolveNewQueryTableInput): NewQuery * full-history scan the moment the user hits Run. VictoriaMetrics * already has its own metric range template above. */ -export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick & Partial>, identifierQuote?: string, driverProfile?: string, includeDatabaseName = false): string { +export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick & Partial>, identifierQuote?: string, driverProfile?: string, includeDatabaseName = false, quoteIdentifiers = true): string { if (databaseType === "victoriametrics") return metricRangeQuery(table.tableName); - const ref = qualifiedTableName({ databaseType, driverProfile, identifierQuote, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName, includeDatabaseName }); + const ref = qualifiedTableName({ databaseType, driverProfile, identifierQuote, database: table.database, schema: table.schema, catalog: table.catalog, tableName: table.tableName, includeDatabaseName, quoteIdentifiers }); if (databaseType === "influxdb") { return `SELECT * FROM ${ref} WHERE time > now() - 5m ORDER BY time DESC LIMIT 100`; } @@ -179,5 +180,5 @@ export function resolveNewQueryInitialSql(input: ResolveNewQueryInitialSqlInput) const table = resolveNewQueryTable(input); if (!table || table.connectionId !== input.targetConnectionId || table.database !== input.targetDatabase) return undefined; - return buildSelectAllSql(input.databaseType, table, input.identifierQuote, input.driverProfile, input.includeDatabaseName); + return buildSelectAllSql(input.databaseType, table, input.identifierQuote, input.driverProfile, input.includeDatabaseName, input.quoteIdentifiers); } diff --git a/apps/desktop/src/lib/table/tableSelectSql.ts b/apps/desktop/src/lib/table/tableSelectSql.ts index f7d84e602f..056933b3d5 100644 --- a/apps/desktop/src/lib/table/tableSelectSql.ts +++ b/apps/desktop/src/lib/table/tableSelectSql.ts @@ -33,6 +33,8 @@ export interface BuildTableSelectSqlOptions { database?: string; /** Include the active database when this dialect supports `database.table` references. */ includeDatabaseName?: boolean; + /** Emit bare identifiers instead of dialect-specific identifier quotes. */ + quoteIdentifiers?: boolean; } const DATABASE_QUALIFIED_TABLE_TYPES = new Set(["mysql", "clickhouse", "doris", "starrocks", "goldendb"]); @@ -107,44 +109,46 @@ function quoteCypherIdentifier(name: string): string { return `\`${name.replace(/`/g, "``")}\``; } -export function qualifiedTableName(options: Pick): string { - const { databaseType, driverProfile, identifierQuote, schema, tableName, catalog, database, includeDatabaseName } = options; +export function qualifiedTableName(options: Pick): string { + const { databaseType, driverProfile, identifierQuote, schema, tableName, catalog, database, includeDatabaseName, quoteIdentifiers } = options; + const quoteTable = (name: string) => (quoteIdentifiers === false ? name : quoteTableIdentifier(databaseType, name)); + const quoteTableData = (name: string) => (quoteIdentifiers === false ? name : quoteTableDataIdentifier(databaseType, name, identifierQuote)); if (databaseType === "informix" && driverProfile?.trim().toLowerCase() === "gbase8s") { - return quoteTableDataIdentifier(databaseType, tableName, identifierQuote); + return quoteTableData(tableName); } // Doris / StarRocks multi-catalog: address external-catalog tables with the // 3-part `catalog.database.table` form, which the engines accept directly. if (catalog && catalog !== "internal" && (databaseType === "doris" || databaseType === "starrocks")) { - const quotedCatalog = quoteTableIdentifier(databaseType, catalog); - const quotedTable = quoteTableIdentifier(databaseType, tableName); + const quotedCatalog = quoteTable(catalog); + const quotedTable = quoteTable(tableName); // Doris/StarRocks have no separate schema concept; the database under the // external catalog is the middle segment. Prefer schema when a caller // passes it that way, otherwise fall back to database. const middle = schema?.trim() || database?.trim(); if (middle) { - return `${quotedCatalog}.${quoteTableIdentifier(databaseType, middle)}.${quotedTable}`; + return `${quotedCatalog}.${quoteTable(middle)}.${quotedTable}`; } return `${quotedCatalog}.${quotedTable}`; } if (databaseType === "iotdb") { const trimmedSchema = schema?.trim(); if (trimmedSchema && tableName !== trimmedSchema && !tableName.startsWith(`${trimmedSchema}.`)) { - return `${quoteTableIdentifier(databaseType, trimmedSchema)}.${quoteTableIdentifier(databaseType, tableName)}`; + return `${quoteTable(trimmedSchema)}.${quoteTable(tableName)}`; } - return quoteTableIdentifier(databaseType, tableName); + return quoteTable(tableName); } if ((databaseType === "gaussdb" || databaseType === "opengauss" || databaseType === "postgres" || databaseType === "kingbase") && identifierQuote != null) { - const quotedTable = quoteTableDataIdentifier(databaseType, tableName, identifierQuote); + const quotedTable = quoteTableData(tableName); const trimmedSchema = schema?.trim(); if (trimmedSchema) { - return `${quoteTableDataIdentifier(databaseType, trimmedSchema, identifierQuote)}.${quotedTable}`; + return `${quoteTableData(trimmedSchema)}.${quotedTable}`; } return quotedTable; } if (databaseType === "jdbc" && jdbcDriverProfileUsesSchemaQualification(driverProfile)) { - const quotedTable = quoteTableDataIdentifier(databaseType, tableName, identifierQuote); + const quotedTable = quoteTableData(tableName); const trimmedSchema = schema?.trim(); - return trimmedSchema ? `${quoteTableDataIdentifier(databaseType, trimmedSchema, identifierQuote)}.${quotedTable}` : quotedTable; + return trimmedSchema ? `${quoteTableData(trimmedSchema)}.${quotedTable}` : quotedTable; } // Cloud Spanner mirrors `uses_connection_identifier_quote` on the backend, which // counts Spanner unconditionally: the branch must not be gated on a reported @@ -154,34 +158,34 @@ export function qualifiedTableName(options: Pick { expect(normalizeEditorSettings({ openDataTabsNextToActive: null } as any).openDataTabsNextToActive).toBe(false); }); + it("quotes generated SQL identifiers by default and preserves an explicit opt-out", () => { + expect(normalizeEditorSettings({}).generateSqlQuoteIdentifiers).toBe(true); + expect(normalizeEditorSettings({ generateSqlQuoteIdentifiers: true }).generateSqlQuoteIdentifiers).toBe(true); + expect(normalizeEditorSettings({ generateSqlQuoteIdentifiers: false }).generateSqlQuoteIdentifiers).toBe(false); + expect(normalizeEditorSettings({ generateSqlQuoteIdentifiers: "false" } as any).generateSqlQuoteIdentifiers).toBe(true); + }); + it("keeps SQL-file save formatting disabled unless explicitly enabled", () => { expect(normalizeEditorSettings({}).formatSqlOnSqlFileSave).toBe(false); expect(normalizeEditorSettings({ formatSqlOnSqlFileSave: true }).formatSqlOnSqlFileSave).toBe(true); diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts index c94ccc6547..c407d197f5 100644 --- a/apps/desktop/src/stores/settingsStore.ts +++ b/apps/desktop/src/stores/settingsStore.ts @@ -799,6 +799,7 @@ export interface EditorSettings { openDataTabsNextToActive: boolean; prefillNewQueryWithSelect: boolean; generateSqlIncludeDatabaseName: boolean; + generateSqlQuoteIdentifiers: boolean; formatSqlOnSqlFileSave: boolean; updateNotificationsEnabled: boolean; sidebarHiddenTablePrefixes: string[]; @@ -1028,6 +1029,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { openDataTabsNextToActive: false, prefillNewQueryWithSelect: true, generateSqlIncludeDatabaseName: false, + generateSqlQuoteIdentifiers: true, formatSqlOnSqlFileSave: false, updateNotificationsEnabled: true, sidebarHiddenTablePrefixes: [], @@ -1499,6 +1501,7 @@ export function normalizeEditorSettings(settings: Partial, exist openDataTabsNextToActive: typeof settings.openDataTabsNextToActive === "boolean" ? settings.openDataTabsNextToActive : DEFAULT_EDITOR_SETTINGS.openDataTabsNextToActive, prefillNewQueryWithSelect: typeof settings.prefillNewQueryWithSelect === "boolean" ? settings.prefillNewQueryWithSelect : DEFAULT_EDITOR_SETTINGS.prefillNewQueryWithSelect, generateSqlIncludeDatabaseName: settings.generateSqlIncludeDatabaseName === true, + generateSqlQuoteIdentifiers: typeof settings.generateSqlQuoteIdentifiers === "boolean" ? settings.generateSqlQuoteIdentifiers : DEFAULT_EDITOR_SETTINGS.generateSqlQuoteIdentifiers, formatSqlOnSqlFileSave: settings.formatSqlOnSqlFileSave === true, updateNotificationsEnabled: settings.updateNotificationsEnabled ?? DEFAULT_EDITOR_SETTINGS.updateNotificationsEnabled, sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes), @@ -2187,6 +2190,7 @@ export const useSettingsStore = defineStore("settings", () => { if (partial.openDataTabsNextToActive !== undefined) editorSettings.value.openDataTabsNextToActive = partial.openDataTabsNextToActive === true; if (partial.prefillNewQueryWithSelect !== undefined) editorSettings.value.prefillNewQueryWithSelect = partial.prefillNewQueryWithSelect; if (partial.generateSqlIncludeDatabaseName !== undefined) editorSettings.value.generateSqlIncludeDatabaseName = partial.generateSqlIncludeDatabaseName === true; + if (partial.generateSqlQuoteIdentifiers !== undefined) editorSettings.value.generateSqlQuoteIdentifiers = partial.generateSqlQuoteIdentifiers === true; if (partial.formatSqlOnSqlFileSave !== undefined) editorSettings.value.formatSqlOnSqlFileSave = partial.formatSqlOnSqlFileSave === true; if (partial.updateNotificationsEnabled !== undefined) editorSettings.value.updateNotificationsEnabled = partial.updateNotificationsEnabled; if (partial.sidebarHiddenTablePrefixes !== undefined) editorSettings.value.sidebarHiddenTablePrefixes = normalizeSidebarHiddenTablePrefixes(partial.sidebarHiddenTablePrefixes);