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
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
+1
-1
@@ -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") }}
|
||||
|
||||
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);
|
||||
|
||||
+136
-3
@@ -1,10 +1,11 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { shallowRef } from "vue";
|
||||
import { shallowRef, reactive, nextTick, effectScope } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ColumnInfo, TreeNode } from "@/types/database";
|
||||
|
||||
const source = readFileSync(new URL("../useSidebarTreeExportRuntime.ts", import.meta.url), "utf8");
|
||||
const toastMock = vi.hoisted(() => vi.fn());
|
||||
const copyToClipboardMock = vi.hoisted(() => vi.fn());
|
||||
const addExportTaskMock = vi.hoisted(() => vi.fn());
|
||||
const updateTableExportTaskMock = vi.hoisted(() => vi.fn());
|
||||
const apiMock = vi.hoisted(() => ({
|
||||
@@ -20,6 +21,7 @@ const apiMock = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
vi.mock("@/lib/common/clipboard", () => ({ copyToClipboard: copyToClipboardMock }));
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: toastMock }) }));
|
||||
vi.mock("@/composables/useExportTracker", () => ({ useExportTracker: () => ({ addTask: addExportTaskMock, updateTableExportTask: updateTableExportTaskMock }) }));
|
||||
@@ -36,7 +38,7 @@ vi.mock("vue-i18n", () => ({
|
||||
|
||||
import { useSidebarTreeExportRuntime } from "@/composables/useSidebarTreeExportRuntime";
|
||||
import { DEFAULT_DATA_GRID_EXTRACTOR_OPTIONS } from "@/lib/dataGrid/dataGridCopyExtractor";
|
||||
import { showStructurePreviewDialog, structurePreviewSql, structurePreviewTitle } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
import { isLoadingStructurePreview, showStructurePreviewDialog, structurePreviewDefaultFileName, structurePreviewError, structurePreviewSql, structurePreviewTitle } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
|
||||
function functionBody(name: string): string {
|
||||
const signature = new RegExp(`(?:async\\s+)?function\\s+${name}\\s*\\([^)]*\\)\\s*(?::\\s*[^\\{]+)?\\{`, "m").exec(source);
|
||||
@@ -84,9 +86,44 @@ function exportSettings() {
|
||||
};
|
||||
}
|
||||
|
||||
function structureRuntimeFixture(databaseType: "mysql" | "oceanbase-oracle" = "mysql") {
|
||||
const scope = effectScope();
|
||||
const firstNode = { id: "first-table", type: "table", label: "A", connectionId: "conn-1", database: "db", schema: "app" } as TreeNode;
|
||||
const secondNode = { ...firstNode, id: "second-table", label: "B" } as TreeNode;
|
||||
const activeNode = shallowRef(firstNode);
|
||||
const settingsStore = reactive({ editorSettings: { excludeDdlStorage: true } });
|
||||
const connectionStore = { ensureConnected: vi.fn(), getConfig: () => ({ db_type: databaseType }), treeNodes: [firstNode, secondNode], selectedTreeNodeIds: [] };
|
||||
const runtime = scope.run(() => useSidebarTreeExportRuntime({ activeNode, connectionStore: connectionStore as never, settingsStore: settingsStore as never, acceptedSelectionIds: () => null }))!;
|
||||
return { scope, activeNode, secondNode, settingsStore, runtime };
|
||||
}
|
||||
|
||||
describe("useSidebarTreeExportRuntime", () => {
|
||||
it("toggles OceanBase structure exports from the original DDL without querying again", async () => {
|
||||
const scope = effectScope();
|
||||
const ddl = 'CREATE TABLE "T" ("ID" NUMBER) REPLICA_NUM=1 PCTFREE=0 PARTITION BY HASH("ID") PARTITIONS 2';
|
||||
apiMock.getTableDdl.mockResolvedValue(ddl);
|
||||
const node = { id: "ob-table", type: "table", label: "T", connectionId: "ob", database: "SYS", schema: "APP" } as TreeNode;
|
||||
const settingsStore = reactive({ editorSettings: { excludeDdlStorage: true } });
|
||||
const connectionStore = { ensureConnected: vi.fn(), getConfig: () => ({ db_type: "oceanbase-oracle" }), treeNodes: [node], selectedTreeNodeIds: [] };
|
||||
const runtime = scope.run(() => useSidebarTreeExportRuntime({ activeNode: shallowRef(node), connectionStore: connectionStore as never, settingsStore: settingsStore as never, acceptedSelectionIds: () => null }))!;
|
||||
try {
|
||||
await runtime.exportStructure();
|
||||
expect(structurePreviewSql.value).not.toContain("REPLICA_NUM");
|
||||
expect(structurePreviewSql.value).toContain('PARTITION BY HASH("ID") PARTITIONS 2');
|
||||
settingsStore.editorSettings.excludeDdlStorage = false;
|
||||
await nextTick();
|
||||
expect(structurePreviewSql.value).toBe(ddl + ";\n");
|
||||
settingsStore.editorSettings.excludeDdlStorage = true;
|
||||
await nextTick();
|
||||
expect(structurePreviewSql.value).not.toContain("PCTFREE");
|
||||
expect(apiMock.getTableDdl).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
scope.stop();
|
||||
}
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiMock.getTableDdl.mockReset();
|
||||
addExportTaskMock.mockImplementation((tableName: string, format: string, filePath: string) => ({
|
||||
exportId: "export-1",
|
||||
tableName,
|
||||
@@ -102,6 +139,101 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
structurePreviewTitle.value = "";
|
||||
});
|
||||
|
||||
it.each(["older-first", "newer-first"])("isolates overlapping structure requests completed %s", async (order) => {
|
||||
const { scope, activeNode, secondNode, runtime } = structureRuntimeFixture();
|
||||
const firstDdl = deferred<string>();
|
||||
const secondDdl = deferred<string>();
|
||||
apiMock.getTableDdl.mockReturnValueOnce(firstDdl.promise).mockReturnValueOnce(secondDdl.promise);
|
||||
const firstRequest = runtime.exportStructure();
|
||||
await vi.waitFor(() => expect(apiMock.getTableDdl).toHaveBeenCalledTimes(1));
|
||||
activeNode.value = secondNode;
|
||||
const secondRequest = runtime.exportStructure();
|
||||
await vi.waitFor(() => expect(apiMock.getTableDdl).toHaveBeenCalledTimes(2));
|
||||
try {
|
||||
if (order === "older-first") {
|
||||
firstDdl.resolve("CREATE TABLE A(id INT)");
|
||||
await firstRequest;
|
||||
expect(isLoadingStructurePreview.value).toBe(true);
|
||||
expect(structurePreviewSql.value).toBe("");
|
||||
secondDdl.resolve("CREATE TABLE B(id INT)");
|
||||
await secondRequest;
|
||||
} else {
|
||||
secondDdl.resolve("CREATE TABLE B(id INT)");
|
||||
await secondRequest;
|
||||
firstDdl.resolve("CREATE TABLE A(id INT)");
|
||||
await firstRequest;
|
||||
}
|
||||
expect(structurePreviewSql.value).toBe("CREATE TABLE B(id INT);\n");
|
||||
expect(structurePreviewDefaultFileName.value).toBe("B.sql");
|
||||
expect(isLoadingStructurePreview.value).toBe(false);
|
||||
await runtime.copyStructurePreview();
|
||||
expect(copyToClipboardMock).toHaveBeenLastCalledWith("CREATE TABLE B(id INT);\n");
|
||||
} finally {
|
||||
firstDdl.resolve("CREATE TABLE A(id INT)");
|
||||
secondDdl.resolve("CREATE TABLE B(id INT)");
|
||||
await Promise.all([firstRequest, secondRequest]);
|
||||
scope.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["close", "dispose"])("ignores pending structure responses after %s", async (action) => {
|
||||
const { scope, runtime } = structureRuntimeFixture();
|
||||
const pendingDdl = deferred<string>();
|
||||
apiMock.getTableDdl.mockReturnValueOnce(pendingDdl.promise);
|
||||
const pendingRequest = runtime.exportStructure();
|
||||
await vi.waitFor(() => expect(apiMock.getTableDdl).toHaveBeenCalledOnce());
|
||||
if (action === "close") showStructurePreviewDialog.value = false;
|
||||
else scope.stop();
|
||||
pendingDdl.resolve("CREATE TABLE A(id INT)");
|
||||
try {
|
||||
await pendingRequest;
|
||||
expect(structurePreviewSql.value).toBe("");
|
||||
expect(isLoadingStructurePreview.value).toBe(false);
|
||||
} finally {
|
||||
scope.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores an older failure after another structure request succeeds", async () => {
|
||||
const { scope, activeNode, secondNode, runtime } = structureRuntimeFixture();
|
||||
const firstDdl = deferred<string>();
|
||||
apiMock.getTableDdl.mockReturnValueOnce(firstDdl.promise).mockResolvedValueOnce("CREATE TABLE B(id INT)");
|
||||
const firstRequest = runtime.exportStructure();
|
||||
await vi.waitFor(() => expect(apiMock.getTableDdl).toHaveBeenCalledOnce());
|
||||
activeNode.value = secondNode;
|
||||
await runtime.exportStructure();
|
||||
firstDdl.reject(new Error("old request failed"));
|
||||
try {
|
||||
await firstRequest;
|
||||
expect(structurePreviewSql.value).toBe("CREATE TABLE B(id INT);\n");
|
||||
expect(structurePreviewError.value).toBe("");
|
||||
expect(isLoadingStructurePreview.value).toBe(false);
|
||||
} finally {
|
||||
scope.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the latest storage preference when a structure request completes", async () => {
|
||||
const { scope, settingsStore, runtime } = structureRuntimeFixture("oceanbase-oracle");
|
||||
const pendingDdl = deferred<string>();
|
||||
const ddl = "CREATE TABLE A(id INT) PCTFREE=0";
|
||||
apiMock.getTableDdl.mockReturnValueOnce(pendingDdl.promise);
|
||||
const pendingRequest = runtime.exportStructure();
|
||||
await vi.waitFor(() => expect(apiMock.getTableDdl).toHaveBeenCalledOnce());
|
||||
settingsStore.editorSettings.excludeDdlStorage = false;
|
||||
pendingDdl.resolve(ddl);
|
||||
try {
|
||||
await pendingRequest;
|
||||
expect(structurePreviewSql.value).toBe(ddl + ";\n");
|
||||
settingsStore.editorSettings.excludeDdlStorage = true;
|
||||
await nextTick();
|
||||
expect(structurePreviewSql.value).not.toContain("PCTFREE");
|
||||
expect(apiMock.getTableDdl).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
scope.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("translates direct executeQuery errors for sidebar JSON export", async () => {
|
||||
apiMock.executeQuery.mockRejectedValueOnce(new Error("The previous DuckDB query is still stopping. Please try again shortly."));
|
||||
const activeNode = shallowRef({ id: "table-1", type: "table", label: "users", connectionId: "conn-1", database: "db", schema: "main", children: [] } as TreeNode);
|
||||
@@ -201,13 +333,14 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
const activeNode = shallowRef(first);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "postgres" })),
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [second.id, first.id],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: {} as never,
|
||||
settingsStore: exportSettings() as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createApp, type ShallowRef } from "vue";
|
||||
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import { watch, createApp, getCurrentScope, onScopeDispose, type ShallowRef } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import i18n from "@/i18n";
|
||||
import { useExportTracker, type ExportTask } from "@/composables/useExportTracker";
|
||||
@@ -18,7 +19,18 @@ import { fetchTableDataForExport } from "@/lib/table/tableDataExport";
|
||||
import { forceCsvTextForTemporalColumns } from "@/lib/dataGrid/columnFormatter";
|
||||
import XlsxHeaderDialog from "@/components/export/XlsxHeaderDialog.vue";
|
||||
import { buildXlsxHeaderOverrides, hasXlsxHeaderComments, type XlsxExportOptions, type XlsxHeaderMode } from "@/lib/export/xlsxHeader";
|
||||
import { isLoadingStructurePreview, showStructureDocCopyDialog, showStructurePreviewDialog, structureDocCopyText, structureDocCopyTitle, structurePreviewDefaultFileName, structurePreviewError, structurePreviewSql, structurePreviewTitle } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
import {
|
||||
isLoadingStructurePreview,
|
||||
showStructureDocCopyDialog,
|
||||
showStructurePreviewDialog,
|
||||
structureDocCopyText,
|
||||
structureDocCopyTitle,
|
||||
structurePreviewDefaultFileName,
|
||||
structurePreviewError,
|
||||
structurePreviewSql,
|
||||
structurePreviewTitle,
|
||||
structurePreviewHasOceanBase,
|
||||
} from "@/components/sidebar/sidebarTreeDialogState";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
|
||||
type StructureCopyFormat = "tsv" | "markdown";
|
||||
@@ -126,28 +138,62 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
return sidebarTableDataExportTargets(activeNode.value, connectionStore.treeNodes, options.acceptedSelectionIds() ?? connectionStore.selectedTreeNodeIds);
|
||||
}
|
||||
|
||||
let structureSource: Array<{ ddl: string; databaseType: ReturnType<typeof effectiveDatabaseTypeForConnection> }> = [];
|
||||
let structureRequestId = 0;
|
||||
function invalidateStructureRequest() {
|
||||
structureRequestId++;
|
||||
structureSource = [];
|
||||
isLoadingStructurePreview.value = false;
|
||||
}
|
||||
watch(
|
||||
showStructurePreviewDialog,
|
||||
(visible) => {
|
||||
if (!visible) invalidateStructureRequest();
|
||||
},
|
||||
{ flush: "sync" },
|
||||
);
|
||||
if (getCurrentScope()) onScopeDispose(invalidateStructureRequest);
|
||||
function renderStructurePreview() {
|
||||
structurePreviewSql.value = joinExportedDdls(structureSource.map(({ ddl, databaseType }) => applyDdlStoragePreference(ddl, databaseType, settingsStore.editorSettings.excludeDdlStorage)));
|
||||
}
|
||||
watch(
|
||||
() => settingsStore.editorSettings.excludeDdlStorage,
|
||||
() => {
|
||||
if (showStructurePreviewDialog.value && structureSource.length) renderStructurePreview();
|
||||
},
|
||||
);
|
||||
|
||||
async function exportStructure() {
|
||||
const targets = structureExportTargets();
|
||||
if (!targets.length) return;
|
||||
const requestId = ++structureRequestId;
|
||||
const requestSource: typeof structureSource = [];
|
||||
isLoadingStructurePreview.value = true;
|
||||
structurePreviewError.value = "";
|
||||
structurePreviewSql.value = "";
|
||||
structurePreviewHasOceanBase.value = false;
|
||||
structureSource = [];
|
||||
structurePreviewTitle.value = targets.length === 1 ? t("contextMenu.exportStructurePreviewTitle", { name: targets[0]!.label }) : t("contextMenu.exportStructurePreviewTitleMultiple", { count: targets.length });
|
||||
structurePreviewDefaultFileName.value = targets.length === 1 ? `${targets[0]!.label}.sql` : "structures.sql";
|
||||
showStructurePreviewDialog.value = true;
|
||||
try {
|
||||
const parts: string[] = [];
|
||||
for (const target of targets) {
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
if (requestId !== structureRequestId) return;
|
||||
const ddl = await api.getTableDdl(target.connectionId, target.database, target.schema || target.database, target.label, tableDdlObjectTypeForNode(target.type), target.catalog, true);
|
||||
parts.push(ddl.trim());
|
||||
if (requestId !== structureRequestId) return;
|
||||
const databaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(target.connectionId));
|
||||
requestSource.push({ ddl, databaseType });
|
||||
}
|
||||
structurePreviewSql.value = joinExportedDdls(parts);
|
||||
structureSource = requestSource;
|
||||
structurePreviewHasOceanBase.value = requestSource.some(({ databaseType }) => databaseType === "oceanbase-oracle");
|
||||
renderStructurePreview();
|
||||
} catch (error: any) {
|
||||
if (requestId !== structureRequestId) return;
|
||||
structurePreviewError.value = error?.message || String(error);
|
||||
console.error("Export structure failed:", error);
|
||||
} finally {
|
||||
isLoadingStructurePreview.value = false;
|
||||
if (requestId === structureRequestId) isLoadingStructurePreview.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3367,6 +3367,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "DDL-ə bax",
|
||||
viewDdlLoading: "DDL yüklənir...",
|
||||
ddlCopied: "DDL kopyalandı",
|
||||
excludeDdlStorage: "Saxlama atributlarını çıxar",
|
||||
excludeDdlStorageHint: "OceanBase Oracle DDL-in göstərilməsinə, kopyalanmasına və strukturun ixracına tətbiq olunur. Sıxılma, replika və saxlama parametrlərini saxlamaq üçün söndürün. Bölmələr və məhdudiyyətlər həmişə saxlanılır.",
|
||||
refreshDdlOnOpen: "Hər açılışda yenilə",
|
||||
refreshDdlOnOpenHint: "Aktiv olduqda DDL hər açılışda verilənlər bazasından yenidən yüklənir; deaktiv olduqda keşdəki məlumatlara üstünlük verilir",
|
||||
dropObject: "Obyekti sil",
|
||||
|
||||
@@ -3537,6 +3537,8 @@ export default {
|
||||
viewDdl: "View DDL",
|
||||
viewDdlLoading: "Loading DDL...",
|
||||
ddlCopied: "DDL copied",
|
||||
excludeDdlStorage: "Exclude storage attributes",
|
||||
excludeDdlStorageHint: "Applies to OceanBase Oracle DDL display, copying and structure export. Disable to retain compression, replica and storage options. Partitions and constraints are preserved.",
|
||||
refreshDdlOnOpen: "Refresh each time it opens",
|
||||
refreshDdlOnOpenHint: "When enabled, DDL is reloaded from the database each time it opens; when disabled, cached data is preferred",
|
||||
dropObject: "Drop Object",
|
||||
|
||||
@@ -3389,6 +3389,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "Ver DDL",
|
||||
viewDdlLoading: "Cargando DDL...",
|
||||
ddlCopied: "DDL copiado",
|
||||
excludeDdlStorage: "Excluir atributos de almacenamiento",
|
||||
excludeDdlStorageHint: "Se aplica a la visualización y copia de DDL y a la exportación de estructuras de OceanBase Oracle. Desactiva esta opción para conservar los parámetros de compresión, réplicas y almacenamiento. Las particiones y restricciones se conservan siempre.",
|
||||
refreshDdlOnOpen: "Actualizar cada vez que se abra",
|
||||
refreshDdlOnOpenHint: "Si se activa, el DDL se vuelve a cargar de la base de datos cada vez que se abre; si se desactiva, se usan primero los datos en caché",
|
||||
dropObject: "Eliminar objeto",
|
||||
|
||||
@@ -3387,6 +3387,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "Visualizza DDL",
|
||||
viewDdlLoading: "Caricamento DDL...",
|
||||
ddlCopied: "DDL copiato",
|
||||
excludeDdlStorage: "Escludi attributi di archiviazione",
|
||||
excludeDdlStorageHint: "Si applica alla visualizzazione e alla copia del DDL e all'esportazione della struttura in OceanBase Oracle. Disattiva questa opzione per mantenere i parametri di compressione, replica e archiviazione. Le partizioni e i vincoli vengono sempre mantenuti.",
|
||||
refreshDdlOnOpen: "Aggiorna a ogni apertura",
|
||||
refreshDdlOnOpenHint: "Se attivo, il DDL viene ricaricato dal database a ogni apertura; se disattivo, vengono usati prima i dati memorizzati nella cache",
|
||||
dropObject: "Elimina Oggetto",
|
||||
|
||||
@@ -3625,6 +3625,8 @@ export default withEnglishFallback({
|
||||
instanceInfo: "インスタンス情報",
|
||||
viewDdlLoading: "DDLを読み込み中...",
|
||||
ddlCopied: "DDLをコピーしました",
|
||||
excludeDdlStorage: "ストレージ属性を除外",
|
||||
excludeDdlStorageHint: "OceanBase Oracle の DDL の表示・コピーと構造のエクスポートに適用されます。無効にすると、圧縮・レプリカ・ストレージのパラメーターを保持します。パーティションと制約は常に保持されます。",
|
||||
refreshDdlOnOpen: "開くたびに更新",
|
||||
refreshDdlOnOpenHint: "有効にすると開くたびにデータベースから DDL を再取得し、無効にするとキャッシュを優先して使用します",
|
||||
manageExtension: "拡張機能を管理...",
|
||||
|
||||
@@ -3291,6 +3291,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "DDL 보기",
|
||||
viewDdlLoading: "DDL을 불러오는 중...",
|
||||
ddlCopied: "DDL을 복사했습니다",
|
||||
excludeDdlStorage: "스토리지 속성 제외",
|
||||
excludeDdlStorageHint: "OceanBase Oracle의 DDL 표시, 복사 및 구조 내보내기에 적용됩니다. 끄면 압축, 복제본 및 스토리지 매개변수를 유지합니다. 파티션과 제약 조건은 항상 유지됩니다.",
|
||||
dropObject: "객체 삭제",
|
||||
dropView: "뷰 삭제",
|
||||
dropColumn: "컬럼 삭제",
|
||||
|
||||
@@ -3385,6 +3385,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "Ver DDL",
|
||||
viewDdlLoading: "Carregando DDL...",
|
||||
ddlCopied: "DDL copiado",
|
||||
excludeDdlStorage: "Excluir atributos de armazenamento",
|
||||
excludeDdlStorageHint: "Aplica-se à exibição e cópia de DDL e à exportação de estruturas do OceanBase Oracle. Desative esta opção para manter os parâmetros de compressão, réplicas e armazenamento. As partições e restrições são sempre mantidas.",
|
||||
refreshDdlOnOpen: "Atualizar sempre que abrir",
|
||||
refreshDdlOnOpenHint: "Quando ativado, o DDL é recarregado do banco de dados sempre que é aberto; quando desativado, os dados em cache têm prioridade",
|
||||
dropObject: "Remover Objeto",
|
||||
|
||||
@@ -3339,6 +3339,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "DDL'i Görüntüle",
|
||||
viewDdlLoading: "DDL yükleniyor...",
|
||||
ddlCopied: "DDL kopyalandı",
|
||||
excludeDdlStorage: "Depolama özelliklerini hariç tut",
|
||||
excludeDdlStorageHint: "OceanBase Oracle DDL görüntüleme, kopyalama ve yapı dışa aktarma işlemlerine uygulanır. Sıkıştırma, replika ve depolama parametrelerini korumak için kapatın. Bölümler ve kısıtlamalar her zaman korunur.",
|
||||
refreshDdlOnOpen: "Her açılışta yenile",
|
||||
refreshDdlOnOpenHint: "Etkinleştirildiğinde DDL her açılışta veritabanından yeniden yüklenir; kapatıldığında önbellekteki veri tercih edilir",
|
||||
dropObject: "Nesneyi Sil",
|
||||
|
||||
@@ -3451,6 +3451,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "查看 DDL",
|
||||
viewDdlLoading: "正在读取 DDL...",
|
||||
ddlCopied: "DDL 已复制",
|
||||
excludeDdlStorage: "去掉存储属性",
|
||||
excludeDdlStorageHint: "用于 OceanBase Oracle 的 DDL 显示、复制和结构导出。关闭后保留压缩、副本和存储参数;分区和约束始终保留。",
|
||||
refreshDdlOnOpen: "每次打开时刷新",
|
||||
refreshDdlOnOpenHint: "开启后每次打开都会从数据库重新读取;关闭时优先使用缓存",
|
||||
dropObject: "删除对象",
|
||||
|
||||
@@ -3389,6 +3389,8 @@ export default withEnglishFallback({
|
||||
viewDdl: "檢視 DDL",
|
||||
viewDdlLoading: "正在讀取 DDL...",
|
||||
ddlCopied: "DDL 已複製",
|
||||
excludeDdlStorage: "移除儲存屬性",
|
||||
excludeDdlStorageHint: "用於 OceanBase Oracle 的 DDL 顯示、複製和結構匯出。關閉後保留壓縮、副本和儲存參數;分割區和約束條件一律保留。",
|
||||
refreshDdlOnOpen: "每次開啟時重新整理",
|
||||
refreshDdlOnOpenHint: "開啟後每次開啟都會從資料庫重新讀取;關閉時優先使用快取",
|
||||
dropObject: "刪除物件",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyDdlStoragePreference } from "../ddlStorage";
|
||||
|
||||
const raw = `CREATE TABLE "APP"."SALES" (
|
||||
"ID" NUMBER NOT NULL,
|
||||
"REGION" VARCHAR2(30) DEFAULT 'PCTFREE = 20',
|
||||
CONSTRAINT "CK" CHECK ("ID" > 0)
|
||||
) COMPRESS FOR ARCHIVE REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0
|
||||
partition by range("ID") subpartition by list("REGION")
|
||||
(partition "P1" values less than (100) (subpartition "S1" values ('PCTFREE = 20')));
|
||||
CREATE INDEX "I" ON "APP"."SALES"("REGION") LOCAL;
|
||||
COMMENT ON TABLE "APP"."SALES" IS 'COMPRESS FOR ARCHIVE';`;
|
||||
|
||||
describe("OceanBase Oracle DDL storage preference", () => {
|
||||
it("omits table storage by default and preserves the semantic DDL and comments", () => {
|
||||
const logical = applyDdlStoragePreference(raw, "oceanbase-oracle");
|
||||
expect(logical).not.toContain("REPLICA_NUM");
|
||||
expect(logical).not.toContain("BLOCK_SIZE");
|
||||
expect(logical).not.toContain("TABLET_SIZE");
|
||||
expect(logical).not.toContain("PCTFREE = 0");
|
||||
expect(logical).toContain("DEFAULT 'PCTFREE = 20'");
|
||||
expect(logical.slice(logical.indexOf("partition by"))).toBe(raw.slice(raw.indexOf("partition by")));
|
||||
expect(logical).toContain('CONSTRAINT "CK" CHECK ("ID" > 0)');
|
||||
});
|
||||
it("returns the complete original when disabled and leaves other databases unchanged", () => {
|
||||
expect(applyDdlStoragePreference(raw, "oceanbase-oracle", false)).toBe(raw);
|
||||
expect(applyDdlStoragePreference(raw, "oracle")).toBe(raw);
|
||||
expect(applyDdlStoragePreference(raw, undefined)).toBe(raw);
|
||||
});
|
||||
it("preserves quoted identifiers, Oracle alternative strings, temporary-table semantics and comments", () => {
|
||||
const ddl = `CREATE GLOBAL TEMPORARY TABLE "PCTFREE" ("A" VARCHAR2(100) DEFAULT q'[can't ) PCTFREE=42]') /* PCTFREE=9 */ PCTFREE=0 ON COMMIT PRESERVE ROWS;`;
|
||||
expect(applyDdlStoragePreference(ddl, "oceanbase-oracle")).toBe(ddl.replace(" PCTFREE=0", " "));
|
||||
});
|
||||
it("filters each table without altering unknown attributes, non-table statements or incomplete SQL", () => {
|
||||
const ddl = `CREATE TABLE T(A NUMBER) PCTFREE 0 TABLE_MODE='QUEUING'; CREATE TABLE U(A NUMBER) NOCOMPRESS PCTFREE=10; ALTER TABLE T PCTFREE 20;`;
|
||||
expect(applyDdlStoragePreference(ddl, "oceanbase-oracle")).toBe(`CREATE TABLE T(A NUMBER) TABLE_MODE='QUEUING'; CREATE TABLE U(A NUMBER) ; ALTER TABLE T PCTFREE 20;`);
|
||||
const broken = `CREATE TABLE T(A VARCHAR2(100) DEFAULT 'unclosed) PCTFREE=10`;
|
||||
expect(applyDdlStoragePreference(broken, "oceanbase-oracle")).toBe(broken);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
/** Remove only known OceanBase table storage options, keeping the cached source intact. */
|
||||
export function applyDdlStoragePreference(sql: string, databaseType: DatabaseType | undefined, excludeStorage = true): string {
|
||||
if (databaseType !== "oceanbase-oracle" || !excludeStorage) return sql;
|
||||
const tokens = tokenizeSqlSemantic(sql, "oracle");
|
||||
if (tokens.some((token) => token.closed === false)) return sql;
|
||||
const removals: Array<{ start: number; end: number }> = [];
|
||||
let table = false;
|
||||
let options = false;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]!;
|
||||
if (token.depth !== 0) continue;
|
||||
if (token.text === ";") {
|
||||
table = options = false;
|
||||
continue;
|
||||
}
|
||||
if (token.kind === "word" && token.normalized === "create") {
|
||||
const offset = tokens[i + 1]?.normalized === "global" && tokens[i + 2]?.normalized === "temporary" ? 3 : 1;
|
||||
table = tokens[i + offset]?.normalized === "table";
|
||||
options = false;
|
||||
}
|
||||
if (!table) continue;
|
||||
if (token.text === ")") options = true;
|
||||
if (!options || token.kind !== "word") continue;
|
||||
// Partition expressions and bounds must never be treated as table options.
|
||||
if (token.normalized === "partition") {
|
||||
table = options = false;
|
||||
continue;
|
||||
}
|
||||
let last = i;
|
||||
if (token.normalized === "compress" && tokens[i + 1]?.normalized === "for" && tokens[i + 2]?.normalized === "archive") {
|
||||
last = i + 2;
|
||||
if (["high", "low"].includes(tokens[last + 1]?.normalized ?? "")) last++;
|
||||
} else if (token.normalized === "nocompress") {
|
||||
last = i;
|
||||
} else if (["replica_num", "block_size", "tablet_size", "pctfree", "use_bloom_filter"].includes(token.normalized)) {
|
||||
const valueIndex = tokens[i + 1]?.text === "=" ? i + 2 : i + 1;
|
||||
const value = tokens[valueIndex];
|
||||
const valid = token.normalized === "use_bloom_filter" ? value?.kind === "word" && ["true", "false"].includes(value.normalized) : value?.kind === "number" && /^\d+$/.test(value.text);
|
||||
if (!valid) continue;
|
||||
last = valueIndex;
|
||||
} else continue;
|
||||
removals.push({ start: token.span.start, end: tokens[last]!.span.end });
|
||||
i = last;
|
||||
}
|
||||
let result = sql;
|
||||
for (const { start, end } of removals.reverse()) result = result.slice(0, start) + result.slice(end);
|
||||
return result;
|
||||
}
|
||||
@@ -149,6 +149,15 @@ export function tokenizeSqlSemantic(input: string, dialectId = "mysql", options?
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dialectId === "oracle" && (ch === "q" || ch === "Q") && next === "'" && input[index + 2]) {
|
||||
const opener = input[index + 2]!;
|
||||
const closer = ({ "[": "]", "{": "}", "(": ")", "<": ">" } as Record<string, string>)[opener] ?? opener;
|
||||
const end = input.indexOf(closer + "'", index + 3);
|
||||
index = end < 0 ? input.length : end + 2;
|
||||
tokens.push(token("string", input.slice(start, index), start, index, depth, "q'", end >= 0));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
const quoted = readQuotedString(input, start, "'", mysqlBackslashEscape);
|
||||
index = quoted.end;
|
||||
|
||||
@@ -115,6 +115,24 @@ describe("connectionStore Xugu table child metadata", () => {
|
||||
expect(childTypes).toEqual(["group-columns", "group-fkeys", "group-triggers", "group-indexes"]);
|
||||
});
|
||||
|
||||
it("loads OceanBase Oracle partitions and subpartitions through the metadata tree", async () => {
|
||||
const partition = { name: "P1", position: 1, value: "100", partition_type: "RANGE", partition_key: '"ID"' };
|
||||
const subpartition = { name: "P1_E", position: 1, value: "'east'", partition_type: "LIST", partition_key: '"REGION"' };
|
||||
const { api, config, store, tableId } = await setup("oceanbase-oracle", {
|
||||
listPartitions: vi.fn().mockResolvedValue([partition]),
|
||||
listSubpartitions: vi.fn().mockResolvedValue([subpartition]),
|
||||
});
|
||||
const partitions = findNode(store.treeNodes, `${tableId}:__table-partitions`)!;
|
||||
const subpartitions = findNode(store.treeNodes, `${tableId}:__table-subpartitions`)!;
|
||||
expect(partitions).toBeDefined();
|
||||
expect(subpartitions).toBeDefined();
|
||||
await store.loadTreeNodeChildren(partitions);
|
||||
await store.loadTreeNodeChildren(subpartitions);
|
||||
expect(api.listPartitions).toHaveBeenCalledWith(config.id, "SHOP_DEMO", "SYSDBA", "SHOP_ORDERS", undefined);
|
||||
expect(partitions.children?.[0]).toMatchObject({ label: "P1 (RANGE: 100)", meta: partition });
|
||||
expect(subpartitions.children?.[0]).toMatchObject({ label: "P1_E (LIST: 'east')", meta: subpartition });
|
||||
});
|
||||
|
||||
it("routes Xugu child groups to their dedicated metadata calls, including empty results", async () => {
|
||||
const listConstraints = vi.fn().mockResolvedValue([
|
||||
{
|
||||
|
||||
@@ -6608,34 +6608,34 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
children: [],
|
||||
});
|
||||
}
|
||||
if (supportsXuguChildMetadata) {
|
||||
children.push(
|
||||
{
|
||||
id: `${parentId}:__table-partitions`,
|
||||
label: "tree.partitions",
|
||||
type: "group-table-partitions",
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
catalog,
|
||||
tableName: table,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: `${parentId}:__table-subpartitions`,
|
||||
label: "tree.subpartitions",
|
||||
type: "group-table-subpartitions",
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
catalog,
|
||||
tableName: table,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (supportsXuguChildMetadata || effectiveDbType === "oceanbase-oracle") {
|
||||
children.push(
|
||||
{
|
||||
id: `${parentId}:__table-partitions`,
|
||||
label: "tree.partitions",
|
||||
type: "group-table-partitions",
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
catalog,
|
||||
tableName: table,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
id: `${parentId}:__table-subpartitions`,
|
||||
label: "tree.subpartitions",
|
||||
type: "group-table-subpartitions",
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
catalog,
|
||||
tableName: table,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -775,6 +775,7 @@ export interface EditorSettings {
|
||||
wordWrap: boolean;
|
||||
tableDdlWordWrap: boolean;
|
||||
refreshDdlOnOpen: boolean;
|
||||
excludeDdlStorage: boolean;
|
||||
vimModeEnabled: boolean;
|
||||
autoCloseBrackets: boolean;
|
||||
sqlSemanticDiagnosticsMode: SqlSemanticDiagnosticsMode;
|
||||
@@ -1019,6 +1020,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
wordWrap: false,
|
||||
tableDdlWordWrap: true,
|
||||
refreshDdlOnOpen: false,
|
||||
excludeDdlStorage: true,
|
||||
vimModeEnabled: false,
|
||||
autoCloseBrackets: true,
|
||||
sqlSemanticDiagnosticsMode: "auto",
|
||||
@@ -1501,6 +1503,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
selectFirstCompletionOnOpen: typeof settings.selectFirstCompletionOnOpen === "boolean" ? settings.selectFirstCompletionOnOpen : DEFAULT_EDITOR_SETTINGS.selectFirstCompletionOnOpen,
|
||||
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
||||
tableDdlWordWrap: typeof settings.tableDdlWordWrap === "boolean" ? settings.tableDdlWordWrap : DEFAULT_EDITOR_SETTINGS.tableDdlWordWrap,
|
||||
excludeDdlStorage: typeof settings.excludeDdlStorage === "boolean" ? settings.excludeDdlStorage : DEFAULT_EDITOR_SETTINGS.excludeDdlStorage,
|
||||
refreshDdlOnOpen: typeof settings.refreshDdlOnOpen === "boolean" ? settings.refreshDdlOnOpen : DEFAULT_EDITOR_SETTINGS.refreshDdlOnOpen,
|
||||
vimModeEnabled: typeof settings.vimModeEnabled === "boolean" ? settings.vimModeEnabled : DEFAULT_EDITOR_SETTINGS.vimModeEnabled,
|
||||
autoCloseBrackets: typeof settings.autoCloseBrackets === "boolean" ? settings.autoCloseBrackets : DEFAULT_EDITOR_SETTINGS.autoCloseBrackets,
|
||||
@@ -2264,6 +2267,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.selectFirstCompletionOnOpen !== undefined) editorSettings.value.selectFirstCompletionOnOpen = partial.selectFirstCompletionOnOpen === true;
|
||||
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
|
||||
if (partial.tableDdlWordWrap !== undefined) editorSettings.value.tableDdlWordWrap = partial.tableDdlWordWrap === true;
|
||||
if (partial.excludeDdlStorage !== undefined) editorSettings.value.excludeDdlStorage = partial.excludeDdlStorage === true;
|
||||
if (partial.refreshDdlOnOpen !== undefined) editorSettings.value.refreshDdlOnOpen = partial.refreshDdlOnOpen === true;
|
||||
if (partial.vimModeEnabled !== undefined) editorSettings.value.vimModeEnabled = partial.vimModeEnabled === true;
|
||||
if (partial.autoCloseBrackets !== undefined) editorSettings.value.autoCloseBrackets = partial.autoCloseBrackets === true;
|
||||
|
||||
@@ -1499,6 +1499,9 @@ export interface TableStructureEditorDraft {
|
||||
activeTab: TableInfoTab;
|
||||
/** DDL as loaded from the database — the baseline `ddlDraft` is compared against. */
|
||||
ddlContent?: string;
|
||||
/** Original DDL and display preference retained so restoring a draft cannot change its baseline. */
|
||||
rawDdlContent?: string;
|
||||
excludeDdlStorage?: boolean;
|
||||
/** Edited DDL script, or null/undefined when the DDL tab was left untouched. */
|
||||
ddlDraft?: string | null;
|
||||
newTableName: string;
|
||||
|
||||
Reference in new issue
Block a user