fix(ddl): honor database qualification settings

This commit is contained in:
zipg authored and GitHub committed 2026-09-18 17:34:40 +08:00
1 parent 06bbf99e3d
commit 8168114734
17 files changed
+346 -30

No files matched your search

@@ -48,7 +48,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 { omitDdlDatabaseQualifier, 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";
@@ -3515,7 +3515,8 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
// the aligned column layout from reformatHoverDdl.
const isViewObject = objectMetadataRequest.objectType === "VIEW" || objectMetadataRequest.objectType === "MATERIALIZED_VIEW";
const formatted = isViewObject ? await formatSqlForDisplay(rawDdl, formatDialect, settingsStore.editorSettings.sqlFormatter) : reformatHoverDdl(rawDdl, quoteQualifiedName(hoverQualifiedName));
sqlContent = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
const unqualified = omitDdlDatabaseQualifier(formatted, formatDialect, props.databaseType, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.catalog);
sqlContent = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? unqualified : omitDdlIdentifierQuotes(unqualified, formatDialect);
}
} catch (error) {
console.warn(`[DBX] Failed to load table DDL for ${hoverDatabase}.${hoverSchema}.${table.name}:`, error);
@@ -3548,6 +3549,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
}
if (fullColumns.length > 0) {
sqlContent = buildHoverTableSql(quoteQualifiedName(hoverQualifiedName), fullColumns, fullIndexes, tableComment);
sqlContent = omitDdlDatabaseQualifier(sqlContent, formatDialect, props.databaseType, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.catalog);
if (!settingsStore.editorSettings.generateSqlQuoteIdentifiers) sqlContent = omitDdlIdentifierQuotes(sqlContent, formatDialect);
metadataLoadFailed = false;
}
@@ -11,7 +11,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 { omitDdlDatabaseQualifier, 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";
@@ -131,7 +131,8 @@ async function loadDdl(force = false) {
);
const formatDialect = props.formatDialect ?? props.dialect;
const formatted = await formatSqlForDisplay(ddl, formatDialect, settingsStore.editorSettings.sqlFormatter);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
const unqualified = omitDdlDatabaseQualifier(formatted, formatDialect, props.databaseType, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.catalog);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? unqualified : omitDdlIdentifierQuotes(unqualified, formatDialect);
} catch (e: any) {
ddlError.value = e?.message || String(e);
} finally {
@@ -122,7 +122,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 { omitDdlDatabaseQualifier, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { isCancelSearchShortcut } from "@/lib/editor/keyboardShortcuts";
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
import { connectionIsEffectivelyReadOnly } from "@/lib/database/readOnlyWriteAccess";
@@ -1189,7 +1189,8 @@ async function fetchTableDdl(force = settingsStore.editorSettings.refreshDdlOnOp
const { ddl } = await loadObjectDdl(tableMetadataRequest(row), { force });
if (sidePanelGuard.isStale(epoch)) return;
const formatDialect = sqlFormatDialectForDbType(effectiveDatabaseType.value);
rawTableDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect);
const unqualified = omitDdlDatabaseQualifier(ddl, formatDialect, effectiveDatabaseType.value, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.catalog);
rawTableDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? unqualified : omitDdlIdentifierQuotes(unqualified, formatDialect);
loadedSuccessfully = true;
} catch (e: any) {
if (sidePanelGuard.isStale(epoch)) return;
@@ -160,7 +160,6 @@ import { buildRenameObjectSql, buildRenameDatabaseSql, buildRenameDatabasePrefli
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
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, connectionTableSqlSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { isObjectCacheInvalidationError } from "@/lib/metadata/objectCacheInvalidationError";
@@ -190,7 +189,7 @@ import { sidebarTreeArrowAction } from "@/lib/sidebar/sidebarTreeArrowNavigation
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
import { buildSidebarDdlTemplateSql } from "@/lib/sidebar/sidebarDdlTemplate";
import { buildSidebarDdlTemplateSql, formatSidebarDdlTemplateForDisplay } from "@/lib/sidebar/sidebarDdlTemplate";
import { resolveSidebarDdlTargets } from "@/lib/sidebar/sidebarDdlTargets";
import { sidebarTableDataExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
import { formatSidebarTableCopyText, type FormatSidebarTableNamesOptions } from "@/lib/sidebar/sidebarTableNameCopy";
@@ -2152,7 +2151,7 @@ async function openSidebarMultiTableDdlTab(targets: Array<TreeNode & { connectio
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);
return formatSidebarDdlTemplateForDisplay(formatted, formatDialect, databaseTypeForNode(target), settingsStore.editorSettings.generateSqlIncludeDatabaseName, settingsStore.editorSettings.generateSqlQuoteIdentifiers, target.catalog);
},
);
connectionStore.activeConnectionId = tabTarget.connectionId;
@@ -0,0 +1,22 @@
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const runtimeSource = readFileSync(fileURLToPath(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url)), "utf8");
function asyncFunctionSource(name: string): string {
const start = runtimeSource.indexOf(`async function ${name}(`);
expect(start, `${name} not found`).toBeGreaterThan(-1);
const next = runtimeSource.indexOf("\nasync function ", start + 1);
return runtimeSource.slice(start, next === -1 ? undefined : next);
}
describe("SidebarTreeRuntimeHost DDL database qualifier setting (#9421)", () => {
it("applies the setting with each target catalog before identifier quote formatting", () => {
const openDdl = asyncFunctionSource("openSidebarMultiTableDdlTab");
const formatterCall = openDdl.indexOf("formatSidebarDdlTemplateForDisplay(");
expect(formatterCall).toBeGreaterThan(-1);
expect(openDdl.slice(formatterCall, openDdl.indexOf(");", formatterCall))).toContain("target.catalog");
});
});
@@ -30,10 +30,10 @@ import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect
import { useToast } from "@/composables/useToast";
import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sql/sqlHighlighter";
import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript";
import { formatGeneratedDdlIdentifierQuotes, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { formatGeneratedDdlIdentifierQuotes, omitDdlDatabaseQualifier, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay";
import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges";
import { copyToClipboard } from "@/lib/common/clipboard";
import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
import { formatSqlForDisplay, sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
import { queryTimeoutSecsForConcurrentIndex, queryTimeoutSecsForConnection } from "@/lib/sql/queryTimeout";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { invalidateObjectDdl, loadObjectDdl } from "@/lib/metadata/objectDdlCache";
@@ -341,6 +341,17 @@ function scheduleDdlEditorInit() {
});
}
/**
* Applies the DDL display preferences (database qualifier + identifier quoting)
* to DDL shown by the structure editor, whether loaded from the server or
* generated from the pending structure changes.
*/
function formatDdlForDisplay(sql: string, dialect: SqlFormatDialect, generated = false): string {
const unqualified = omitDdlDatabaseQualifier(sql, dialect, databaseType.value, settingsStore.editorSettings.generateSqlIncludeDatabaseName, props.catalog);
if (settingsStore.editorSettings.generateSqlQuoteIdentifiers) return unqualified;
return generated ? formatGeneratedDdlIdentifierQuotes(unqualified, dialect, false) : omitDdlIdentifierQuotes(unqualified, dialect);
}
function ddlRequest() {
return {
connectionId: props.connectionId,
@@ -359,7 +370,7 @@ async function fetchDdl(force = false) {
const { ddl } = await loadObjectDdl(ddlRequest(), { force });
const dialect = sqlFormatDialectForDbType(databaseType.value);
const formatted = await formatSqlForDisplay(ddl, dialect, settingsStore.editorSettings.sqlFormatter);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
rawDdlContent.value = formatDdlForDisplay(formatted, dialect);
ddlFetched.value = true;
} catch (e: any) {
rawDdlContent.value = `-- Error: ${e?.message || e}`;
@@ -1518,7 +1529,7 @@ async function hydrateRestoredDraftFromDatabase() {
const { ddl } = await loadObjectDdl({ connectionId, database, schema, tableName, catalog });
const dialect = sqlFormatDialectForDbType(databaseType.value);
const formatted = await formatSqlForDisplay(ddl, dialect, settingsStore.editorSettings.sqlFormatter);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
rawDdlContent.value = formatDdlForDisplay(formatted, dialect);
ddlFetched.value = true;
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
} catch {
@@ -1785,7 +1796,7 @@ async function refreshSqlPreview() {
if (requestId !== sqlPreviewRequestId) return;
const statements = [...result.statements, ...ownerResult.statements, ...(mysqlAutoIncrementStatement ? [mysqlAutoIncrementStatement] : [])];
// SQLite type-change apply regenerates this revision-checked plan, so its preview must stay byte-for-byte aligned.
pendingStatements.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers !== false || hasSqliteTypeChange.value ? statements : statements.map((statement) => formatGeneratedDdlIdentifierQuotes(statement, sqlFormatDialectForDbType(databaseType.value), false));
pendingStatements.value = hasSqliteTypeChange.value ? statements : statements.map((statement) => formatDdlForDisplay(statement, sqlFormatDialectForDbType(databaseType.value), true));
warnings.value = [...result.warnings, ...ownerResult.warnings];
sqliteSchemaRevision.value = "schemaRevision" in result && typeof result.schemaRevision === "string" ? result.schemaRevision : undefined;
} catch (e: any) {
@@ -2178,7 +2189,7 @@ async function loadStructure(
const { ddl } = await loadObjectDdl({ connectionId, database, schema, tableName, catalog }, { force: options.forceDdl });
const dialect = sqlFormatDialectForDbType(databaseType.value);
const formatted = await formatSqlForDisplay(ddl, dialect, settingsStore.editorSettings.sqlFormatter);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
rawDdlContent.value = formatDdlForDisplay(formatted, dialect);
ddlFetched.value = true;
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
} catch {
@@ -23,6 +23,16 @@ const mocks = vi.hoisted(() => ({
getTablePartitionStatus: vi.fn(),
getTableOwner: vi.fn(),
updateEditorSettings: vi.fn(),
editorSettings: {
structureEditorDensity: "compact",
sqlFormatter: {},
tableColumnTemplateFields: [],
fontSize: 13,
fontFamily: "monospace",
theme: "default",
generateSqlIncludeDatabaseName: true,
generateSqlQuoteIdentifiers: true,
},
loadObjectDdl: vi.fn(),
invalidateObjectDdl: vi.fn(),
loadObjectMetadataFacet: vi.fn(),
@@ -259,7 +269,7 @@ vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({ tableStructureRe
vi.mock("@/stores/historyStore", () => ({ useHistoryStore: () => ({ add: vi.fn() }) }));
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: { structureEditorDensity: "compact", sqlFormatter: {}, tableColumnTemplateFields: [], fontSize: 13, fontFamily: "monospace", theme: "default", generateSqlQuoteIdentifiers: true },
editorSettings: mocks.editorSettings,
updateEditorSettings: mocks.updateEditorSettings,
}),
}));
@@ -293,7 +303,7 @@ import TableStructureEditor from "@/components/structure/TableStructureEditor.vu
const mountedApps: App[] = [];
let lastDraft: Record<string, unknown> | undefined;
async function mountStructureEditor() {
async function mountStructureEditor(props: Record<string, unknown> = {}) {
const root = document.createElement("div");
document.body.append(root);
const app = createApp(TableStructureEditor, {
@@ -301,6 +311,7 @@ async function mountStructureEditor() {
database: "test",
tableName: "users",
initialTab: "columns",
...props,
"onUpdate:draft": (draft: Record<string, unknown> | undefined) => {
lastDraft = draft;
},
@@ -373,6 +384,8 @@ async function editDdl(root: HTMLElement, script: string) {
beforeEach(() => {
vi.clearAllMocks();
lastDraft = undefined;
mocks.connection.db_type = "mysql";
mocks.editorSettings.generateSqlIncludeDatabaseName = true;
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.executeQuery.mockResolvedValue({ columns: [], rows: [] });
mocks.executeBatch.mockResolvedValue({ rowsAffected: 0 });
@@ -437,6 +450,24 @@ describe("TableStructureEditor DDL tab", () => {
expect(mocks.buildTableStructureChangeSql).not.toHaveBeenCalled();
});
it("executes generated external-catalog DDL without dropping qualifiers", async () => {
mocks.connection.db_type = "doris";
mocks.editorSettings.generateSqlIncludeDatabaseName = false;
mocks.buildTableStructureChangeSql.mockResolvedValue({
statements: ["ALTER TABLE `iceberg`.`analytics`.`users` ADD COLUMN `nickname` STRING;"],
warnings: [],
});
const root = await mountStructureEditor({ database: "analytics", catalog: "iceberg" });
buttonWithText(root, "structureEditor.addColumn").click();
await vi.waitFor(() => expect(buttonWithText(root, "structureEditor.apply").disabled).toBe(false), { timeout: 3000 });
buttonWithText(root, "structureEditor.apply").click();
await vi.waitFor(() => expect(mocks.executeBatch).toHaveBeenCalledTimes(1), { timeout: 3000 });
expect(mocks.executeBatch.mock.calls[0][2]).toEqual(["ALTER TABLE `iceberg`.`analytics`.`users` ADD COLUMN `nickname` STRING;"]);
});
it("carries the edited script in the draft so the tab reports unsaved work", async () => {
const root = await mountStructureEditor();
await openDdlTab(root);