fix(oceanbase): preserve partition DDL and toggle storage options
This commit is contained in:
1 parent
8eba12ea2f
commit
fe0dd02f2b
36 files changed
+870
-125
No files matched your search
@@ -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") }}
|
||||
|
||||
Vendored
+39
-5
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user