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}