fix(editor): unquote DDL view when identifier quoting disabled

This commit is contained in:
zipg authored and GitHub committed 2026-09-08 00:14:02 +08:00
1 parent 5814ff0088
commit 889fa4f5d0
7 files changed
+87 -5

No files matched your search

@@ -22,6 +22,7 @@ import { looksLikeDmlStatement } from "@/lib/sql/dmlChangePreview";
import { expandToSqlStatementWindow } from "@/lib/sql/insertValueHints";
import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns";
import { canFormatSqlForDatabaseType, formatSqlForDisplay, formatSqlForEditing, compressSqlText, sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { detectAndFormatStructured } from "@/lib/sql/autoFormat";
import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax";
import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/editor/queryEditorTextEdits";
@@ -2995,7 +2996,9 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
// (the same one the sidebar/object-source viewers use). Tables keep
// the aligned column layout from reformatHoverDdl.
const isViewObject = objectMetadataRequest.objectType === "VIEW" || objectMetadataRequest.objectType === "MATERIALIZED_VIEW";
sqlContent = isViewObject ? await formatSqlForDisplay(rawDdl, props.formatDialect ?? sqlFormatDialectForDbType(props.databaseType), settingsStore.editorSettings.sqlFormatter) : reformatHoverDdl(rawDdl, quoteQualifiedName(hoverQualifiedName));
const formatDialect = props.formatDialect ?? sqlFormatDialectForDbType(props.databaseType);
const formatted = isViewObject ? await formatSqlForDisplay(rawDdl, formatDialect, settingsStore.editorSettings.sqlFormatter) : reformatHoverDdl(rawDdl, quoteQualifiedName(hoverQualifiedName));
sqlContent = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
}
} catch (error) {
console.warn(`[DBX] Failed to load table DDL for ${hoverDatabase}.${hoverSchema}.${table.name}:`, error);
@@ -92,6 +92,8 @@ import { loadObjectDdl } from "@/lib/metadata/objectDdlCache";
import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
import * as api from "@/lib/backend/api";
import { formatElapsedSeconds } from "@/lib/common/elapsedTime";
import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
import { dataGridCellDisplayText, dataGridCellEditorText } from "@/lib/dataGrid/dataGridCellCoercion";
import { createColumnDrafts } from "@/lib/table/tableStructureEditorState";
@@ -11400,7 +11402,8 @@ async function fetchDdl(force = settingsStore.editorSettings.refreshDdlOnOpen) {
},
{ force },
);
ddlContent.value = ddl;
const formatDialect = sqlFormatDialectForDbType(resolvedDatabaseType.value);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect);
} catch (e: any) {
ddlContent.value = `-- Error: ${e}`;
} finally {
@@ -9,6 +9,7 @@ import { loadEditorTheme, editorFontTheme } from "@/lib/editor/editorThemes";
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
import { copyToClipboard } from "@/lib/common/clipboard";
import { formatSqlForDisplay, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { loadObjectDdl } from "@/lib/metadata/objectDdlCache";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -125,7 +126,9 @@ async function loadDdl(force = false) {
},
{ force },
);
ddlContent.value = await formatSqlForDisplay(ddl, props.formatDialect ?? props.dialect, settingsStore.editorSettings.sqlFormatter);
const formatDialect = props.formatDialect ?? props.dialect;
const formatted = await formatSqlForDisplay(ddl, formatDialect, settingsStore.editorSettings.sqlFormatter);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
} catch (e: any) {
ddlError.value = e?.message || String(e);
} finally {
@@ -117,6 +117,7 @@ import { useQueryStore } from "@/stores/queryStore";
import QueryEditor from "@/components/editor/QueryEditor.vue";
import MySqlEventEditor from "@/components/objects/MySqlEventEditor.vue";
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { isCancelSearchShortcut } from "@/lib/editor/keyboardShortcuts";
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
@@ -1149,7 +1150,8 @@ async function fetchTableDdl(force = settingsStore.editorSettings.refreshDdlOnOp
try {
const { ddl } = await loadObjectDdl(tableMetadataRequest(row), { force });
if (sidePanelGuard.isStale(epoch)) return;
tableDdlContent.value = ddl;
const formatDialect = sqlFormatDialectForDbType(effectiveDatabaseType.value);
tableDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect);
loadedSuccessfully = true;
} catch (e: any) {
if (sidePanelGuard.isStale(epoch)) return;
@@ -153,6 +153,7 @@ import { buildEditableObjectSource, buildRoutineRenameObjectSourceStatements, su
import { loadEditableObjectSourceForEditor } from "@/lib/table/objectSourceLoad";
import { buildViewDdl } from "@/lib/table/viewDdl";
import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { getTableStructureCapabilities } from "@/lib/table/tableStructureCapabilities";
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { hasTreeNodeDatabaseContext } from "@/lib/sidebar/treeNodeContext";
@@ -2090,7 +2091,11 @@ async function openSidebarMultiTableDdlTab(targets: Array<TreeNode & { connectio
source: result.source,
});
},
(ddl, target) => formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseTypeForNode(target)), settingsStore.editorSettings.sqlFormatter),
async (ddl, target) => {
const formatDialect = sqlFormatDialectForDbType(databaseTypeForNode(target));
const formatted = await formatSqlForDisplay(ddl, formatDialect, settingsStore.editorSettings.sqlFormatter);
return settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
},
);
connectionStore.activeConnectionId = tabTarget.connectionId;
const title = `DDL - ${targets.map((target) => target.label).join(", ")}`;
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
describe("omitDdlIdentifierQuotes", () => {
it("removes safe MySQL identifier quotes without changing literals or comments", () => {
const ddl = "CREATE TABLE `demo_table` (`id` int DEFAULT 'a`b') COMMENT='`keep`'; -- `keep`";
expect(omitDdlIdentifierQuotes(ddl, "mysql")).toBe("CREATE TABLE demo_table (id int DEFAULT 'a`b') COMMENT='`keep`'; -- `keep`");
});
it("keeps quotes for names that require them", () => {
expect(omitDdlIdentifierQuotes("CREATE TABLE `order` (`with space` int)", "mysql")).toBe("CREATE TABLE `order` (`with space` int)");
});
it("supports PostgreSQL and SQL Server identifier delimiters", () => {
expect(omitDdlIdentifierQuotes('CREATE TABLE "demo_table" ("id" integer)', "postgres")).toBe("CREATE TABLE demo_table (id integer)");
expect(omitDdlIdentifierQuotes("CREATE TABLE [demo_table] ([id] int)", "sqlserver")).toBe("CREATE TABLE demo_table (id int)");
});
it("keeps brackets on SQL Server reserved words while unquoting safe names", () => {
const ddl = "CREATE TABLE [demo_table] ([order] int, [key] int, [user] nvarchar(50), [id] int)";
expect(omitDdlIdentifierQuotes(ddl, "sqlserver")).toBe("CREATE TABLE demo_table ([order] int, [key] int, [user] nvarchar(50), id int)");
});
});
+43
View File
@@ -0,0 +1,43 @@
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
import { tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens";
import type { SqlFormatDialect } from "@/lib/sql/sqlFormatter";
const SIMPLE_SQLSERVER_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
function canRenderUnquoted(identifier: string, dialect: SqlFormatDialect): boolean {
switch (dialect) {
case "mysql":
case "clickhouse":
return !requiresMysqlIdentifierQuote(identifier);
case "postgres":
case "sqlite":
case "oracle":
case "duckdb":
case "dameng":
case "generic":
return !requiresPostgresIdentifierQuote(identifier);
case "sqlserver":
return SIMPLE_SQLSERVER_IDENTIFIER.test(identifier) && !requiresMysqlIdentifierQuote(identifier.toLowerCase());
default:
return false;
}
}
/** Removes dialect identifier quotes from safe names while preserving strings, comments, and unsafe names. */
export function omitDdlIdentifierQuotes(sql: string, dialect: SqlFormatDialect): string {
const tokens = tokenizeSqlSemantic(sql, dialect === "sqlserver" ? "sqlserver" : dialect);
const replacements: Array<{ start: number; end: number; value: string }> = [];
for (const token of tokens) {
if (token.kind !== "quoted_identifier") continue;
const identifier = unquoteSqlSemanticIdentifier(token);
if (canRenderUnquoted(identifier, dialect)) replacements.push({ start: token.span.start, end: token.span.end, value: identifier });
}
let result = sql;
for (let index = replacements.length - 1; index >= 0; index -= 1) {
const replacement = replacements[index]!;
result = `${result.slice(0, replacement.start)}${replacement.value}${result.slice(replacement.end)}`;
}
return result;
}