fix(ddl): honor database qualification settings
This commit is contained in:
17 files changed
+346
-30
No files matched your search
@@ -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 {
|
||||
|
||||
+33
-2
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user