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}