fix(editor): unquote DDL view when identifier quoting disabled
This commit is contained in:
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(", ")}`;
|
||||
|
||||
@@ -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)");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user