fix(oceanbase): preserve partition DDL and toggle storage options

This commit is contained in:
efeire authored and GitHub committed 2026-09-18 01:59:42 +08:00
1 parent 8eba12ea2f
commit fe0dd02f2b
36 files changed
+870 -125

No files matched your search

@@ -197,6 +197,7 @@ import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMeta
import { loadTableMetadata } from "@/lib/metadata/tableMetadataCache";
import { analyzeIntentionActions, prepareExpandWildcardContext, buildExpandWildcardReplacement, type IntentionAction } from "@/lib/editor/sqlIntentionActions";
import { loadObjectDdl } from "@/lib/metadata/objectDdlCache";
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
import { queryContextObjectActions, queryContextObjectRoute, queryTableCandidateAtSqlPosition, queryTableNavigationTargetAtSqlPosition, resolveQueryContextCandidateDatabase, resolveQueryContextObjectTarget, type QueryContextObjectAction } from "@/lib/sql/queryCursorTableTarget";
import * as api from "@/lib/backend/api";
@@ -3397,7 +3398,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
// views. Hover only removes PostgreSQL's appended access-control tail.
try {
const { ddl } = await loadObjectDdl(objectMetadataRequest);
const rawDdl = ddlForHoverPreview(ddl);
const rawDdl = ddlForHoverPreview(applyDdlStoragePreference(ddl, props.databaseType, settingsStore.editorSettings.excludeDdlStorage));
if (rawDdl && rawDdl.trim()) {
// A view's display DDL wraps the raw (often single-line) view source
// in `CREATE ... VIEW ... AS`; the table-oriented reformatter cannot
+19 -13
View File
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { useUpdateBlocker } from "@/lib/app/updatePreparation";
import { computed, nextTick, onMounted, onUnmounted, onActivated, onDeactivated, ref, shallowRef, toRaw, useSlots, watch, defineAsyncComponent, type Component, type CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
@@ -10287,7 +10290,8 @@ const showTableInfo = ref(false);
const activeTableInfoTab = ref<TableInfoTab>(settingsStore.editorSettings.tableInfoActiveTab);
const canPinTableInfo = computed(() => props.context === "table-data");
const tableInfoDrawerPinned = computed(() => settingsStore.editorSettings.tableInfoDrawerPinned);
const ddlContent = ref("");
const rawDdlContent = ref("");
const ddlContent = computed(() => applyDdlStoragePreference(rawDdlContent.value, resolvedDatabaseType.value, settingsStore.editorSettings.excludeDdlStorage));
const tableInfoColumns = ref<ColumnInfo[]>(props.tableMeta?.columns ?? []);
const tableInfoColumnsLoading = ref(false);
const tableInfoColumnsRequestGeneration = ref(0);
@@ -10507,7 +10511,7 @@ const canShowTableIndexes = computed(() => tableMetadataCapabilities.value.index
const metadataLoaders = useDataGridTableMetadataLoaders({
props,
state: {
ddlContent,
ddlContent: rawDdlContent,
ddlLoading,
tableInfoColumns,
tableInfoColumnsLoading,
@@ -10677,7 +10681,7 @@ watch(
tableOwnerLoading.value = false;
tableOwnerError.value = "";
tableOwnerRequestGeneration.value += 1;
ddlContent.value = "";
rawDdlContent.value = "";
indexes.value = [];
indexesLoaded.value = false;
indexesLoading.value = false;
@@ -13422,16 +13426,18 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@request-drop-mongo-index="requestDropMongoIndex"
/>
<pre
v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading"
ref="ddlPreRef"
data-native-clipboard
tabindex="0"
class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text outline-none"
:class="settingsStore.editorSettings.tableDdlWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'"
v-html="filteredDdlContent"
@keydown="onDdlKeydown"
></pre>
<template v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading">
<DdlStorageToggle :database-type="resolvedDatabaseType" class="border-b px-3 py-2" />
<pre
ref="ddlPreRef"
data-native-clipboard
tabindex="0"
class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text outline-none"
:class="settingsStore.editorSettings.tableDdlWordWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'"
v-html="filteredDdlContent"
@keydown="onDdlKeydown"
></pre>
</template>
<div v-else class="flex-1 flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
</div>
@@ -0,0 +1,19 @@
<script setup lang="ts">
import { useId } from "vue";
import { useI18n } from "vue-i18n";
import { useSettingsStore } from "@/stores/settingsStore";
import { Switch } from "@/components/ui/switch";
import type { DatabaseType } from "@/types/database";
defineProps<{ databaseType?: DatabaseType; disabled?: boolean }>();
const settings = useSettingsStore();
const { t } = useI18n();
const id = useId();
</script>
<template>
<div v-if="databaseType === 'oceanbase-oracle'" class="flex shrink-0 items-center gap-2 text-xs" :title="t('contextMenu.excludeDdlStorageHint')">
<Switch :id="id" size="sm" :disabled="disabled" :model-value="settings.editorSettings.excludeDdlStorage !== false" @update:model-value="settings.updateEditorSettings({ excludeDdlStorage: $event })" />
<label :for="id" class="cursor-pointer">{{ t("contextMenu.excludeDdlStorage") }}</label>
</div>
</template>
@@ -1,4 +1,6 @@
<script setup lang="ts">
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { computed, nextTick, onUnmounted, ref, shallowRef, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Clipboard, Loader2, RefreshCw } from "@lucide/vue";
@@ -47,7 +49,8 @@ const { toast } = useToast();
const { isDark, themePalette } = useTheme();
const settingsStore = useSettingsStore();
const ddlContent = ref("");
const rawDdlContent = ref("");
const ddlContent = computed(() => applyDdlStoragePreference(rawDdlContent.value, props.databaseType, settingsStore.editorSettings.excludeDdlStorage));
const ddlLoading = ref(false);
const ddlError = ref("");
const ddlEditorContainer = ref<HTMLDivElement>();
@@ -128,7 +131,7 @@ async function loadDdl(force = false) {
);
const formatDialect = props.formatDialect ?? props.dialect;
const formatted = await formatSqlForDisplay(ddl, formatDialect, settingsStore.editorSettings.sqlFormatter);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, formatDialect);
} catch (e: any) {
ddlError.value = e?.message || String(e);
} finally {
@@ -144,7 +147,7 @@ watch(
if (!open) return;
const active = document.activeElement;
editorRootToRestoreFocus = active instanceof HTMLElement ? active.closest(".cm-editor") : null;
ddlContent.value = "";
rawDdlContent.value = "";
await loadDdl(settingsStore.editorSettings.refreshDdlOnOpen);
},
{ immediate: true },
@@ -247,6 +250,11 @@ function copyDdlContent() {
}
}
watch(ddlContent, (content) => {
const view = ddlEditorView.value;
if (view) view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: content } });
});
// When DDL finishes loading, create the editor inside the dialog.
watch(ddlLoading, (loading) => {
if (!loading && ddlContent.value && props.open) {
@@ -270,7 +278,7 @@ onUnmounted(() => {
});
function retry() {
ddlContent.value = "";
rawDdlContent.value = "";
void loadDdl(true);
}
@@ -307,6 +315,7 @@ function onClose() {
<EditorSearchPanel v-if="ddlEditorView" ref="ddlSearchPanelRef" :view="ddlEditorView" />
</div>
</div>
<DdlStorageToggle :database-type="props.databaseType" :disabled="ddlLoading || !!ddlError" />
<DialogFooter>
<div class="mr-auto flex items-center gap-2 text-sm text-muted-foreground">
<Switch id="ddl-refresh-on-open" size="sm" :model-value="settingsStore.editorSettings.refreshDdlOnOpen" @update:model-value="setRefreshDdlOnOpen" />
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { computed, createApp, nextTick, onActivated, onBeforeUnmount, ref, watch, type Component } from "vue";
import { RecycleScroller } from "vue-virtual-scroller";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
@@ -246,7 +249,8 @@ const tableInfoTab = ref<TableInfoTab>("ddl");
const tableColumns = ref<ColumnInfo[]>([]);
const tableColumnsLoading = ref(false);
const tableColumnsLoaded = ref(false);
const tableDdlContent = ref("");
const rawTableDdlContent = ref("");
const tableDdlContent = computed(() => applyDdlStoragePreference(rawTableDdlContent.value, effectiveDatabaseType.value, settingsStore.editorSettings.excludeDdlStorage));
const tableDdlLoading = ref(false);
const tableDdlLoaded = ref(false);
const tableIndexes = ref<IndexInfo[]>([]);
@@ -1134,7 +1138,7 @@ async function openTableInfo(row: ObjectBrowserRow, initialTab?: TableInfoTab) {
sidePanelGuard.bump();
// Reset state
tableColumns.value = [];
tableDdlContent.value = "";
rawTableDdlContent.value = "";
tableIndexes.value = [];
tableForeignKeys.value = [];
tableTriggers.value = [];
@@ -1185,11 +1189,11 @@ async function fetchTableDdl(force = settingsStore.editorSettings.refreshDdlOnOp
const { ddl } = await loadObjectDdl(tableMetadataRequest(row), { force });
if (sidePanelGuard.isStale(epoch)) return;
const formatDialect = sqlFormatDialectForDbType(effectiveDatabaseType.value);
tableDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect);
rawTableDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? ddl : omitDdlIdentifierQuotes(ddl, formatDialect);
loadedSuccessfully = true;
} catch (e: any) {
if (sidePanelGuard.isStale(epoch)) return;
tableDdlContent.value = `-- Error: ${e?.message || e}`;
rawTableDdlContent.value = `-- Error: ${e?.message || e}`;
} finally {
if (sidePanelGuard.isFresh(epoch)) {
tableDdlLoaded.value = loadedSuccessfully;
@@ -1323,7 +1327,7 @@ async function refreshActiveTableInfo() {
sidePanelGuard.bump();
if (tableInfoTab.value === "ddl") {
tableDdlContent.value = "";
rawTableDdlContent.value = "";
tableDdlLoaded.value = false;
await fetchTableDdl(true);
} else if (tableInfoTab.value === "columns") {
@@ -2090,7 +2094,7 @@ async function exportStructure(row: ObjectBrowserRow) {
try {
const schema = row.schema || selectedSchema.value || props.database;
const ddl = await api.getTableDdl(props.connection.id, props.database, schema, row.name, tableDdlObjectType(row.type), props.catalog, true);
await saveFileContent(buildSingleDdlExportFileContent(ddl), `${row.name}.sql`, "SQL", "sql");
await saveFileContent(buildSingleDdlExportFileContent(applyDdlStoragePreference(ddl, effectiveDatabaseType.value, settingsStore.editorSettings.excludeDdlStorage)), `${row.name}.sql`, "SQL", "sql");
} catch (e: any) {
console.error("Export structure failed:", e);
}
@@ -3790,6 +3794,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': activeTableInfoLoading }" />
</Button>
</div>
<div v-if="tableInfoTab === 'ddl' && effectiveDatabaseType === 'oceanbase-oracle'" class="border-b px-3 py-2">
<DdlStorageToggle :database-type="effectiveDatabaseType" :disabled="tableDdlLoading" />
</div>
<div v-if="tableInfoTab === 'columns'" class="flex-1 min-h-0 overflow-auto">
<div v-if="tableColumnsLoading" class="h-full flex items-center justify-center">
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
@@ -30,7 +30,7 @@ describe("ObjectBrowser table metadata refresh", () => {
expect(refreshTableInfo).toContain('sidePanelMode.value !== "table-info" || !sidePanelRow.value');
expect(refreshTableInfo).toContain("sidePanelGuard.bump();");
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "ddl"[\s\S]*?tableDdlContent\.value = "";[\s\S]*?await fetchTableDdl\(true\);/);
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "ddl"[\s\S]*?rawTableDdlContent\.value = "";[\s\S]*?await fetchTableDdl\(true\);/);
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "columns"[\s\S]*?tableColumns\.value = \[\];[\s\S]*?await fetchTableColumns\(true\);/);
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "indexes"[\s\S]*?tableIndexes\.value = \[\];[\s\S]*?await fetchTableIndexes\(true\);/);
expect(refreshTableInfo).toMatch(/tableInfoTab\.value === "foreignKeys"[\s\S]*?tableForeignKeys\.value = \[\];[\s\S]*?await fetchTableForeignKeys\(true\);/);
@@ -1,4 +1,6 @@
<script setup lang="ts">
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { structurePreviewHasOceanBase } from "./sidebarTreeDialogState";
import { computed, toRefs, watch } from "vue";
import { AlertTriangle, Check, Loader2, Clipboard, Plus, Trash2, Upload } from "@lucide/vue";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
@@ -433,6 +435,7 @@ watch(
<pre v-else class="max-h-[56vh] min-h-64 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(structurePreviewSql)"></pre>
</div>
<DialogFooter>
<DdlStorageToggle :database-type="structurePreviewHasOceanBase ? 'oceanbase-oracle' : undefined" :disabled="isLoadingStructurePreview || !!structurePreviewError" class="mr-auto" />
<Button variant="outline" @click="showStructurePreviewDialog = false">{{ t("dangerDialog.cancel") }}</Button>
<Button variant="outline" :disabled="isLoadingStructurePreview || !structurePreviewSql" @click="copyStructurePreview">
<Clipboard class="h-4 w-4" />
@@ -29,6 +29,7 @@ export const showBatchTruncateConfirm = ref(false);
export const showStructurePreviewDialog = ref(false);
export const showStructureDocCopyDialog = ref(false);
export const structurePreviewSql = ref("");
export const structurePreviewHasOceanBase = ref(false);
export const structurePreviewTitle = ref("");
export const structurePreviewDefaultFileName = ref("structure.sql");
export const structurePreviewError = ref("");
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, shallowRef, watch } from "vue";
import { uuid } from "@/lib/common/utils";
import { useI18n } from "vue-i18n";
@@ -173,7 +176,9 @@ const indexesLoading = ref(false);
const foreignKeysLoading = ref(false);
const constraintsLoading = ref(false);
const triggersLoading = ref(false);
const ddlContent = ref("");
const rawDdlContent = ref("");
const ddlStorageExcluded = ref(settingsStore.editorSettings.excludeDdlStorage);
const ddlContent = computed(() => applyDdlStoragePreference(rawDdlContent.value, databaseType.value, ddlStorageExcluded.value));
const ddlLoading = ref(false);
const ddlEditorContainer = ref<HTMLDivElement>();
const ddlSearchPanelRef = ref<InstanceType<typeof EditorSearchPanel>>();
@@ -194,6 +199,12 @@ const ddlDraft = ref<string | null>(null);
*/
const ddlEditingEnabled = computed(() => !isCreateMode.value && !!ddlContent.value.trim());
const ddlDirty = computed(() => ddlDraft.value !== null && ddlDraft.value.trim() !== ddlContent.value.trim());
watch([() => settingsStore.editorSettings.excludeDdlStorage, ddlDirty], ([exclude, dirty]) => {
if (!dirty && ddlStorageExcluded.value !== exclude) {
ddlDraft.value = null;
ddlStorageExcluded.value = exclude;
}
});
function ddlEditorDocument(): string {
return ddlDraft.value ?? (ddlContent.value || t("structureEditor.emptyReadonly"));
@@ -348,10 +359,10 @@ 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);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
ddlFetched.value = true;
} catch (e: any) {
ddlContent.value = `-- Error: ${e?.message || e}`;
rawDdlContent.value = `-- Error: ${e?.message || e}`;
ddlFetched.value = true;
} finally {
ddlLoading.value = false;
@@ -1394,6 +1405,8 @@ function createCurrentDraft(initialized = true): TableStructureEditorDraft {
// Only carried alongside an actual edit: without a draft the baseline is
// refetched, and copying every table's DDL into every draft is pure weight.
ddlContent: ddlDraft.value === null ? undefined : ddlContent.value,
rawDdlContent: ddlDraft.value === null ? undefined : rawDdlContent.value,
excludeDdlStorage: ddlStorageExcluded.value,
newTableName: newTableName.value,
tableComment: tableComment.value,
originalTableComment: originalTableComment.value,
@@ -1433,7 +1446,8 @@ function restoreDraft(draft: TableStructureEditorDraft) {
// Restore the DDL baseline alongside the edit, otherwise the restored script
// would read as dirty (or clean) against the wrong reference text.
if (draft.ddlContent) {
ddlContent.value = draft.ddlContent;
rawDdlContent.value = draft.rawDdlContent ?? draft.ddlContent;
ddlStorageExcluded.value = draft.excludeDdlStorage ?? false;
ddlFetched.value = true;
}
ddlDraft.value = draft.ddlDraft ?? null;
@@ -1504,7 +1518,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);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
ddlFetched.value = true;
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
} catch {
@@ -1835,7 +1849,7 @@ function resetState() {
triggers.value = [];
triggersLoaded.value = false;
clearColumnSelection();
ddlContent.value = "";
rawDdlContent.value = "";
ddlDraft.value = null;
ddlFetched.value = false;
loadedMetadataFacets.clear();
@@ -2164,7 +2178,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);
ddlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
rawDdlContent.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers ? formatted : omitDdlIdentifierQuotes(formatted, dialect);
ddlFetched.value = true;
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
} catch {
@@ -3773,7 +3787,7 @@ async function applyChanges() {
warnings.value = [];
sqliteSchemaRevision.value = undefined;
ddlFetched.value = false;
ddlContent.value = "";
rawDdlContent.value = "";
ddlDraft.value = null;
if (isCreateMode.value) {
clearDraft();
@@ -5179,6 +5193,7 @@ watch(
</div>
<template v-else>
<div v-if="ddlContent && !ddlSearchOpen" class="absolute right-3 top-3 z-10 flex items-center gap-1.5">
<DdlStorageToggle :database-type="databaseType" :disabled="ddlDirty" />
<Button v-if="ddlDirty" variant="outline" size="sm" class="h-7 gap-1 px-2" :title="t('structureEditor.resetDdl')" @click="resetDdlDraft">
<RotateCcw class="h-3.5 w-3.5" />
{{ t("structureEditor.resetDdl") }}
@@ -1,6 +1,6 @@
// @vitest-environment happy-dom
import { createApp, nextTick, ref, type App } from "vue";
import { createApp, nextTick, reactive, ref, type App } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
@@ -25,6 +25,7 @@ const mocks = vi.hoisted(() => ({
getTablePartitionStatus: vi.fn(),
getTableOwner: vi.fn(),
toast: vi.fn(),
copyToClipboard: vi.fn(),
}));
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
@@ -47,6 +48,7 @@ vi.mock("@lucide/vue", async () => {
Maximize2: Icon,
Plus: Icon,
RefreshCw: Icon,
RotateCcw: Icon,
Save: Icon,
Search: Icon,
Settings: Icon,
@@ -174,12 +176,11 @@ vi.mock("@/stores/connectionStore", () => ({
vi.mock("@/stores/productionSafetyStore", () => ({ useProductionSafetyStore: () => ({ requestConfirmation: vi.fn() }) }));
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({ tableStructureRefreshVersion: () => 0 }) }));
vi.mock("@/stores/historyStore", () => ({ useHistoryStore: () => ({ add: vi.fn() }) }));
const settings = reactive({ editorSettings: { structureEditorDensity: "compact", sqlFormatter: {}, tableColumnTemplateFields: [], theme: "default", fontSize: 13, fontFamily: "monospace", generateSqlQuoteIdentifiers: true, excludeDdlStorage: true } });
vi.mock("@/stores/settingsStore", () => ({
useSettingsStore: () => ({
editorSettings: { structureEditorDensity: "compact", sqlFormatter: {}, tableColumnTemplateFields: [], theme: "default", fontSize: 13, fontFamily: "monospace", generateSqlQuoteIdentifiers: true },
updateEditorSettings: vi.fn(),
}),
useSettingsStore: () => ({ ...settings, updateEditorSettings: (patch: object) => Object.assign(settings.editorSettings, patch) }),
}));
vi.mock("@/lib/common/clipboard", () => ({ copyToClipboard: mocks.copyToClipboard }));
vi.mock("@/composables/useTheme", () => ({ useTheme: () => ({ isDark: ref(false), themePalette: ref({}) }) }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
vi.mock("@/lib/sql/sqlHighlighter", () => ({ createShikiSqlHighlighter: vi.fn(async () => (sql: string) => sql) }));
@@ -231,6 +232,8 @@ function selectTab(button: HTMLButtonElement) {
beforeEach(() => {
vi.clearAllMocks();
mocks.connection.db_type = "postgres";
settings.editorSettings.excludeDdlStorage = true;
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.executeQuery.mockResolvedValue({ columns: [], rows: [] });
mocks.listDataTypes.mockResolvedValue([]);
@@ -252,6 +255,37 @@ afterEach(() => {
});
describe("TableStructureEditor DDL tab lifecycle", () => {
it("toggles OceanBase storage, copies the displayed DDL and protects an edited draft", async () => {
mocks.connection.db_type = "oceanbase-oracle";
const ddl = 'CREATE TABLE "T" ("ID" NUMBER) PCTFREE=0 PARTITION BY HASH("ID") PARTITIONS 2;';
mocks.loadObjectDdl.mockResolvedValue({ ddl, cacheStatus: "remote" });
const root = document.createElement("div");
document.body.append(root);
app = createApp(TableStructureEditor, { connectionId: mocks.connection.id, database: "APP", schema: "APP", tableName: "T", initialTab: "ddl" });
app.mount(root);
const content = () => root.querySelector(".cm-content")?.textContent ?? "";
const toggle = () => root.querySelector<HTMLButtonElement>('[role="switch"]')!;
await vi.waitFor(() => expect(content()).toContain("PARTITION BY HASH"));
expect(content()).not.toContain("PCTFREE");
expect(toggle().getAttribute("aria-checked")).toBe("true");
toggle().click();
await vi.waitFor(() => expect(content()).toContain("PCTFREE"));
buttonWithText(root, "grid.copyDdl").click();
await vi.waitFor(() => expect(mocks.copyToClipboard).toHaveBeenLastCalledWith(content()));
const { EditorView } = await import("@codemirror/view");
const view = EditorView.findFromDOM(root.querySelector(".cm-content")!)!;
view.dispatch({ changes: { from: view.state.doc.length, insert: "\n-- local edit" } });
await nextTick();
expect(toggle().disabled).toBe(true);
settings.editorSettings.excludeDdlStorage = true;
await nextTick();
expect(content()).toContain("PCTFREE");
expect(content()).toContain("-- local edit");
buttonWithText(root, "structureEditor.resetDdl").click();
await vi.waitFor(() => expect(content()).not.toContain("PCTFREE"));
expect(content()).not.toContain("-- local edit");
expect(mocks.loadObjectDdl).toHaveBeenCalledTimes(1);
});
it("restores cached DDL after switching to columns and back", async () => {
const root = document.createElement("div");
document.body.append(root);