feat(backup): support openGauss in scheduled database backups

This commit is contained in:
lxk955 authored and GitHub committed 2026-10-25 23:31:05 +08:00
1 parent be5f06fd60
commit bbe6522fd4
33 files changed
+218 -43

No files matched your search

@@ -4,7 +4,7 @@ import { useI18n } from "vue-i18n";
import * as api from "@/lib/backend/api";
import { Button } from "@/components/ui/button";
import TableMultiSelect from "@/components/diff/TableMultiSelect.vue";
import { databaseBackupTableSelectionScopeKey, databaseBackupTableTargetKey, type DatabaseBackupTableTarget, type DatabaseBackupTableSelectionState } from "@/lib/backup/scheduledDatabaseBackup";
import { databaseBackupTableSelectionScopeKey, databaseBackupTableTargetKey, supportsScheduledDatabaseBackup, type DatabaseBackupTableTarget, type DatabaseBackupTableSelectionState } from "@/lib/backup/scheduledDatabaseBackup";
const props = defineProps<{
connectionId: string;
@@ -55,7 +55,7 @@ async function loadTables() {
loading.value = !!connectionId && databases.length > 0;
if (!connectionId || databases.length === 0) return;
try {
if (databaseType !== "mysql" && databaseType !== "postgres") throw new Error("Unsupported backup connection");
if (!supportsScheduledDatabaseBackup(databaseType)) throw new Error("Unsupported backup connection");
const result = await Promise.all(
databases.map(async (database) => {
const schemas = databaseType === "mysql" ? [database] : (await api.listSchemas(connectionId, database)).filter((schema) => schema !== "information_schema" && !schema.startsWith("pg_"));
@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { HelpTooltip } from "@/components/ui/tooltip";
import { Check, ChevronDown, FolderOpen, Loader2, Search } from "@lucide/vue";
import ConnectionGroupBadge from "@/components/connection/ConnectionGroupBadge.vue";
import { databaseBackupFileNamePatternHasRunId, databaseBackupFileNamePatternIsValid, type DatabaseBackupExecutionConfig } from "@/lib/backup/scheduledDatabaseBackup";
import { databaseBackupFileNamePatternHasRunId, databaseBackupFileNamePatternIsValid, supportsNavicatBackupFormat, type DatabaseBackupExecutionConfig } from "@/lib/backup/scheduledDatabaseBackup";
import type { ConnectionConfig } from "@/types/database";
import BackupTableSelector from "./BackupTableSelector.vue";
import type { DatabaseBackupTableSelectionState } from "@/lib/backup/scheduledDatabaseBackup";
@@ -57,6 +57,13 @@ const selectedConnectionName = computed(() => props.connections.find((connection
const selectedConnectionType = computed(() => props.connections.find((connection) => connection.id === props.draft.connectionId)?.db_type);
// Database names and exact table lists belong to one connection, so they only apply when a single connection is selected.
const multipleConnectionsSelected = computed(() => props.multiSelectConnections && props.selectedConnectionIds.length > 1);
const selectedConnectionTypes = computed(() => {
const connectionIds = props.multiSelectConnections ? props.selectedConnectionIds : props.draft.connectionId ? [props.draft.connectionId] : [];
return connectionIds.map((connectionId) => props.connections.find((connection) => connection.id === connectionId)?.db_type);
});
const navicatFormatSupported = computed(() => supportsNavicatBackupFormat(selectedConnectionTypes.value));
// A saved schedule may still carry `.nb3` after its connection changed to an unsupported type.
const navicatFormatUnsupported = computed(() => props.draft.outputCompression === "nb3" && !navicatFormatSupported.value);
const connectionPickerLabel = computed(() => (multipleConnectionsSelected.value ? t("databaseBackup.connectionsSelected", { count: props.selectedConnectionIds.length }) : selectedConnectionName.value));
const fileNamePatternNeedsRunId = computed(() => multipleConnectionsSelected.value && !databaseBackupFileNamePatternHasRunId(props.draft.fileNamePattern));
const filteredConnections = computed(() => {
@@ -240,14 +247,15 @@ watch(
<div class="space-y-2">
<Label>{{ t("databaseBackup.backupFormat") }}</Label>
<Select v-model="draft.outputCompression">
<SelectTrigger data-backup-output-format class="w-full sm:w-65"><SelectValue /></SelectTrigger>
<SelectTrigger data-backup-output-format class="w-full sm:w-65" :aria-invalid="navicatFormatUnsupported"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="none">{{ t("databaseBackup.formatSql") }}</SelectItem>
<SelectItem value="gzip">{{ t("databaseBackup.compressionGzip") }}</SelectItem>
<SelectItem value="nb3">{{ t("databaseBackup.formatNb3") }}</SelectItem>
<SelectItem value="nb3" :disabled="!navicatFormatSupported">{{ t("databaseBackup.formatNb3") }}</SelectItem>
</SelectContent>
</Select>
<p v-if="draft.outputCompression === 'nb3'" data-backup-nb3-hint class="text-xs text-muted-foreground">{{ t("databaseBackup.formatNb3Hint") }}</p>
<p v-if="navicatFormatUnsupported" data-backup-nb3-unsupported class="text-xs text-destructive">{{ t("databaseBackup.formatNb3Unsupported") }}</p>
<p v-else-if="draft.outputCompression === 'nb3'" data-backup-nb3-hint class="text-xs text-muted-foreground">{{ t("databaseBackup.formatNb3Hint") }}</p>
</div>
<div v-if="multipleConnectionsSelected" data-backup-multi-connection-hint class="rounded-md border border-border/70 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
@@ -28,6 +28,7 @@ import {
nextDatabaseBackupRunAt,
normalizeDatabaseBackupRetention,
normalizeDatabaseBackupTablePatterns,
supportsNavicatBackupFormat,
supportsScheduledDatabaseBackup,
type DatabaseBackupExecutionConfig,
type DatabaseBackupFile,
@@ -297,6 +298,10 @@ function databaseLoadIsCurrent(generation: number, dialog: BackupDialogKind, tar
return scheduleDialogOpen.value && draft.value === targetDraft && targetDraft.connectionId === connectionId;
}
// `.nb3` is a MySQL/PostgreSQL-only format; keep a saved or newly selected value from being submitted for other connections.
const scheduleNavicatFormatSupported = computed(() => supportsNavicatBackupFormat([connectionStore.getConfig(draft.value.connectionId)?.db_type]));
const oneShotNavicatFormatSupported = computed(() => supportsNavicatBackupFormat(oneShotConnectionIds.value.map((connectionId) => connectionStore.getConfig(connectionId)?.db_type)));
const canSave = computed(() => {
const hasContent = draft.value.includeStructure || draft.value.includeData || draft.value.includeObjects;
const hasDatabaseScope = allDatabases.value || selectedDatabases.value.length > 0;
@@ -307,6 +312,7 @@ const canSave = computed(() => {
!!draft.value.destinationDirectory.trim() &&
databaseBackupRunDirectoryPatternIsValid(draft.value.runDirectoryPattern || "") &&
databaseBackupFileNamePatternIsValid(draft.value.fileNamePattern || "") &&
(draft.value.outputCompression !== "nb3" || scheduleNavicatFormatSupported.value) &&
hasContent &&
hasDatabaseScope &&
hasTableScope &&
@@ -354,8 +360,18 @@ const canStartOneShot = computed(() => {
const hasTableScope = tableScopeReady(oneShotDraft.value, oneShotTableSelectionState.value);
// Runs started together share a timestamp, so only {runId} keeps their files apart.
const hasUniqueFileNames = !multiple || databaseBackupFileNamePatternHasRunId(oneShotDraft.value.fileNamePattern);
const hasSupportedFormat = oneShotDraft.value.outputCompression !== "nb3" || oneShotNavicatFormatSupported.value;
return (
!!oneShotDraft.value.connectionId && !!oneShotDraft.value.destinationDirectory.trim() && databaseBackupFileNamePatternIsValid(oneShotDraft.value.fileNamePattern || "") && hasContent && hasDatabaseScope && hasTableScope && hasUniqueFileNames && !oneShotStarting.value && !loadingDatabases.value
!!oneShotDraft.value.connectionId &&
!!oneShotDraft.value.destinationDirectory.trim() &&
databaseBackupFileNamePatternIsValid(oneShotDraft.value.fileNamePattern || "") &&
hasContent &&
hasDatabaseScope &&
hasTableScope &&
hasUniqueFileNames &&
hasSupportedFormat &&
!oneShotStarting.value &&
!loadingDatabases.value
);
});
@@ -141,4 +141,15 @@ describe("exact backup table selection", () => {
expect(selection.value).toEqual([]);
expect(states.at(-1)?.ready).toBe(false);
});
it("loads schemas and tables for openGauss connections", async () => {
apiMock.listSchemas.mockResolvedValue(["public", "pg_catalog", "information_schema"]);
apiMock.listTables.mockResolvedValue([{ name: "users" }]);
const { scope, container } = mount();
scope.databaseType = "opengauss";
await settle();
expect(apiMock.listSchemas).toHaveBeenCalledWith("source", "app");
expect(apiMock.listTables).toHaveBeenCalledWith("source", "app", "public");
expect(container.textContent).toContain('"app"."public"."users"');
});
});
@@ -171,6 +171,27 @@ async function selectDialogOption(triggerIndex: number, optionText: string) {
await flush();
}
async function openDialogSelect(triggerIndex: number): Promise<HTMLElement[]> {
const trigger = currentDialog().querySelectorAll<HTMLButtonElement>('[data-slot="select-trigger"]')[triggerIndex];
if (!trigger) throw new Error(`Select trigger not found: ${triggerIndex}`);
trigger.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
await flush();
return Array.from(document.body.querySelectorAll<HTMLElement>('[role="option"]'));
}
function navicatFormatOption(options: HTMLElement[]): HTMLElement {
const label = String(i18n.global.t("databaseBackup.formatNb3"));
const option = options.find((item) => item.textContent?.trim() === label);
if (!option) throw new Error(`Format option not found: ${label}`);
return option;
}
async function chooseOpenSelectOption(option: HTMLElement) {
option.focus();
option.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
await flush();
}
async function setTablePattern(pattern: string) {
await selectDialogOption(TABLE_SCOPE_SELECT, String(i18n.global.t("databaseBackup.includeTables")));
const input = currentDialog().querySelector<HTMLInputElement>(`input[placeholder="${String(i18n.global.t("databaseBackup.tablePatternsPlaceholder"))}"]`);
@@ -1057,6 +1078,66 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
expect(document.body.textContent).toContain(String(i18n.global.t("databaseBackup.noSupportedConnections")));
});
it("enables create schedule when openGauss connection is available", async () => {
mocks.connections.push({ id: "og-1", name: "Local openGauss", db_type: "opengauss" });
await mountSettings();
expect(addScheduleButton().disabled).toBe(false);
});
it.each(["mysql", "postgres"] as const)("offers the Navicat backup format for %s connections", async (dbType) => {
mocks.connections.push({ id: `${dbType}-1`, name: `Local ${dbType}`, db_type: dbType });
await mountSettings();
addScheduleButton().click();
await flush();
const option = navicatFormatOption(await openDialogSelect(0));
expect(option.getAttribute("aria-disabled")).not.toBe("true");
expect(option.hasAttribute("data-disabled")).toBe(false);
await chooseOpenSelectOption(option);
expect(currentDialog().querySelector("[data-backup-nb3-hint]")).not.toBeNull();
expect(currentDialog().querySelector("[data-backup-nb3-unsupported]")).toBeNull();
});
it("disables the Navicat backup format for openGauss connections", async () => {
mocks.connections.push({ id: "og-1", name: "Local openGauss", db_type: "opengauss" });
await mountSettings();
addScheduleButton().click();
await flush();
const option = navicatFormatOption(await openDialogSelect(0));
expect(option.getAttribute("aria-disabled")).toBe("true");
expect(option.hasAttribute("data-disabled")).toBe(true);
await chooseOpenSelectOption(option);
const formatTrigger = currentDialog().querySelector("[data-backup-output-format]");
expect(formatTrigger?.textContent).toContain(String(i18n.global.t("databaseBackup.formatSql")));
expect(formatTrigger?.textContent).not.toContain(String(i18n.global.t("databaseBackup.formatNb3")));
});
it.each([
["none", false],
["nb3", true],
] as const)("validates a saved openGauss schedule using the %s format", async (outputCompression, unsupported) => {
mocks.connections.push({ id: "og-1", name: "Local openGauss", db_type: "opengauss" });
mocks.schedules.push(schedule({ connectionId: "og-1", databases: [], outputCompression }));
await mountSettings();
buttonWithTitle(String(i18n.global.t("databaseBackup.edit"))).click();
await flush();
const hint = currentDialog().querySelector("[data-backup-nb3-unsupported]");
expect(saveScheduleButton().disabled).toBe(unsupported);
if (unsupported) {
expect(hint?.textContent).toContain(String(i18n.global.t("databaseBackup.formatNb3Unsupported")));
expect(currentDialog().querySelector("[data-backup-output-format]")?.getAttribute("aria-invalid")).toBe("true");
expect(currentDialog().querySelector("[data-backup-output-format]")?.textContent).toContain(String(i18n.global.t("databaseBackup.formatNb3")));
} else {
expect(hint).toBeNull();
}
});
it("reconfirms a legacy destination before manually running its schedule", async () => {
mocks.schedules.push(schedule());
mocks.databaseExportDestinationNeedsConfirmation.mockResolvedValueOnce(true);
+1
View File
@@ -9158,6 +9158,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Navicat ehtiyat nüsxəsi (.nb3)",
formatNb3Hint: "MySQL və PostgreSQL üçün Navicat ehtiyat nüsxə formatı. DBX onu istənilən SQL ehtiyat nüsxəsi kimi bərpa edir.",
formatNb3Unsupported: "Navicat .nb3 ehtiyat nüsxələri yalnız MySQL və PostgreSQL bağlantılarını dəstəkləyir. SQL və ya Gzip seçin.",
restoreFromFile: "Fayldan bərpa et",
restoreFromFileHint: "İstənilən yerdən .sql, .sql.gz və ya .nb3 ehtiyat nüsxə faylını bərpa edin",
importBackup: "Ehtiyat nüsxəni idxal et",
+1
View File
@@ -11457,6 +11457,7 @@ export default {
formatSql: "SQL (.sql)",
formatNb3: "Navicat backup (.nb3)",
formatNb3Hint: "Navicat backup format for MySQL and PostgreSQL. DBX restores it like any SQL backup.",
formatNb3Unsupported: "Navicat .nb3 backups support MySQL and PostgreSQL connections only. Choose SQL or Gzip.",
restoreFromFile: "Restore from file",
restoreFromFileHint: "Restore a .sql, .sql.gz or .nb3 backup file from anywhere",
importBackup: "Import backup",
+1
View File
@@ -12365,6 +12365,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Copia de seguridad de Navicat (.nb3)",
formatNb3Hint: "Formato de copia de seguridad de Navicat para MySQL y PostgreSQL. DBX lo restaura como cualquier copia SQL.",
formatNb3Unsupported: "Las copias .nb3 de Navicat solo admiten conexiones MySQL y PostgreSQL. Elige SQL o Gzip.",
restoreFromFile: "Restaurar desde archivo",
restoreFromFileHint: "Restaura un archivo de copia de seguridad .sql, .sql.gz o .nb3 desde cualquier ubicación",
importBackup: "Importar copia de seguridad",
+1
View File
@@ -9995,6 +9995,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Cadangan Navicat (.nb3)",
formatNb3Hint: "Format cadangan Navicat untuk MySQL dan PostgreSQL. DBX memulihkannya seperti cadangan SQL lainnya.",
formatNb3Unsupported: "Cadangan .nb3 Navicat hanya mendukung koneksi MySQL dan PostgreSQL. Pilih SQL atau Gzip.",
restoreFromFile: "Pulihkan dari file",
restoreFromFileHint: "Pulihkan file cadangan .sql, .sql.gz, atau .nb3 dari lokasi mana pun",
importBackup: "Impor cadangan",
+1
View File
@@ -11906,6 +11906,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Backup Navicat (.nb3)",
formatNb3Hint: "Formato di backup di Navicat per MySQL e PostgreSQL. DBX lo ripristina come qualsiasi backup SQL.",
formatNb3Unsupported: "I backup .nb3 di Navicat supportano solo connessioni MySQL e PostgreSQL. Scegli SQL o Gzip.",
restoreFromFile: "Ripristina da file",
restoreFromFileHint: "Ripristina un file di backup .sql, .sql.gz o .nb3 da qualsiasi percorso",
importBackup: "Importa backup",
+1
View File
@@ -11945,6 +11945,7 @@ export default withEnglishFallback({
formatSql: "SQL(.sql)",
formatNb3: "Navicat バックアップ(.nb3)",
formatNb3Hint: "MySQL と PostgreSQL 向けの Navicat バックアップ形式です。DBX では通常の SQL バックアップと同様に復元できます。",
formatNb3Unsupported: "Navicat .nb3 バックアップは MySQL と PostgreSQL 接続でのみ使用できます。SQL または Gzip を選択してください。",
restoreFromFile: "ファイルから復元",
restoreFromFileHint: "任意の場所にある .sql、.sql.gz、.nb3 のバックアップファイルを復元します",
importBackup: "バックアップをインポート",
+1
View File
@@ -10236,6 +10236,7 @@ export default withEnglishFallback({
formatSql: "SQL(.sql)",
formatNb3: "Navicat 백업(.nb3)",
formatNb3Hint: "MySQL 및 PostgreSQL용 Navicat 백업 형식입니다. DBX에서는 일반 SQL 백업처럼 복원할 수 있습니다.",
formatNb3Unsupported: "Navicat .nb3 백업은 MySQL 및 PostgreSQL 연결에서만 지원됩니다. SQL 또는 Gzip을 선택하세요.",
restoreFromFile: "파일에서 복원",
restoreFromFileHint: "어느 위치에 있든 .sql, .sql.gz, .nb3 백업 파일을 복원합니다",
importBackup: "백업 가져오기",
+1
View File
@@ -10022,6 +10022,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Kopia zapasowa Navicat (.nb3)",
formatNb3Hint: "Format kopii zapasowej Navicat dla MySQL i PostgreSQL. DBX przywraca go jak każdą kopię SQL.",
formatNb3Unsupported: "Kopie zapasowe Navicat .nb3 są obsługiwane tylko dla połączeń MySQL i PostgreSQL. Wybierz SQL lub Gzip.",
restoreFromFile: "Przywróć z pliku",
restoreFromFileHint: "Przywróć plik kopii zapasowej .sql, .sql.gz lub .nb3 z dowolnego miejsca",
importBackup: "Importuj kopię zapasową",
+1
View File
@@ -11925,6 +11925,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Backup do Navicat (.nb3)",
formatNb3Hint: "Formato de backup do Navicat para MySQL e PostgreSQL. O DBX o restaura como qualquer backup SQL.",
formatNb3Unsupported: "Backups .nb3 do Navicat suportam apenas conexões MySQL e PostgreSQL. Escolha SQL ou Gzip.",
restoreFromFile: "Restaurar de arquivo",
restoreFromFileHint: "Restaure um arquivo de backup .sql, .sql.gz ou .nb3 de qualquer local",
importBackup: "Importar backup",
+1
View File
@@ -11658,6 +11658,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Резервная копия Navicat (.nb3)",
formatNb3Hint: "Формат резервной копии Navicat для MySQL и PostgreSQL. DBX восстанавливает его так же, как обычную SQL-копию.",
formatNb3Unsupported: "Резервные копии Navicat .nb3 поддерживаются только для подключений MySQL и PostgreSQL. Выберите SQL или Gzip.",
restoreFromFile: "Восстановить из файла",
restoreFromFileHint: "Восстановить резервную копию из файла .sql, .sql.gz или .nb3 в любом месте",
importBackup: "Импортировать копию",
+1
View File
@@ -12031,6 +12031,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "ไฟล์สำรอง Navicat (.nb3)",
formatNb3Hint: "รูปแบบไฟล์สำรองของ Navicat สำหรับ MySQL และ PostgreSQL โดย DBX กู้คืนได้เหมือนไฟล์สำรอง SQL ทั่วไป",
formatNb3Unsupported: "ไฟล์สำรอง .nb3 ของ Navicat รองรับเฉพาะการเชื่อมต่อ MySQL และ PostgreSQL เท่านั้น โปรดเลือก SQL หรือ Gzip",
restoreFromFile: "กู้คืนจากไฟล์",
restoreFromFileHint: "กู้คืนไฟล์สำรอง .sql, .sql.gz หรือ .nb3 จากตำแหน่งใดก็ได้",
importBackup: "นำเข้าไฟล์สำรอง",
+1
View File
@@ -9052,6 +9052,7 @@ export default withEnglishFallback({
formatSql: "SQL (.sql)",
formatNb3: "Navicat yedeği (.nb3)",
formatNb3Hint: "MySQL ve PostgreSQL için Navicat yedek biçimi. DBX bunu herhangi bir SQL yedeği gibi geri yükler.",
formatNb3Unsupported: "Navicat .nb3 yedekleri yalnızca MySQL ve PostgreSQL bağlantılarını destekler. SQL veya Gzip seçin.",
restoreFromFile: "Dosyadan geri yükle",
restoreFromFileHint: "Herhangi bir konumdaki .sql, .sql.gz veya .nb3 yedek dosyasını geri yükleyin",
importBackup: "Yedeği içe aktar",
+1
View File
@@ -11419,6 +11419,7 @@ export default withEnglishFallback({
formatSql: "SQL(.sql)",
formatNb3: "Navicat 备份(.nb3)",
formatNb3Hint: "适用于 MySQL 和 PostgreSQL 的 Navicat 备份格式,DBX 可像普通 SQL 备份一样恢复。",
formatNb3Unsupported: "Navicat .nb3 备份仅支持 MySQL 和 PostgreSQL 连接,请选择 SQL 或 Gzip。",
restoreFromFile: "从文件恢复",
restoreFromFileHint: "从任意位置恢复 .sql、.sql.gz 或 .nb3 备份文件",
importBackup: "导入备份",
+1
View File
@@ -11913,6 +11913,7 @@ export default withEnglishFallback({
formatSql: "SQL(.sql)",
formatNb3: "Navicat 備份(.nb3)",
formatNb3Hint: "適用於 MySQL 與 PostgreSQL 的 Navicat 備份格式,DBX 可像一般 SQL 備份一樣還原。",
formatNb3Unsupported: "Navicat .nb3 備份僅支援 MySQL 與 PostgreSQL 連線,請選擇 SQL 或 Gzip。",
restoreFromFile: "從檔案還原",
restoreFromFileHint: "從任意位置還原 .sql、.sql.gz 或 .nb3 備份檔案",
importBackup: "匯入備份",
@@ -25,7 +25,10 @@ export function normalizeDatabaseBackupOutputCompression(value: unknown): Databa
return value === "gzip" || value === "nb3" ? value : "none";
}
const CONSISTENT_BACKUP_DATABASE_TYPES = new Set(["mysql", "postgres"]);
const CONSISTENT_BACKUP_DATABASE_TYPES = new Set(["mysql", "postgres", "opengauss"]);
// The backend writes and restores Navicat `.nb3` archives for MySQL and PostgreSQL only
// (`navicat_nb3.rs`); openGauss backups must use plain or gzipped SQL.
const NAVICAT_BACKUP_DATABASE_TYPES = new Set<DatabaseType>(["mysql", "postgres"]);
export class DatabaseBackupConnectionQueue {
private readonly tails = new Map<string, Promise<void>>();
@@ -57,6 +60,11 @@ export function supportsScheduledDatabaseBackup(databaseType: string | undefined
return !!databaseType && CONSISTENT_BACKUP_DATABASE_TYPES.has(databaseType);
}
/** Navicat `.nb3` archives are only supported when every selected connection is MySQL or PostgreSQL. */
export function supportsNavicatBackupFormat(databaseTypes: readonly (DatabaseType | undefined)[]): boolean {
return databaseTypes.length > 0 && databaseTypes.every((databaseType) => !!databaseType && NAVICAT_BACKUP_DATABASE_TYPES.has(databaseType));
}
export function resolveScheduledDatabaseBackupTargets(configuredDatabases: readonly string[], availableDatabases: readonly string[], databaseType?: DatabaseType): string[] {
const available = [...new Set(availableDatabases.map((database) => database.trim()).filter(Boolean))];
if (configuredDatabases.length === 0) return available.filter((database) => !isSystemDatabaseName(databaseType, database));
+13 -9
View File
@@ -2888,8 +2888,10 @@ pub async fn begin_database_backup_snapshot_core_for_export(
.get(connection_id)
.map(|config| config.db_type)
.ok_or_else(|| format!("Connection config not found: {connection_id}"))?;
if !matches!(db_type, DatabaseType::Mysql | DatabaseType::Postgres) {
return Err("Consistent database backup snapshots are only supported for MySQL and PostgreSQL".to_string());
if !matches!(db_type, DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::OpenGauss) {
return Err(
"Consistent database backup snapshots are only supported for MySQL, PostgreSQL, and openGauss".to_string()
);
}
let session_id = if let Some(export_id) = export_id {
@@ -2919,7 +2921,7 @@ pub async fn begin_database_backup_snapshot_core_for_export(
crate::query::begin_database_backup_snapshot(state, connection_id, database).await?
};
let schemas_result = if matches!(db_type, DatabaseType::Postgres) {
let schemas_result = if matches!(db_type, DatabaseType::Postgres | DatabaseType::OpenGauss) {
const POSTGRES_BACKUP_SCHEMAS_SQL: &str = "SELECT n.nspname FROM pg_catalog.pg_namespace n \
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog', 'pg_toast') \
AND n.nspname NOT LIKE 'pg_toast_temp_%' \
@@ -3006,7 +3008,7 @@ fn database_export_metadata_prefetch_concurrency(db_type: DatabaseType) -> usize
// PostgreSQL exports can hold a snapshot connection while metadata and UI
// requests share the base pool. Keep enough capacity available for normal
// browsing instead of filling nearly the entire ten-connection pool.
if db_type == DatabaseType::Postgres {
if matches!(db_type, DatabaseType::Postgres | DatabaseType::OpenGauss) {
4
} else {
8
@@ -3705,11 +3707,12 @@ pub async fn export_database_sql_core(
// Keep the large export state machine on the heap. Besides making the
// caller future small, this prevents the metadata-prefetch locals from
// exhausting the bounded stack used by test and runtime worker threads.
let result = if matches!(db_type, DatabaseType::Postgres) && request.schema.trim().is_empty() {
Box::pin(export_postgres_all_schemas_sql_core(state, request, &on_progress)).await
} else {
Box::pin(export_database_sql_core_inner(state, request, &on_progress)).await
};
let result =
if matches!(db_type, DatabaseType::Postgres | DatabaseType::OpenGauss) && request.schema.trim().is_empty() {
Box::pin(export_postgres_all_schemas_sql_core(state, request, &on_progress)).await
} else {
Box::pin(export_database_sql_core_inner(state, request, &on_progress)).await
};
let metadata_session_id = database_export_client_session_id(&request.export_id);
if let Err(error) =
state.close_metadata_session_pool(&request.connection_id, Some(&request.database), &metadata_session_id).await
@@ -6157,6 +6160,7 @@ mod tests {
#[test]
fn postgres_metadata_prefetch_reserves_pool_capacity() {
assert_eq!(database_export_metadata_prefetch_concurrency(DatabaseType::Postgres), 4);
assert_eq!(database_export_metadata_prefetch_concurrency(DatabaseType::OpenGauss), 4);
assert_eq!(database_export_metadata_prefetch_concurrency(DatabaseType::Mysql), 8);
}
@@ -168,13 +168,13 @@ impl BackupService {
.into_iter()
.find(|c| c.id == job.config.connection_id)
.ok_or("Saved backup connection is unavailable")?;
if !matches!(connection.db_type, DatabaseType::Mysql | DatabaseType::Postgres) {
if !matches!(connection.db_type, DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::OpenGauss) {
return Err("This connection does not support consistent scheduled backups".into());
}
if !connection.save_password || connection.one_time {
return Err("Unattended backups require a saved connection and saved credentials".into());
}
let postgres = connection.db_type == DatabaseType::Postgres;
let postgres = matches!(connection.db_type, DatabaseType::Postgres | DatabaseType::OpenGauss);
job.run.connection_name = connection.name.clone();
state.configs.write().await.insert(connection.id.clone(), connection);
check_cancel(stop)?;
@@ -447,7 +447,7 @@ fn check_cancel(stop: &CancellationToken) -> Result<(), String> {
}
}
fn system_database(name: &str, postgres: bool) -> bool {
pub(crate) fn system_database(name: &str, postgres: bool) -> bool {
let name = name.to_ascii_lowercase();
if postgres {
matches!(name.as_str(), "template0" | "template1")
@@ -86,6 +86,19 @@ fn exact_backup_scope_preserves_literal_names_and_rejects_empty_or_foreign_scope
assert!(config.validate().is_err());
}
#[test]
fn scheduled_backup_system_databases_filter_templates_for_postgres_and_opengauss() {
assert!(super::engine::system_database("template0", true));
assert!(super::engine::system_database("template1", true));
assert!(super::engine::system_database("TEMPLATE0", true));
assert!(!super::engine::system_database("my_db", true));
assert!(super::engine::system_database("information_schema", false));
assert!(super::engine::system_database("mysql", false));
assert!(super::engine::system_database("performance_schema", false));
assert!(super::engine::system_database("sys", false));
assert!(!super::engine::system_database("my_db", false));
}
#[tokio::test]
async fn schedule_survives_reopen_and_rejects_stale_edit() {
let dir = tempfile::tempdir().unwrap();
+1 -1
View File
@@ -1,6 +1,6 @@
# Background Database Backups
DBX schedules MySQL and PostgreSQL SQL backups in a shared Rust worker. The
DBX schedules MySQL, PostgreSQL, and openGauss SQL backups in a shared Rust worker. The
desktop window and browser display persisted state; neither needs to keep a
JavaScript timer alive. Backup options, table filters, gzip compression,
per-database snapshots, cancellation, and successful-run retention also apply
+7 -7
View File
@@ -1,9 +1,9 @@
---
title: 定时数据库备份
description: 在 DBX 桌面端为 MySQL 和 PostgreSQL 创建可手动运行或按小时、每天、每周执行的 SQL 备份计划。
description: 在 DBX 桌面端为 MySQL、PostgreSQL 和 openGauss 创建可手动运行或按小时、每天、每周执行的 SQL 备份计划。
---
定时数据库备份使用 DBX 的数据库导出引擎,为 MySQL 和 PostgreSQL 连接生成 SQL 文件。它适合开发、测试和中小型数据库的周期性逻辑备份,并提供表范围、保留数量、运行历史和手动执行。
定时数据库备份使用 DBX 的数据库导出引擎,为 MySQL、PostgreSQL 和 openGauss 连接生成 SQL 文件。它适合开发、测试和中小型数据库的周期性逻辑备份,并提供表范围、保留数量、运行历史和手动执行。
如果只需要立即备份一次,可以在设置中使用**立即备份**,不必创建计划。配置连接、目录、数据库、表范围和备份内容后点击**开始备份**。一次性备份不会创建或修改计划,不会计算下次执行时间,并会出现在统一的备份历史中。
@@ -13,10 +13,10 @@ description: 在 DBX 桌面端为 MySQL 和 PostgreSQL 创建可手动运行或
| 项目 | 当前行为 |
| --- | --- |
| 数据库 | MySQL、PostgreSQL |
| 数据库 | MySQL、PostgreSQL、openGauss |
| 平台 | DBX 桌面端 |
| 输出 | 默认模板为每次计划运行创建独立目录;目录内按数据库/Schema 生成备份文件,且可通过模板自定义目录与文件名 |
| 格式 | 普通 SQL(`.sql`)、Gzip 压缩 SQL(`.sql.gz`)或 Navicat 备份归档(`.nb3`) |
| 格式 | 所有支持的数据库均可使用普通 SQL(`.sql`)和 Gzip 压缩 SQL(`.sql.gz`);Navicat 备份归档(`.nb3`)仅支持 MySQL 和 PostgreSQL |
| 频率 | 每隔若干小时、每天指定时间、每周指定星期和时间 |
| 内容 | 表结构、表数据、数据库对象、可选 `DROP TABLE IF EXISTS` |
| 表范围 | 全部、按模式包含、按模式排除 |
@@ -34,7 +34,7 @@ description: 在 DBX 桌面端为 MySQL 和 PostgreSQL 创建可手动运行或
<Step>
### 配置备份范围
选择 MySQL 或 PostgreSQL 连接、备份目录、数据库范围、表范围和备份内容。一次性备份不包含计划名称、执行频率、执行时间、保留数量或启用状态。
选择 MySQL、PostgreSQL 或 openGauss 连接、备份目录、数据库范围、表范围和备份内容。一次性备份不包含计划名称、执行频率、执行时间、保留数量或启用状态。
连接列表支持多选,可一次勾选多个连接统一备份;每个连接会在**备份历史**中生成独立的备份记录,并依次执行。选择多个连接时,每个连接都备份其全部数据库,不支持精确选择表,且文件名模板必须包含 `{runId}`,避免不同连接的备份文件重名。
@@ -59,7 +59,7 @@ description: 在 DBX 桌面端为 MySQL 和 PostgreSQL 创建可手动运行或
<Step>
### 选择连接和数据库
选择 MySQL 或 PostgreSQL 连接。可以备份所有非系统数据库,也可以选择指定数据库。
选择 MySQL、PostgreSQL 或 openGauss 连接。可以备份所有非系统数据库,也可以选择指定数据库。
</Step>
<Step>
@@ -151,7 +151,7 @@ DBX 从不删除导入的文件。删除导入记录只会将其从历史中移
## Navicat 备份(.nb3)
将备份格式选择为 **Navicat 备份(.nb3)** 时,DBX 会按 Navicat 用于 MySQL 和 PostgreSQL 的归档结构写出文件:一个包含 `meta.json` 索引、Gzip 压缩的对象元数据(DDL、索引、约束、触发器)以及 Gzip 压缩数据分块的 tar 归档,每个成员都带有 SHA-1 校验值。
在 MySQL 或 PostgreSQL 连接上将备份格式选择为 **Navicat 备份(.nb3)** 时,DBX 会按 Navicat 的归档结构写出文件:一个包含 `meta.json` 索引、Gzip 压缩的对象元数据(DDL、索引、约束、触发器)以及 Gzip 压缩数据分块的 tar 归档,每个成员都带有 SHA-1 校验值。其他数据库类型不提供此格式。
DBX 也可以恢复 `.nb3` 文件,包括由 Navicat 创建的备份。在 [SQL 文件执行](/cn/docs/sql-file) 中打开文件,或在备份历史中点击**恢复**。DBX 会把归档转换为恢复脚本,依次:
+7 -7
View File
@@ -1,9 +1,9 @@
---
title: Scheduled Database Backups
description: Create manual, hourly, daily, or weekly SQL backup schedules for MySQL and PostgreSQL in DBX Desktop.
description: Create manual, hourly, daily, or weekly SQL backup schedules for MySQL, PostgreSQL, and openGauss in DBX Desktop.
---
Scheduled database backups use the DBX database-export engine to generate SQL files for MySQL and PostgreSQL connections. They are useful for recurring logical backups of development, test, and small or medium databases and include table scope, retention, run history, and manual execution.
Scheduled database backups use the DBX database-export engine to generate SQL files for MySQL, PostgreSQL, and openGauss connections. They are useful for recurring logical backups of development, test, and small or medium databases and include table scope, retention, run history, and manual execution.
For a backup that should run only once, use **Back up now** in Settings instead of creating a schedule. Configure the same connection, destination, database, table, and content scope, then choose **Start backup**. This one-shot run does not create or change a schedule, does not calculate a next run, and appears in the same backup history as scheduled runs.
@@ -13,10 +13,10 @@ For a backup that should run only once, use **Back up now** in Settings instead
| Item | Current behavior |
| --- | --- |
| Databases | MySQL and PostgreSQL |
| Databases | MySQL, PostgreSQL, and openGauss |
| Platform | DBX Desktop |
| Output | The default template creates one directory per scheduled run, containing one file per database/schema target; directory and file-name templates can be customized |
| Format | Plain SQL (`.sql`), gzip-compressed SQL (`.sql.gz`), or a Navicat backup archive (`.nb3`) |
| Format | Plain SQL (`.sql`) and gzip-compressed SQL (`.sql.gz`) for every supported database; a Navicat backup archive (`.nb3`) for MySQL and PostgreSQL only |
| Frequency | Every N hours, daily at a time, or weekly on a weekday and time |
| Contents | Table structure, data, database objects, and optional `DROP TABLE IF EXISTS` |
| Table scope | All, include by pattern, or exclude by pattern |
@@ -34,7 +34,7 @@ For a backup that should run only once, use **Back up now** in Settings instead
<Step>
### Configure the backup
Select a MySQL or PostgreSQL connection, destination directory, database scope, table scope, and backup contents. A one-shot backup has no schedule name, frequency, retention, or enabled state.
Select a MySQL, PostgreSQL, or openGauss connection, destination directory, database scope, table scope, and backup contents. A one-shot backup has no schedule name, frequency, retention, or enabled state.
You can tick several connections in the connection list to back them all up in one go; each connection becomes its own run in **Backup history** and the runs execute one after another. With more than one connection, every connection backs up all of its databases, exact table selection is unavailable, and the file name template must contain `{runId}` so files from different connections never collide.
@@ -59,7 +59,7 @@ For a backup that should run only once, use **Back up now** in Settings instead
<Step>
### Choose connection and databases
Select a MySQL or PostgreSQL connection. Back up all non-system databases or choose specific databases.
Select a MySQL, PostgreSQL, or openGauss connection. Back up all non-system databases or choose specific databases.
</Step>
<Step>
@@ -151,7 +151,7 @@ DBX never deletes imported files. Deleting an imported entry only removes it fro
## Navicat Backups (.nb3)
Choose **Navicat backup (.nb3)** as the backup format to write the archive layout Navicat uses for MySQL and PostgreSQL: a tar archive with a `meta.json` index, gzipped per-object metadata (DDL, indexes, constraints, triggers), and gzipped row chunks, each protected by a SHA-1 checksum.
Choose **Navicat backup (.nb3)** as the backup format on a MySQL or PostgreSQL connection to write the archive layout Navicat uses: a tar archive with a `meta.json` index, gzipped per-object metadata (DDL, indexes, constraints, triggers), and gzipped row chunks, each protected by a SHA-1 checksum. The format is not offered for other database types.
DBX also restores `.nb3` files, including archives created by Navicat. Open the file in [SQL File Execution](/en/docs/sql-file) or click **Restore** in backup history. DBX turns the archive into a restore script that:
+4 -2
View File
@@ -5,7 +5,7 @@ description: 将一个或多个数据库的结构、数据和可读取对象流
数据库导出会生成可审阅的 SQL 脚本,用于小型备份、迁移、测试数据准备或高风险操作前留档。它可以导出单个数据库/Schema,也可以按连接批量导出多个数据库。
<Callout type="info">SQL 导出是逻辑导出,不等同于数据库原生物理备份。大型生产库、时间点恢复、增量备份或严格一致性要求,应优先使用数据库官方备份工具;MySQL 和 PostgreSQL 的定时一致性备份可参阅 [数据库备份](/cn/docs/database-backup)。</Callout>
<Callout type="info">SQL 导出是逻辑导出,不等同于数据库原生物理备份。大型生产库、时间点恢复、增量备份或严格一致性要求,应优先使用数据库官方备份工具;MySQL、PostgreSQL 和 openGauss 的定时一致性备份可参阅 [数据库备份](/cn/docs/database-backup)。</Callout>
## 可以导出的内容
@@ -19,6 +19,8 @@ description: 将一个或多个数据库的结构、数据和可读取对象流
启用“仅选中表”时,表结构和数据只处理所选表。例程等 Schema 级对象不会因为选择少量表而被一并带出;对象导出始终受当前数据库能力限制。
openGauss 导出目前不会生成 `CREATE SEQUENCE`、`ALTER SEQUENCE ... OWNED BY` 和 `setval`,恢复后自增计数器会从 1 开始,自定义序列也会缺失;同样因为相关处理仅适用于 PostgreSQL,存在循环外键的 Schema 无法恢复。
## 单库导出
<Steps>
@@ -113,7 +115,7 @@ DBX 使用分页或流式读取,把数据分批写入输出文件。交互式
<Cards>
<Card title="数据库备份" href="/cn/docs/database-backup">
在 Desktop 中为 MySQL 和 PostgreSQL 配置定时一致性 SQL 备份。
在 Desktop 中为 MySQL、PostgreSQL 和 openGauss 配置定时一致性 SQL 备份。
</Card>
<Card title="SQL 文件执行" href="/cn/docs/sql-file">
预览、批量执行并检查 SQL 文件结果。
+4 -2
View File
@@ -5,7 +5,7 @@ description: Stream structure, data, and readable database objects from one or m
Database Export creates reviewable SQL scripts for small backups, migrations, test datasets, or snapshots before risky changes. It can export one database/schema or orchestrate separate exports for multiple databases in a connection.
<Callout type="info">SQL export is a logical export, not a database-native physical backup. Use official database tools for large production systems, point-in-time recovery, incremental backup, or strict consistency. For scheduled consistent MySQL and PostgreSQL exports, see [Database Backup](/en/docs/database-backup).</Callout>
<Callout type="info">SQL export is a logical export, not a database-native physical backup. Use official database tools for large production systems, point-in-time recovery, incremental backup, or strict consistency. For scheduled consistent MySQL, PostgreSQL, and openGauss exports, see [Database Backup](/en/docs/database-backup).</Callout>
## Exported Content
@@ -19,6 +19,8 @@ Database Export creates reviewable SQL scripts for small backups, migrations, te
When exporting only selected tables, structure and data are limited to that subset. Schema-wide routines are not pulled in merely because a few tables were selected. Object coverage always depends on the database and driver.
openGauss exports currently omit `CREATE SEQUENCE`, `ALTER SEQUENCE ... OWNED BY`, and `setval`, so restored serial counters start at 1 and custom sequences are missing; the same PostgreSQL-only handling means schemas with cyclic foreign keys are not restorable.
## Single-Database Export
<Steps>
@@ -113,7 +115,7 @@ Use [SQL File Execution](/en/docs/sql-file) to preview and run an exported scrip
<Cards>
<Card title="Database Backup" href="/en/docs/database-backup">
Schedule consistent MySQL and PostgreSQL SQL backups in Desktop.
Schedule consistent MySQL, PostgreSQL, and openGauss SQL backups in Desktop.
</Card>
<Card title="SQL File Execution" href="/en/docs/sql-file">
Preview, batch-execute, and review SQL file results.
+1 -1
View File
@@ -95,7 +95,7 @@ Redis、MongoDB、搜索、向量、HBase、配置中心和消息队列工作区
### 备份和导出
- [数据库导出](/cn/docs/database-export) 是能力驱动的逻辑 SQL 导出
- [数据库备份](/cn/docs/database-backup) 当前只在 Desktop 为 MySQL 和 PostgreSQL 提供定时一致性备份
- [数据库备份](/cn/docs/database-backup) 当前只在 Desktop 为 MySQL、PostgreSQL 和 openGauss 提供定时一致性备份
- 云同步和配置导出只迁移 DBX 配置,不会备份业务数据库
### 原生 Job 与 Scheduler 作业
+1 -1
View File
@@ -95,7 +95,7 @@ Redis, MongoDB, search, vector, HBase, configuration-center, and message-queue w
### Backup and Export
- [Database Export](/en/docs/database-export) is a capability-driven logical SQL export
- [Database Backup](/en/docs/database-backup) currently schedules consistent MySQL and PostgreSQL backups in Desktop only
- [Database Backup](/en/docs/database-backup) currently schedules consistent MySQL, PostgreSQL, and openGauss backups in Desktop only
- Cloud Sync and configuration export migrate DBX settings, not business database contents
### Native Job and Scheduler Management
+1 -1
View File
@@ -115,7 +115,7 @@ DBX 流式读取文件,并使用数据库感知的分割逻辑,而不是简
生成结构、数据和对象 SQL 文件。
</Card>
<Card title="数据库备份" href="/cn/docs/database-backup">
为 MySQL 和 PostgreSQL 配置 Desktop 定时备份。
为 MySQL、PostgreSQL 和 openGauss 配置 Desktop 定时备份。
</Card>
<Card title="生产环境安全" href="/cn/docs/production-safety">
了解执行前确认、只读保护和权限边界。
+1 -1
View File
@@ -115,7 +115,7 @@ Query execution support does not guarantee compatibility with every client scrip
Generate structure, data, and object SQL files.
</Card>
<Card title="Database Backup" href="/en/docs/database-backup">
Schedule MySQL and PostgreSQL backups in Desktop.
Schedule MySQL, PostgreSQL, and openGauss backups in Desktop.
</Card>
<Card title="Production Safety" href="/en/docs/production-safety">
Understand pre-execution confirmation, read-only protection, and privilege boundaries.
+16
View File
@@ -16,6 +16,7 @@ import {
normalizeDatabaseBackupTablePatterns,
resolveScheduledDatabaseBackupTableScope,
resolveScheduledDatabaseBackupTargets,
supportsNavicatBackupFormat,
supportsScheduledDatabaseBackup,
type DatabaseBackupRun,
type DatabaseBackupSchedule,
@@ -148,6 +149,16 @@ test("Navicat backups use the .nb3 suffix and survive schedule normalization", (
assert.equal(normalizeDatabaseBackupSchedule({ ...schedule(), outputCompression: "zip" })?.outputCompression, "none");
});
test("Navicat backups are limited to MySQL and PostgreSQL connections", () => {
assert.equal(supportsNavicatBackupFormat(["mysql"]), true);
assert.equal(supportsNavicatBackupFormat(["postgres"]), true);
assert.equal(supportsNavicatBackupFormat(["mysql", "postgres"]), true);
assert.equal(supportsNavicatBackupFormat(["opengauss"]), false);
assert.equal(supportsNavicatBackupFormat(["mysql", "opengauss"]), false);
assert.equal(supportsNavicatBackupFormat([undefined]), false);
assert.equal(supportsNavicatBackupFormat([]), false);
});
test("scheduled runs render a user-defined relative directory template", () => {
const directory = databaseBackupRunDirectory("C:\\backups", "archives/{schedule}/{date}/{timestamp}-{runId}", "Nightly: prod", new Date(2026, 6, 16, 2, 3, 4), "12345678-abcd");
@@ -283,6 +294,10 @@ test("all-database PostgreSQL backups exclude template databases", () => {
assert.deepEqual(resolveScheduledDatabaseBackupTargets([], ["template0", "postgres", "template1", "app"], "postgres"), ["postgres", "app"]);
});
test("all-database openGauss backups exclude template databases", () => {
assert.deepEqual(resolveScheduledDatabaseBackupTargets([], ["template0", "opengauss", "template1", "app"], "opengauss"), ["opengauss", "app"]);
});
test("explicit system database targets remain available", () => {
assert.deepEqual(resolveScheduledDatabaseBackupTargets(["mysql"], ["mysql", "app"], "mysql"), ["mysql"]);
});
@@ -294,6 +309,7 @@ test("explicit backup databases fail when any configured target is missing", ()
test("scheduled backups are limited to databases with consistent snapshot support", () => {
assert.equal(supportsScheduledDatabaseBackup("postgres"), true);
assert.equal(supportsScheduledDatabaseBackup("mysql"), true);
assert.equal(supportsScheduledDatabaseBackup("opengauss"), true);
assert.equal(supportsScheduledDatabaseBackup("dameng"), false);
assert.equal(supportsScheduledDatabaseBackup("sqlite"), false);
assert.equal(supportsScheduledDatabaseBackup("sqlserver"), false);