@@ -28,6 +28,23 @@ func (p *Pipeline) ConvertDeltaToZSet(delta object.Delta) (*dbsp.DocumentZSet, e
2828 // Strip UID (if any)
2929 object .RemoveUID (deltaObj )
3030
31+ // Optimization: If the new object deep-equals the cached one, suppress the delta.
32+ // This avoids unnecessary DBSP processing for no-op updates.
33+ // IMPORTANT: Only apply to Updated/Replaced/Upserted, NOT to Added or Deleted!
34+ // - Added events should NOT be suppressed: DBSP multiplicity needs to accumulate
35+ // - Deleted events should NOT be suppressed: always process deletes
36+ // - Updated/Replaced/Upserted: safe to suppress if content hasn't changed
37+ if old != nil && (delta .Type == object .Updated || delta .Type == object .Replaced ||
38+ delta .Type == object .Upserted ) {
39+ oldNoUID := object .DeepCopy (old )
40+ object .RemoveUID (oldNoUID )
41+ if object .DeepEqual (deltaObj , oldNoUID ) {
42+ p .log .V (5 ).Info ("suppressing no-op delta in ConvertDeltaToZSet" ,
43+ "key" , ObjectKey (deltaObj ), "type" , delta .Type )
44+ return dbsp .NewDocumentZSet (), nil
45+ }
46+ }
47+
3148 zset := dbsp .NewDocumentZSet ()
3249 switch delta .Type {
3350 case object .Added :
@@ -171,8 +188,8 @@ func (p *Pipeline) ConvertZSetToDelta(zset *dbsp.DocumentZSet, target schema.Gro
171188// When these are converted to deltas, we get separate Add and Delete entries for the same
172189// primary key (namespace/name), but with no guaranteed ordering. This creates ambiguity:
173190//
174- // - delete + add → UPDATE (object existed, now has new content)
175- // - add + delete → NO-OP (object was transiently created and destroyed in the same batch)
191+ // - delete + add -> UPDATE (object existed, now has new content)
192+ // - add + delete -> NO-OP (object was transiently created and destroyed in the same batch)
176193//
177194// Both sequences produce identical delta lists, but require different reconciliation actions.
178195// Content-based comparison fails when documents contain dynamic fields (e.g., @now timestamps)
@@ -184,8 +201,8 @@ func (p *Pipeline) ConvertZSetToDelta(zset *dbsp.DocumentZSet, target schema.Gro
184201// whether an object existed prior to this batch, which determines the semantic meaning of
185202// add+delete pairs:
186203//
187- // - If object existed → it's an UPDATE (the delete removes old state, add provides new state)
188- // - If object didn't exist → it's TRANSIENT (created and destroyed within the batch, emit nothing)
204+ // - If object existed -> it's an UPDATE (the delete removes old state, add provides new state)
205+ // - If object didn't exist -> it's TRANSIENT (created and destroyed within the batch, emit nothing)
189206//
190207// # Algorithm
191208//
@@ -272,18 +289,18 @@ func (p *Pipeline) Reconcile(ds []object.Delta) ([]object.Delta, error) {
272289 // else: deleting non-existent object - silently skip
273290
274291 case hasAdds && hasDeletes :
275- // CRITICAL DISAMBIGUATION using cache state:
292+ // Disambiguate using cache state
276293 if ops .existedBefore {
277- // Object existed → this is an UPDATE (delete old + add new)
294+ // Object existed -> this is an UPDATE (delete old + add new)
278295 d := ops .adds [len (ops .adds )- 1 ]
279296 if err := p .targetCache .Add (d .Object ); err != nil {
280297 return nil , err
281298 }
282299 deltaCache [key ] = object.Delta {Object : d .Object , Type : object .Upserted }
283300 }
284- // else: Object didn't exist → TRANSIENT (add + delete in same batch = no-op)
285- // The object was created and destroyed within this delta batch,
286- // so it should never reach the target. Emit nothing.
301+ // else: Object didn't exist -> TRANSIENT (add + delete in same batch =
302+ // no-op) The object was created and destroyed within this delta batch, so
303+ // it should never reach the target. Emit nothing.
287304 }
288305 }
289306
0 commit comments