feat(editor): add toggle for SQL identifier quoting

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-07 01:11:42 +08:00
1 parent 14e1d4f25b
commit 2eaf7f9914
23 files changed
+125 -23

No files matched your search

+1
View File
@@ -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) {
@@ -538,6 +538,7 @@ const editDataTabReuseMode = ref<DataTabReuseMode>(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<ClickTableNavigationTarget>(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(() => {
<Switch id="generate-sql-include-database-name" v-model="editGenerateSqlIncludeDatabaseName" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="generate-sql-quote-identifiers">{{ t("settings.generateSqlQuoteIdentifiers") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.generateSqlQuoteIdentifiersDescription") }}
</p>
</div>
<Switch id="generate-sql-quote-identifiers" v-model="editGenerateSqlQuoteIdentifiers" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="format-sql-on-sql-file-save">{{ t("settings.formatSqlOnSqlFileSave") }}</Label>
@@ -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,
});
+2
View File
@@ -6971,6 +6971,8 @@ export default {
prefillNewQueryWithSelectDescription: "When creating a new query, prefill the editor with SELECT * FROM <table> 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",
+2
View File
@@ -6532,6 +6532,8 @@ export default withEnglishFallback({
prefillNewQueryWithSelectDescription: "Al crear una nueva consulta, rellena el editor con SELECT * FROM <tabla> 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",
+2
View File
@@ -6532,6 +6532,8 @@ export default withEnglishFallback({
prefillNewQueryWithSelectDescription: "Alla creazione di una nuova query, precompila l'editor con SELECT * FROM <tabella> 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",
+2
View File
@@ -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+クリックでテーブル構造を開く",
+2
View File
@@ -6285,6 +6285,8 @@ export default withEnglishFallback({
prefillNewQueryWithSelectDescription: "새 쿼리를 만들 때 활성 테이블 탭이나 사이드바에서 선택한 테이블을 기반으로 편집기에 SELECT * FROM <table>을 미리 채웁니다.",
generateSqlIncludeDatabaseName: "생성된 SQL에 데이터베이스 이름 포함",
generateSqlIncludeDatabaseNameDescription: "데이터베이스가 지원하는 경우 생성된 테이블 SQL에서 테이블 이름만이 아니라 database.table을 사용합니다.",
generateSqlQuoteIdentifiers: "생성된 SQL에서 식별자 인용",
generateSqlQuoteIdentifiersDescription: "끄면 새 쿼리의 테이블 참조에서 데이터베이스 방언별 인용을 생략합니다. 예약어, 특수 문자 또는 대소문자 구분 이름에는 인용이 필요할 수 있습니다.",
formatSqlOnSqlFileSave: "SQL 파일 저장 시 SQL 서식 지정",
formatSqlOnSqlFileSaveDescription: "SQL 파일을 저장할 때 SQL을 자동으로 서식 지정합니다. SQL 라이브러리에 저장된 파일과 새 쿼리를 .sql 파일로 저장하는 경우도 포함합니다.",
sqlVariableSyntax: "SQL 변수 및 자리표시자 치환",
+2
View File
@@ -6534,6 +6534,8 @@ export default withEnglishFallback({
prefillNewQueryWithSelectDescription: "Ao criar uma nova consulta, preenche o editor com SELECT * FROM <tabela> 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",
+2
View File
@@ -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 <tablo> 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",
+2
View File
@@ -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",
+2
View File
@@ -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",
@@ -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({
@@ -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;");
});
});
@@ -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");
});
@@ -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" },
@@ -75,6 +75,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"openDataTabsNextToActive",
"prefillNewQueryWithSelect",
"generateSqlIncludeDatabaseName",
"generateSqlQuoteIdentifiers",
"formatSqlOnSqlFileSave",
"showTableDdlHoverPreview",
"updateNotificationsEnabled",
@@ -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" },
+4 -3
View File
@@ -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 <table>`
@@ -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<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>, identifierQuote?: string, driverProfile?: string, includeDatabaseName = false): string {
export function buildSelectAllSql(databaseType: DatabaseType | undefined, table: Pick<NewQueryTable, "schema" | "catalog" | "tableName"> & Partial<Pick<NewQueryTable, "database">>, 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);
}
+24 -20
View File
@@ -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<DatabaseType>(["mysql", "clickhouse", "doris", "starrocks", "goldendb"]);
@@ -107,44 +109,46 @@ function quoteCypherIdentifier(name: string): string {
return `\`${name.replace(/`/g, "``")}\``;
}
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "driverProfile" | "identifierQuote" | "schema" | "tableName" | "catalog" | "database" | "includeDatabaseName">): string {
const { databaseType, driverProfile, identifierQuote, schema, tableName, catalog, database, includeDatabaseName } = options;
export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "databaseType" | "driverProfile" | "identifierQuote" | "schema" | "tableName" | "catalog" | "database" | "includeDatabaseName" | "quoteIdentifiers">): 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<BuildTableSelectSqlOptions, "da
// `` `s`.`t` `` with an empty `s` is a Spanner syntax error; a missing quote falls
// back to the GoogleSQL backtick default, exactly like `identifiers.rs`.
if (databaseType === "spanner") {
const quotedTable = quoteTableDataIdentifier(databaseType, tableName, identifierQuote);
const quotedTable = quoteTableData(tableName);
// `database` holds the resource path `projects/{p}/instances/{i}/databases/{d}`, and callers
// that treat the database as the schema (the sidebar SQL templates collapse
// `node.schema || node.database`) would otherwise emit `` `projects/…`.`singers` ``. A Spanner
// schema name is letters, digits and underscores, so the path separator identifies it.
const trimmedSchema = schema?.trim();
const schemaQualifier = trimmedSchema && !trimmedSchema.includes("/") ? trimmedSchema : undefined;
return schemaQualifier ? `${quoteTableDataIdentifier(databaseType, schemaQualifier, identifierQuote)}.${quotedTable}` : quotedTable;
return schemaQualifier ? `${quoteTableData(schemaQualifier)}.${quotedTable}` : quotedTable;
}
if (databaseType === "informix" && identifierQuote != null) {
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;
}
if ((isSchemaAware(databaseType) || databaseType === "sqlite") && !usesDatabaseObjectTreeMode(databaseType) && schema) {
if (databaseType === "sqlserver") {
const linked = parseSqlServerLinkedSchema(schema);
if (linked) return sqlServerLinkedTableName(linked, tableName);
if (linked) return quoteIdentifiers === false ? [linked.server, linked.catalog, linked.schema, tableName].join(".") : sqlServerLinkedTableName(linked, tableName);
}
return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`;
return `${quoteTable(schema)}.${quoteTable(tableName)}`;
}
// MySQL-style engines use the selected database as their table namespace.
// Keep this opt-in so existing generated SQL remains unchanged by default.
const trimmedDatabase = database?.trim();
if (includeDatabaseName && trimmedDatabase && databaseType && DATABASE_QUALIFIED_TABLE_TYPES.has(databaseType)) {
return `${quoteTableIdentifier(databaseType, trimmedDatabase)}.${quoteTableIdentifier(databaseType, tableName)}`;
return `${quoteTable(trimmedDatabase)}.${quoteTable(tableName)}`;
}
return quoteTableIdentifier(databaseType, tableName);
return quoteTable(tableName);
}
interface SqlCteVisibility {
@@ -9,6 +9,8 @@ export interface TableSqlTemplateOptions {
catalog?: string;
database?: string;
includeDatabaseName?: boolean;
/** Emit bare table identifiers instead of dialect-specific quotes. */
quoteIdentifiers?: boolean;
tableName: string;
columns?: ColumnInfo[];
tableType?: string;
@@ -107,6 +109,7 @@ function templateTableName(options: TableSqlTemplateOptions): string {
catalog: options.catalog,
database: options.database,
includeDatabaseName: options.includeDatabaseName,
quoteIdentifiers: options.quoteIdentifiers,
schema: options.schema,
tableName: options.tableName,
});
@@ -98,6 +98,13 @@ describe("normalizeEditorSettings", () => {
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);
+4
View File
@@ -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<EditorSettings>, 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);