fix(data-transfer): handle missing owner role policy and align web sync behavior

* Feat: Add PostgreSQL transfer ownership preview and resolution

- Add ownership policy support for data transfers: preserve, skip, and reassign missing owners
- Preview missing PostgreSQL roles before creating target schema objects
- Show ownership confirmation dialog when target roles are missing
- Allow users to skip ownership changes or reassign missing owners to the target connection user
- Add Tauri and HTTP API support for ownership preview
- Apply ownership statements according to selected policy during PostgreSQL transfers
- Add Chinese localization for ownership confirmation UI

* Feat: Add ownership confirmation localization

- Add ownership confirmation messages for supported desktop locales
- Include skip and confirm labels for ownership handling
- Update transfer test requests to use Preserve ownership policy
This commit is contained in:
Moe. authored and GitHub committed 2026-07-09 00:35:34 +08:00
1 parent 135d4e15db
commit a07f115407
18 files changed
+508 -33

No files matched your search

@@ -55,6 +55,11 @@ const transferMode = ref<TransferMode>("append");
const targetTableNameCase = ref<TransferTableNameCase>("preserve");
const batchSize = ref(1000);
const isSubmitting = ref(false);
const ownershipDialogOpen = ref(false);
const ownershipMissingOwners = ref<string[]>([]);
const ownershipTargetOwner = ref("");
const pendingOwnershipRequest = ref<api.TransferRequest | null>(null);
const pendingOwnershipRefresh = ref<{ targetConnection: string; targetDatabase: string; targetSchema: string; shouldRefreshTargetTree: boolean } | null>(null);
const filteredTables = computed(() => {
const q = tableSearch.value.toLowerCase();
@@ -247,6 +252,11 @@ function resetState() {
targetTableNameCase.value = "preserve";
batchSize.value = 1000;
isSubmitting.value = false;
ownershipDialogOpen.value = false;
ownershipMissingOwners.value = [];
ownershipTargetOwner.value = "";
pendingOwnershipRequest.value = null;
pendingOwnershipRefresh.value = null;
}
async function startTransfer() {
@@ -272,10 +282,39 @@ async function startTransfer() {
createTable: createTable.value,
mode: transferMode.value,
targetTableNameCase: targetTableNameCase.value,
ownershipPolicy: "preserve",
batchSize: batchSize.value,
};
startDataTransferTask(request, `${sourceDatabaseName} → ${targetDatabaseName}`, {
if (createTable.value) {
try {
const preview = await api.previewTransferOwnership(request);
if (preview.missingOwners.length > 0) {
ownershipMissingOwners.value = preview.missingOwners;
ownershipTargetOwner.value = preview.targetOwner;
pendingOwnershipRequest.value = request;
pendingOwnershipRefresh.value = {
targetConnection,
targetDatabase: targetDatabaseName,
targetSchema: effectiveTargetSchema,
shouldRefreshTargetTree,
};
ownershipDialogOpen.value = true;
isSubmitting.value = false;
return;
}
} catch {
isSubmitting.value = false;
return;
}
}
runTransfer(request, targetConnection, targetDatabaseName, effectiveTargetSchema, shouldRefreshTargetTree);
}
function runTransfer(request: api.TransferRequest, targetConnection: string, targetDatabaseName: string, effectiveTargetSchema: string, shouldRefreshTargetTree: boolean) {
isSubmitting.value = true;
startDataTransferTask(request, `${request.sourceDatabase} → ${targetDatabaseName}`, {
formatOverlapError: (tables) => t("transfer.targetTableBusy", { tables: tables.join(", ") }),
onDone: async () => {
if (shouldRefreshTargetTree) {
@@ -287,6 +326,21 @@ async function startTransfer() {
resetState();
}
function resolveOwnershipDecision(policy: api.TransferOwnershipPolicy | null) {
const request = pendingOwnershipRequest.value;
const refresh = pendingOwnershipRefresh.value;
pendingOwnershipRequest.value = null;
pendingOwnershipRefresh.value = null;
ownershipDialogOpen.value = false;
ownershipMissingOwners.value = [];
ownershipTargetOwner.value = "";
if (!policy || !request || !refresh) {
isSubmitting.value = false;
return;
}
runTransfer({ ...request, ownershipPolicy: policy }, refresh.targetConnection, refresh.targetDatabase, refresh.targetSchema, refresh.shouldRefreshTargetTree);
}
function getConnectionName(id: string) {
return store.connections.find((c) => c.id === id)?.name ?? id;
}
@@ -514,4 +568,34 @@ function getConnectionName(id: string) {
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="ownershipDialogOpen">
<DialogContent class="sm:max-w-[520px]" @interact-outside.prevent>
<DialogHeader>
<DialogTitle>{{ t("transfer.ownershipTitle") }}</DialogTitle>
</DialogHeader>
<div class="space-y-3 text-sm">
<p class="text-muted-foreground">
{{ t("transfer.ownershipMessage", { owners: ownershipMissingOwners.join(", ") }) }}
</p>
<div class="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
{{ t("transfer.ownershipSkipDetails") }}
</div>
<div class="rounded-md border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
{{ t("transfer.ownershipTargetOwner", { owner: ownershipTargetOwner }) }}
</div>
</div>
<DialogFooter class="gap-2">
<Button variant="outline" size="sm" @click="resolveOwnershipDecision(null)">
{{ t("transfer.cancel") }}
</Button>
<Button variant="secondary" size="sm" @click="resolveOwnershipDecision('skip')">
{{ t("transfer.ownershipSkip") }}
</Button>
<Button size="sm" @click="resolveOwnershipDecision('reassignMissing')">
{{ t("transfer.ownershipConfirm") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
+6
View File
@@ -2242,6 +2242,12 @@ export default {
editConfig: "Edit config",
retry: "Retry",
targetTableBusy: "Another transfer is already writing target table(s): {tables}",
ownershipTitle: "Confirm ownership",
ownershipMessage: "The target database is missing these owner users or roles: {owners}. Applying the original ownership may fail.",
ownershipSkipDetails: "Skip will not run object ownership change statements. Table data and other structure synchronization will continue.",
ownershipTargetOwner: "Confirm will reassign objects with missing owners to the target connection user: {owner}.",
ownershipSkip: "Skip",
ownershipConfirm: "Confirm",
selectConnection: "Select connection",
selectDatabase: "Select database",
selectSchema: "Select schema",
+6
View File
@@ -2174,6 +2174,12 @@ export default withEnglishFallback({
editConfig: "Editar config",
retry: "Reintentar",
targetTableBusy: "Otra transferencia ya está escribiendo en la(s) tabla(s) de destino: {tables}",
ownershipTitle: "Confirmar propietario",
ownershipMessage: "La base de datos de destino no tiene estos usuarios o roles propietarios: {owners}. Aplicar el propietario original puede fallar.",
ownershipSkipDetails: "Omitir no ejecutará las sentencias de cambio de propietario de objetos. Los datos de tablas y otras estructuras seguirán sincronizándose.",
ownershipTargetOwner: "Confirmar reasignará los objetos con propietarios ausentes al usuario de conexión de destino: {owner}.",
ownershipSkip: "Omitir",
ownershipConfirm: "Confirmar",
selectConnection: "Seleccionar conexión",
selectDatabase: "Seleccionar base de datos",
selectSchema: "Seleccionar esquema",
+6
View File
@@ -2172,6 +2172,12 @@ export default withEnglishFallback({
editConfig: "Modifica config",
retry: "Riprova",
targetTableBusy: "Un altro trasferimento sta gia scrivendo le tabelle di destinazione: {tables}",
ownershipTitle: "Conferma proprietario",
ownershipMessage: "Nel database di destinazione mancano questi utenti o ruoli proprietari: {owners}. Applicare il proprietario originale potrebbe non riuscire.",
ownershipSkipDetails: "Salta non eseguirà le istruzioni di modifica del proprietario degli oggetti. I dati delle tabelle e le altre strutture continueranno a essere sincronizzati.",
ownershipTargetOwner: "Conferma riassegnerà gli oggetti con proprietari mancanti all'utente della connessione di destinazione: {owner}.",
ownershipSkip: "Salta",
ownershipConfirm: "Conferma",
selectConnection: "Seleziona connessione",
selectDatabase: "Seleziona database",
selectSchema: "Seleziona schema",
+6
View File
@@ -2172,6 +2172,12 @@ export default withEnglishFallback({
editConfig: "設定を編集",
retry: "リトライ",
targetTableBusy: "別の転送がすでにターゲットテーブルに書き込み中です: {tables}",
ownershipTitle: "所有者の確認",
ownershipMessage: "対象データベースに次の所有者ユーザーまたはロールがありません: {owners}。元の所有者を適用すると失敗する可能性があります。",
ownershipSkipDetails: "スキップすると、オブジェクト所有者の変更ステートメントは実行されません。テーブルデータとその他の構造の同期は続行されます。",
ownershipTargetOwner: "確認すると、所有者が存在しないオブジェクトは対象接続ユーザーに再割り当てされます: {owner}。",
ownershipSkip: "スキップ",
ownershipConfirm: "確認",
selectConnection: "接続を選択",
selectDatabase: "データベースを選択",
selectSchema: "スキーマを選択",
+6
View File
@@ -2173,6 +2173,12 @@ export default withEnglishFallback({
editConfig: "Editar configuração",
retry: "Tentar novamente",
targetTableBusy: "Outra transferência já está gravando na(s) tabela(s) de destino: {tables}",
ownershipTitle: "Confirmar proprietário",
ownershipMessage: "O banco de dados de destino não tem estes usuários ou roles proprietários: {owners}. Aplicar o proprietário original pode falhar.",
ownershipSkipDetails: "Pular não executará as instruções de alteração de proprietário dos objetos. Os dados das tabelas e outras estruturas continuarão sendo sincronizados.",
ownershipTargetOwner: "Confirmar reatribuirá os objetos com proprietários ausentes ao usuário da conexão de destino: {owner}.",
ownershipSkip: "Pular",
ownershipConfirm: "Confirmar",
selectConnection: "Selecionar conexão",
selectDatabase: "Selecionar banco de dados",
selectSchema: "Selecionar schema",
+6
View File
@@ -2242,6 +2242,12 @@ export default withEnglishFallback({
editConfig: "编辑配置",
retry: "重新传输",
targetTableBusy: "已有传输任务正在写入目标表:{tables}",
ownershipTitle: "归属用户确认",
ownershipMessage: "目标端缺少以下归属用户或角色:{owners}。按原归属用户应用可能失败。",
ownershipSkipDetails: "选择跳过后,将不执行对象归属变更语句,表数据和其它结构同步会继续执行。",
ownershipTargetOwner: "选择确认后,缺失归属用户对应的对象会重归属至目标连接用户:{owner}。",
ownershipSkip: "跳过",
ownershipConfirm: "确认",
selectConnection: "选择连接",
selectDatabase: "选择数据库",
selectSchema: "选择模式",
+6
View File
@@ -2075,6 +2075,12 @@ export default withEnglishFallback({
editConfig: "編輯配置",
retry: "重試",
targetTableBusy: "另一個傳輸正在寫入目標資料表:{tables}",
ownershipTitle: "歸屬使用者確認",
ownershipMessage: "目標端缺少以下歸屬使用者或角色:{owners}。依原歸屬使用者套用可能失敗。",
ownershipSkipDetails: "選擇跳過後,將不執行物件歸屬變更語句,資料表資料和其他結構同步會繼續執行。",
ownershipTargetOwner: "選擇確認後,缺失歸屬使用者對應的物件會重歸屬至目標連線使用者:{owner}。",
ownershipSkip: "跳過",
ownershipConfirm: "確認",
selectConnection: "選擇連線",
selectDatabase: "選擇資料庫",
selectSchema: "選擇結構描述",
+3
View File
@@ -278,6 +278,7 @@ export const nacosRawRequest = forward("nacosRawRequest");
// Data Transfer
export const startTransfer = forward("startTransfer");
export const cancelTransfer = forward("cancelTransfer");
export const previewTransferOwnership = forward("previewTransferOwnership");
export const sortTablesByFkDependency = forward("sortTablesByFkDependency");
// Table File Import
@@ -511,6 +512,8 @@ export type {
TransferProgress,
TransferMode,
TransferTableNameCase,
TransferOwnershipPolicy,
TransferOwnershipPreview,
TableImportMode,
TableImportStatus,
TableImportSourceFormat,
+5
View File
@@ -75,6 +75,7 @@ import type {
SqlFileProgress,
TransferRequest,
TransferProgress,
TransferOwnershipPreview,
TableImportPreviewRequest,
TableImportPreview,
TableImportRequest,
@@ -1319,6 +1320,10 @@ export async function cancelTransfer(transferId: string): Promise<void> {
return post("/api/transfer/cancel", { transferId });
}
export async function previewTransferOwnership(request: TransferRequest): Promise<TransferOwnershipPreview> {
return post("/api/transfer/ownership-preview", { request });
}
export interface SortTablesByFkOptions {
connectionId: string;
database: string;
+11
View File
@@ -1889,6 +1889,7 @@ export async function listenSqlFileProgress(handler: (progress: SqlFileProgress)
// --- Data Transfer ---
export type TransferMode = "append" | "overwrite" | "upsert";
export type TransferTableNameCase = "preserve" | "lower" | "upper";
export type TransferOwnershipPolicy = "preserve" | "skip" | "reassignMissing";
export interface TransferRequest {
transferId: string;
@@ -1902,9 +1903,15 @@ export interface TransferRequest {
createTable: boolean;
mode: TransferMode;
targetTableNameCase: TransferTableNameCase;
ownershipPolicy?: TransferOwnershipPolicy;
batchSize: number;
}
export interface TransferOwnershipPreview {
missingOwners: string[];
targetOwner: string;
}
export interface TransferProgress {
transferId: string;
table: string;
@@ -1943,6 +1950,10 @@ export async function cancelTransfer(transferId: string): Promise<void> {
return invoke("cancel_transfer", { transferId });
}
export async function previewTransferOwnership(request: TransferRequest): Promise<TransferOwnershipPreview> {
return invoke("preview_transfer_ownership", { request });
}
export interface SortTablesByFkOptions {
connectionId: string;
database: string;