From 2b90a62159b3d186431718b1fd573c43fc5a2b19 Mon Sep 17 00:00:00 2001 From: Kunghim Date: Sun, 9 Aug 2026 14:11:19 +0800 Subject: [PATCH] fix(sync): constrain time-series targets to append writes --- .../data-sync/DataSyncDeliveryStage.test.tsx | 38 +++++++++++++++++++ .../data-sync/DataSyncTaskEditor.tsx | 20 +++++++++- .../data-sync/DataSyncWorkbenchShell.tsx | 1 + frontend/src/components/data-sync/gateway.ts | 1 + frontend/src/components/data-sync/model.ts | 1 + frontend/src/components/data-sync/text.ts | 4 ++ .../src/components/data-sync/wailsDto.test.ts | 3 ++ frontend/src/components/data-sync/wailsDto.ts | 5 +++ .../src/components/data-sync/wailsGateway.ts | 1 + internal/app/data_sync_job_approval_test.go | 19 ++++++++++ internal/app/data_sync_job_preflight.go | 27 +++++++++++++ internal/sync/migration_capability.go | 7 ++++ internal/sync/migration_capability_test.go | 22 +++++++++++ 13 files changed, 148 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/data-sync/DataSyncDeliveryStage.test.tsx b/frontend/src/components/data-sync/DataSyncDeliveryStage.test.tsx index 06c260d0..abfdee55 100644 --- a/frontend/src/components/data-sync/DataSyncDeliveryStage.test.tsx +++ b/frontend/src/components/data-sync/DataSyncDeliveryStage.test.tsx @@ -19,6 +19,7 @@ const supportedCapability: DataSyncRouteCapability = { supportsAutoCreate: true, supportsAutoAddColumns: true, requiresExistingTarget: false, + supportsMutations: true, supportsCdc: true, }; @@ -140,6 +141,43 @@ describe('DataSyncTaskEditor delivery stage', () => { }); }); + it('constrains append-only targets to inserts and removes delete propagation', async () => { + const mapping = { + ...createDataSyncTableMapping('timeseries:mapping:1', 'orders', 'orders'), + keyColumns: ['id'], + }; + const base = createDataSyncTaskDraft({ id: 'timeseries', kind: 'reconcile' }); + const task = reviseDataSyncTask(base, { + source: endpoint('source'), + target: endpoint('target'), + mappings: [mapping], + delivery: { + ...base.delivery, + writeMode: 'upsert', + retryLimit: 3, + propagateDeletes: true, + }, + }); + const { renderer, onPatch } = await renderDelivery(task, vi.fn(), { + ...supportedCapability, + supportsMutations: false, + }); + + expect(JSON.stringify(renderer.toJSON())).toContain('当前时序目标仅支持追加写入'); + expect(renderer.root.findAllByProps({ 'data-delete-propagation': 'true' })).toHaveLength(0); + const upsert = renderer.root + .findAllByType('option') + .find((option) => option.props.value === 'upsert')!; + expect(upsert.props.disabled).toBe(true); + expect(onPatch).toHaveBeenCalledWith({ + delivery: expect.objectContaining({ + writeMode: 'append', + retryLimit: 0, + propagateDeletes: false, + }), + }); + }); + it('reveals migration schema controls only for supported implicit same-name mappings', async () => { const implicitMapping = { ...createDataSyncTableMapping('migration:mapping:1', 'public.orders', 'public.orders'), diff --git a/frontend/src/components/data-sync/DataSyncTaskEditor.tsx b/frontend/src/components/data-sync/DataSyncTaskEditor.tsx index 2f75ce61..8e0052da 100644 --- a/frontend/src/components/data-sync/DataSyncTaskEditor.tsx +++ b/frontend/src/components/data-sync/DataSyncTaskEditor.tsx @@ -300,6 +300,8 @@ const DeliveryStage: React.FC<{ (task.kind !== 'cdc' || capability.supportsCdc === true)); const rowIsolationAvailable = routeCanWrite && canUseDataSyncRowErrorIsolation(task); + const appendOnlyTarget = + capability.level !== 'unknown' && capability.supportsMutations === false; const enabledMappings = task.mappings.filter((mapping) => mapping.enabled); const allEnabledMappingsHaveKeys = enabledMappings.length > 0 && @@ -308,6 +310,7 @@ const DeliveryStage: React.FC<{ ); const canPropagateDeletes = routeCanWrite && + !appendOnlyTarget && task.delivery.writeMode === 'upsert' && ((task.kind === 'reconcile' && task.incremental.mode === 'snapshot' && @@ -370,6 +373,10 @@ const DeliveryStage: React.FC<{ if (!canPropagateDeletes && task.delivery.propagateDeletes) { patch.propagateDeletes = false; } + if (appendOnlyTarget && task.delivery.writeMode !== 'append') { + patch.writeMode = 'append'; + patch.retryLimit = 0; + } if ( structureCapabilityResolved && !canAutoAddColumns && @@ -391,6 +398,7 @@ const DeliveryStage: React.FC<{ canAutoAddColumns, canCreateIndexes, canPropagateDeletes, + appendOnlyTarget, onPatch, readOnly, rowIsolationAvailable, @@ -400,6 +408,7 @@ const DeliveryStage: React.FC<{ task.delivery.createIndexes, task.delivery.errorPolicy, task.delivery.propagateDeletes, + task.delivery.writeMode, ]); if (readOnly) { @@ -428,6 +437,11 @@ const DeliveryStage: React.FC<{
+ {appendOnlyTarget ? ( +

+ {t('delivery.append_only_target_note')} +

+ ) : null}