--- /dev/null
+diff --git a/lib/cache/cdb_api.h b/lib/cache/cdb_api.h
+index 9e8a34d58..b589b564a 100644
+--- a/lib/cache/cdb_api.h
++++ b/lib/cache/cdb_api.h
+@@ -65,11 +65,15 @@ struct kr_cdb_api {
+ * \return error code - accepting RW transactions can fail with LMDB.
+ */
+ int (*commit)(kr_cdb_pt db, struct kr_cdb_stats *stat, bool accept_rw, bool reset_ro);
++ /** Run before a row of operations to ensure they happen in a single RW transaction,
++ * at least in case of successes. */
++ int (*txn_open_rw)(kr_cdb_pt db, struct kr_cdb_stats *stat/*unused*/);
+
+ /* Data access */
+
+ int (*read)(kr_cdb_pt db, struct kr_cdb_stats *stat,
+ const knot_db_val_t *key, knot_db_val_t *val, int maxcount);
++ /* TODO: the behavior of write() around transactions is a bit complex. */
+ int (*write)(kr_cdb_pt db, struct kr_cdb_stats *stat, const knot_db_val_t *key,
+ knot_db_val_t *val, int maxcount);
+
+@@ -111,13 +115,19 @@ struct kr_cdb_api {
+ int (*check_health)(kr_cdb_pt db, struct kr_cdb_stats *stat);
+
+
+- /** Start iterating; return the first *val with *key.
++ /** Start iterating: get the first *val with *key + return error code.
+ *
+- * This only makes sense if !is_cache.
+- * TODO: it only works inside RO transactions for now.
++ * - in cache: ensures a RO transaction (and commits the RW txn if any)
++ * - in ruledb: transaction is preserved if exists, otherwise RO txn gets opened
+ */
+ int (*it_first)(kr_cdb_pt db, struct kr_cdb_stats *stat,
+ const knot_db_val_t *key, knot_db_val_t *val);
+- /** Advance to the next *val with the same key. */
++ /** Advance to the next *val with the same key. Return error code. */
+ int (*it_next)(kr_cdb_pt db, struct kr_cdb_stats *stat, knot_db_val_t *val);
++ /** Delete the current *val + return error code.
++ *
++ * You can it_next() to continue.
++ * This assumed that you got it_first() in a RW txn.
++ */
++ int (*it_del)(kr_cdb_pt db, struct kr_cdb_stats *stat);
+ };
+diff --git a/lib/cache/cdb_lmdb.c b/lib/cache/cdb_lmdb.c
+index 0319b5cbb..6221c9492 100644
+--- a/lib/cache/cdb_lmdb.c
++++ b/lib/cache/cdb_lmdb.c
+@@ -38,12 +38,13 @@ struct lmdb_env
+ *
+ * - only one of (ro,rw) may be active at once
+ * - non-NULL .ro may be active or reset
+- * - non-NULL .rw is always active
++ * - ro_curs can survive an RW transaction and renewed later in an RO txn
++ * - non-NULL .rw is always active, non-NULL rw_curs is always active
+ */
+ struct {
+ bool ro_active, ro_curs_active;
+ MDB_txn *ro, *rw;
+- MDB_cursor *ro_curs;
++ MDB_cursor *ro_curs, *rw_curs;
+ } txn;
+
+ bool is_cache; /**< cache vs. rules; from struct kr_cdb_opts::is_cache */
+@@ -236,6 +237,7 @@ static int cdb_commit(kr_cdb_pt db, struct kr_cdb_stats *stats, bool accept_rw,
+ mdb_txn_abort(env->txn.rw);
+ }
+ env->txn.rw = NULL; /* the transaction got freed even in case of errors */
++ env->txn.rw_curs = NULL; /* auto-closed with txn if existed */
+ } else if (reset_ro && env->txn.ro && env->txn.ro_active) {
+ mdb_txn_reset(env->txn.ro);
+ env->txn.ro_active = false;
+@@ -244,21 +246,39 @@ static int cdb_commit(kr_cdb_pt db, struct kr_cdb_stats *stats, bool accept_rw,
+ return ret;
+ }
+
+-/** Obtain a read-only cursor (and a read-only transaction).
+- * TODO: allow RW transaction (for ruledb iterator) */
++static int cdb_txn_open_rw(kr_cdb_pt db, struct kr_cdb_stats *stat/*unused*/)
++{
++ struct lmdb_env *env = db2env(db);
++ MDB_txn *txn = NULL;
++ return txn_get(env, &txn, false);
++}
++
++/** Obtain a cursor (and a transaction).
++ *
++ * - for cache these are read only
++ * - for policy the RW transaction gets preserved if open */
+ static int txn_curs_get(struct lmdb_env *env, MDB_cursor **curs, struct kr_cdb_stats *stats)
+ {
+ if (kr_fails_assert(env && curs))
+ return kr_error(EINVAL);
+ if (env->txn.ro_curs_active)
+- goto success;
+- /* Only in a read-only txn; TODO: it's a bit messy/coupled
+- * At least for rules we don't do the auto-commit feature. */
+- if (env->txn.rw) {
+- if (!env->is_cache) return kr_error(EINPROGRESS);
++ goto success_ro;
++ /* Only in a read-only txn if for cache; TODO: it's a bit messy/coupled */
++ if (env->txn.rw && env->is_cache) {
+ int ret = cdb_commit(env2db(env), stats, true, false);
+ if (ret) return ret;
+ }
++ if (env->txn.rw && !env->is_cache) {
++ if (!env->txn.rw_curs) {
++ MDB_txn *txn = env->txn.rw;
++ int ret = mdb_cursor_open(txn, env->dbi, &env->txn.rw_curs);
++ if (ret) return lmdb_error(env, ret);
++ }
++ if (kr_fails_assert(env->txn.rw_curs))
++ return kr_error(EINVAL);
++ *curs = env->txn.rw_curs;
++ return kr_ok();
++ }
+ MDB_txn *txn = NULL;
+ int ret = txn_get(env, &txn, true);
+ if (ret) return ret;
+@@ -270,7 +290,7 @@ static int txn_curs_get(struct lmdb_env *env, MDB_cursor **curs, struct kr_cdb_s
+ }
+ if (ret) return lmdb_error(env, ret);
+ env->txn.ro_curs_active = true;
+-success:
++success_ro:
+ kr_assert(env->txn.ro_curs_active && env->txn.ro && env->txn.ro_active
+ && !env->txn.rw);
+ *curs = env->txn.ro_curs;
+@@ -301,6 +321,7 @@ static void txn_abort(struct lmdb_env *env)
+ if (env->txn.rw) {
+ mdb_txn_abort(env->txn.rw);
+ env->txn.rw = NULL; /* the transaction got freed even in case of errors */
++ env->txn.rw_curs = NULL; /* auto-closed with txn if existed */
+ }
+ }
+
+@@ -940,7 +961,7 @@ static int cdb_it_next(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t *
+ if (kr_fails_assert(db && val))
+ return kr_error(EINVAL);
+ struct lmdb_env *env = db2env(db);
+- if (kr_fails_assert(!env->is_cache && env->txn.ro_curs_active))
++ if (kr_fails_assert(!env->is_cache && (env->txn.ro_curs_active || env->txn.rw_curs)))
+ return kr_error(EINVAL);
+
+ MDB_cursor *curs = NULL;
+@@ -952,19 +973,35 @@ static int cdb_it_next(kr_cdb_pt db, struct kr_cdb_stats *stats, knot_db_val_t *
+ *val = val_mdb2knot(val2_m);
+ return kr_ok();
+ }
++static int cdb_it_del(kr_cdb_pt db, struct kr_cdb_stats *stats)
++{
++ if (kr_fails_assert(db))
++ return kr_error(EINVAL);
++ struct lmdb_env *env = db2env(db);
++ if (kr_fails_assert(!env->is_cache && env->txn.rw_curs))
++ return kr_error(EINVAL);
++
++ MDB_cursor *curs = NULL;
++ int ret = txn_curs_get(env, &curs, stats);
++ if (ret) return ret;
++ ret = mdb_cursor_del(curs, 0);
++ if (ret) return lmdb_error(env, ret);
++ return kr_ok();
++}
+
+
+ const struct kr_cdb_api *kr_cdb_lmdb(void)
+ {
+ static const struct kr_cdb_api api = {
+ "lmdb",
+- cdb_init, cdb_deinit, cdb_count, cdb_clear, cdb_commit,
++ cdb_init, cdb_deinit, cdb_count, cdb_clear,
++ cdb_commit, cdb_txn_open_rw,
+ cdb_readv, cdb_writev, cdb_remove,
+ cdb_match,
+ cdb_read_leq, cdb_read_less,
+ cdb_usage_percent, cdb_get_maxsize,
+ cdb_check_health,
+- cdb_it_first, cdb_it_next,
++ cdb_it_first, cdb_it_next, cdb_it_del,
+ };
+ return &api;
+ }
+diff --git a/lib/rules/api.c b/lib/rules/api.c
+index d9dbfcf1f..4c213d6f5 100644
+--- a/lib/rules/api.c
++++ b/lib/rules/api.c
+@@ -663,21 +663,27 @@ int kr_rule_local_data_merge(const knot_rrset_t *rrs, const kr_rule_tags_t tags,
+ uint8_t key_data[KEY_MAXLEN];
+ knot_db_val_t key = local_data_key(rrs, key_data, RULESET_DEFAULT);
+ knot_db_val_t val;
++ int ret = ruledb_op(txn_open_rw);
++ if (ret)
++ return kr_error(ret);
+ // Transaction: we assume that we're in a RW transaction already,
+ // so that here we already "have a lock" on the last version.
+- // FIXME: iterate over multiple tags, once iterator supports RW TXN
+- int ret = ruledb_op(read, &key, &val, 1);
++ // Multiple variants are possible, with different tags.
++ for (ret = ruledb_op(it_first, &key, &val); ret == 0; ret = ruledb_op(it_next, &val)) {
++ // we're looking for the same tag-set
++ kr_rule_tags_t tags_old;
++ if (deserialize_fails_assert(&val, &tags_old) || tags_old != tags)
++ continue;
++ kr_rule_opts_t opts_old;
++ if (deserialize_fails_assert(&val, &opts_old))
++ continue;
++ break;
++ }
+ if (abs(ret) == abs(ENOENT))
+ goto fallback;
+ if (ret)
+ return kr_error(ret);
+- // check tags
+- kr_rule_tags_t tags_old;
+- if (deserialize_fails_assert(&val, &tags_old) || tags_old != tags)
+- goto fallback;
+- kr_rule_opts_t opts_old;
+- if (deserialize_fails_assert(&val, &opts_old))
+- goto fallback;
++
+ // merge TTLs
+ uint32_t ttl;
+ if (deserialize_fails_assert(&val, &ttl))
+@@ -700,6 +706,10 @@ int kr_rule_local_data_merge(const knot_rrset_t *rrs, const kr_rule_tags_t tags,
+ mm_ctx_delete(mm);
+ return kr_error(ret);
+ }
++ // ATM ruledb does not overwrite, so we `remove` before `write`.
++ ret = ruledb_op(it_del);
++ if (ret)
++ return kr_error(ret);
+ // everything is ready to insert the merged RRset
+ ret = local_data_ins(key, &rrs_new, NULL, tags, opts);
+ mm_ctx_delete(mm);
+diff --git a/lib/rules/api.h b/lib/rules/api.h
+index feea98e09..c3af5901b 100644
+--- a/lib/rules/api.h
++++ b/lib/rules/api.h
+@@ -144,10 +144,8 @@ const uint32_t KR_RULE_TTL_DEFAULT;
+ KR_EXPORT
+ int kr_rule_local_data_ins(const knot_rrset_t *rrs, const knot_rdataset_t *sig_rds,
+ kr_rule_tags_t tags, kr_rule_opts_t opts);
+-/** Merge RRs into a local data rule.
++/** Merge RRs into a local data rule with the same set of tags.
+ *
+- * - FIXME: with multiple tags variants for the same name-type pair,
+- * you typically end up with a single RR per RRset
+ * - RRSIGs get dropped, if any were attached.
+ * - We assume that this is called with a RW transaction open already,
+ * which is always true in normal usage (long RW txn covering whole config).