etcd-io/etcd · error

unexpected mod revision filter in delete

Error message

unexpected mod revision filter in delete

What it means

OpDelete rejects WithMinModRev / WithMaxModRev. Mod-rev filters restrict range reads to keys modified within a revision window; delete ranges operate on current state and cannot be filtered that way. The validation switch checks ret.minModRev != 0 || ret.maxModRev != 0 and panics with "unexpected mod revision filter in delete".

Source

Thrown at client/v3/op.go:289

		panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one")
	}
	ret := Op{t: tDeleteRange, key: []byte(key)}
	ret.applyOpts(opts)
	switch {
	case ret.leaseID != 0:
		panic("unexpected lease in delete")
	case ret.limit != 0:
		panic("unexpected limit in delete")
	case ret.rev != 0:
		panic("unexpected revision in delete")
	case ret.sort != nil:
		panic("unexpected sort in delete")
	case ret.serializable:
		panic("unexpected serializable in delete")
	case ret.countOnly:
		panic("unexpected countOnly in delete")
	case ret.minModRev != 0, ret.maxModRev != 0:
		panic("unexpected mod revision filter in delete")
	case ret.minCreateRev != 0, ret.maxCreateRev != 0:
		panic("unexpected create revision filter in delete")
	case ret.filterDelete, ret.filterPut:
		panic("unexpected filter in delete")
	case ret.createdNotify:
		panic("unexpected createdNotify in delete")
	}
	return ret
}

// OpPut returns "put" operation based on given key-value and operation options.
func OpPut(key, val string, opts ...OpOption) Op {
	ret := Op{t: tPut, key: []byte(key), val: []byte(val)}
	ret.applyOpts(opts)
	switch {
	case ret.end != nil:
		panic("unexpected range in put")
	case ret.limit != 0:

View on GitHub (pinned to f744d457f4)

Solutions

  1. Do the filtered listing with Get + WithMinModRev/WithMaxModRev, then delete the returned keys explicitly (Txn of OpDelete per key or batches).
  2. If deletion must be conditional on the key's revision, use a Txn If with CompareModified instead of a filter option.
  3. Remove the mod-rev options from the Delete call.

Example fix

// before
_, err := cli.Delete(ctx, "/tmp/", clientv3.WithPrefix(), clientv3.WithMaxModRev(watermarkRev)) // panics

// after
gr, err := cli.Get(ctx, "/tmp/", clientv3.WithPrefix(), clientv3.WithMaxModRev(watermarkRev))
ops := make([]clientv3.Op, 0, len(gr.Kvs))
for _, kv := range gr.Kvs {
    ops = append(ops, clientv3.OpDelete(string(kv.Key)))
}
_, err = cli.Txn(ctx).Then(ops...).Commit()
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: clientv3.OpDelete("/k/", clientv3.WithPrefix(), clientv3.WithMaxModRev(someRev)) — e.g. 'delete only entries not touched since rev N'; or option-slice reuse from an incremental-scan Get.

Common situations: Garbage-collection routines that first list stale keys with mod-rev filters and then try to apply the same filter to the purge Delete; sync tooling that carries filter options through one code path for both read and delete.

Related errors


AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15). Data as JSON: /api/errors/3dd8bcd353935030. Report an issue: GitHub.