diff --git a/bindings-sophia.cabal b/bindings-sophia.cabal
--- a/bindings-sophia.cabal
+++ b/bindings-sophia.cabal
@@ -1,5 +1,5 @@
 name:         bindings-sophia
-version:      0.2.0
+version:      0.2.0.2
 category:     Database
 
 author:       Eyal Lotem <eyal.lotem+sophia@gmail.com>
@@ -66,6 +66,8 @@
     cbits/wrappers.c
 
   cc-options: -g -Wall -O2
+
+  extra-libraries: pthread
 
 --------------------------------------------------------------------------------
 
diff --git a/sophia/README b/sophia/README
--- a/sophia/README
+++ b/sophia/README
@@ -1,5 +1,5 @@
 
-sophia - is an modern embeddable key-value database
-designed for a highload.
+sophia - is a modern embeddable key-value database
+designed for a high load.
 
 http://sphia.org
diff --git a/sophia/db/cat.c b/sophia/db/cat.c
--- a/sophia/db/cat.c
+++ b/sophia/db/cat.c
@@ -125,7 +125,7 @@
 	return 1;
 }
 
-inline sppage*
+sppage*
 sp_catfind(spcat *c, char *rkey, int size, uint32_t *index)
 {
 	register int min = 0;
diff --git a/sophia/db/core.h b/sophia/db/core.h
--- a/sophia/db/core.h
+++ b/sophia/db/core.h
@@ -24,6 +24,12 @@
 
 typedef enum spmagic spmagic;
 
+enum sptxn {
+	SPTSS, SPTMS
+};
+
+typedef enum sptxn sptxn;
+
 struct spenv {
 	spmagic m;
 	spe e;
@@ -45,47 +51,53 @@
 
 struct sp {
 	spmagic m;
-	spenv *e;
+	spenv *env;
 	spa a;
 	sprep rep;
+	sptxn txn;          /* transaction mode: single or multi-stmt */
 	spi *i, i0, i1;
+	spi itxn;
 	int iskip;          /* skip second index during read */
 	uint64_t psn;       /* page sequence number */
 	spcat s;
+	spbatch lb;         /* log batch related */
 	volatile int stop;
 	sptask merger;
 	sprefset refs;      /* pre allocated key buffer (page merge) */
+	spfile lockdb;      /* per-process database lock */
 	int lockc;          /* incremental cursor lock */
 	spspinlock lockr;   /* repository lock */
 	spspinlock locks;   /* space lock */
 	spspinlock locki;   /* index lock */
+	spe e, em;          /* separate error contexts: fe and merger */
 };
 
-int sp_rotate(sp*);
+int sp_rotate(sp*, spe*);
 
 static inline int sp_active(sp *s) {
 	return !s->stop;
 }
 
-static inline int
-sp_e(sp *s, int type, ...) {
-	va_list args;
-	va_start(args, type);
-	sp_ve(&s->e->e, type, args);
-	va_end(args);
-	return -1;
+static inline spi*
+sp_ipair(sp *s) {
+	return (s->i == &s->i0 ? &s->i1 : &s->i0);
 }
 
-static inline int
-sp_ee(spenv *e, int type, ...) {
-	va_list args;
-	va_start(args, type);
-	sp_ve(&e->e, type, args);
-	va_end(args);
-	return -1;
+static inline spi*
+sp_iswap(sp *s) {
+	spi *old = s->i;
+	s->i = sp_ipair(s);
+	return old;
 }
 
 static inline void
+sp_iskipset(sp *s, int v) {
+	sp_lock(&s->locki);
+	s->iskip = v;
+	sp_unlock(&s->locki);
+}
+
+static inline void
 sp_glock(sp *s) {
 	if (s->lockc > 0)
 		return;
@@ -107,23 +119,36 @@
 	sp_unlock(&s->lockr);
 }
 
-static inline void
-sp_iskipset(sp *s, int v) {
-	sp_lock(&s->locki);
-	s->iskip = v;
-	sp_unlock(&s->locki);
+static inline int
+sp_evalidate(sp *s) {
+	return sp_echeck(&s->e) + sp_echeck(&s->em);
 }
 
-static inline spi*
-sp_ipair(sp *s) {
-	return (s->i == &s->i0 ? &s->i1 : &s->i0);
+static inline int
+sp_e(sp *s, int type, ...) {
+	va_list args;
+	va_start(args, type);
+	sp_vef(&s->e, type, args);
+	va_end(args);
+	return -1;
 }
 
-static inline spi*
-sp_iswap(sp *s) {
-	spi *old = s->i;
-	s->i = sp_ipair(s);
-	return old;
+static inline int
+sp_em(sp *s, int type, ...) {
+	va_list args;
+	va_start(args, type);
+	sp_vef(&s->em, type, args);
+	va_end(args);
+	return -1;
+}
+
+static inline int
+sp_ee(spenv *e, int type, ...) {
+	va_list args;
+	va_start(args, type);
+	sp_vef(&e->e, type, args);
+	va_end(args);
+	return -1;
 }
 
 #endif
diff --git a/sophia/db/cursor.c b/sophia/db/cursor.c
--- a/sophia/db/cursor.c
+++ b/sophia/db/cursor.c
@@ -22,7 +22,6 @@
 	assert(c->ph->id > 0);
 	c->pvi = 0 ;
 	c->pv  = (spvh*)((char*)c->ph + sizeof(sppageh));
-	c->dup = 0;
 }
 
 static inline void sp_pageclose(spc *c) {
@@ -49,7 +48,7 @@
 		mid = min + ((max - min) >> 1);
 		spvh *a = (spvh*)((char*)c->ph + sizeof(sppageh) + c->ph->bsize * mid);
 		register int rc =
-			c->s->e->cmp(a->key, a->size, rkey, size, c->s->e->cmparg);
+			c->s->env->cmp(a->key, a->size, rkey, size, c->s->env->cmparg);
 		switch (rc) {
 		case -1: min = mid + 1;
 			continue;
@@ -80,14 +79,10 @@
 	} else {
 		match = min;
 	}
-	/* not found, set minimal */
-	if (match >= c->ph->count) {
-		c->pv  = (spvh*)((char*)c->ph + sizeof(sppageh));
-		c->pvi = 0;
-	} else {
-		c->pv  = (spvh*)((char*)c->ph + sizeof(sppageh) + c->ph->bsize * match);
-		c->pvi = match;
-	}
+	if (match >= c->ph->count)
+		match = c->ph->count; /* skip */
+	c->pv  = (spvh*)((char*)c->ph + sizeof(sppageh) + c->ph->bsize * match);
+	c->pvi = match;
 	return eq;
 }
 
@@ -103,7 +98,9 @@
 	} else {
 		match = max;
 	}
-	/* not found, set max */
+	if (match < 0)
+		match = c->ph->count; /* skip */
+	else
 	if (match >= c->ph->count)
 		match = c->ph->count - 1;
 	c->pv  = (spvh*)((char*)c->ph + sizeof(sppageh) + c->ph->bsize * match);
@@ -144,18 +141,22 @@
 	sp_iopen(&c->i0, c->s->i);
 	if (! c->s->iskip)
 		sp_iopen(&c->i1, sp_ipair(c->s));
+	if (c->s->txn == SPTMS)
+		sp_iopen(&c->itxn, &c->s->itxn);
 	if (spunlikely(c->s->s.count == 0))
 		return;
 	sp_pageopen(c, 0);
 }
 
 static inline int
-sp_firstkey(spc *c, char *rkey, int size)
+sp_lte(spc *c, char *rkey, int size)
 {
 	/* do lte search on all indexes for the key */
 	int eq = sp_ilte(c->s->i, &c->i0, rkey, size);
 	if (! c->s->iskip)
 		eq = eq + sp_ilte(sp_ipair(c->s), &c->i1, rkey, size);
+	if (c->s->txn == SPTMS)
+		eq = eq + sp_ilte(&c->s->itxn, &c->itxn, rkey, size);
 	/* match page for the key */
 	if (spunlikely(c->s->s.count == 0))
 		return eq;
@@ -166,6 +167,8 @@
 	assert(p != NULL);
 	sp_pageopen(c, idx);
 	eq = eq + sp_pagesetlte(c, rkey, size);
+	if (spunlikely(c->pvi == c->ph->count))
+		sp_pageclose(c);
 	return eq;
 }
 
@@ -177,18 +180,25 @@
 		sp_iopen(&c->i1, sp_ipair(c->s));
 		sp_ilast(&c->i1);
 	}
+	if (c->s->txn == SPTMS) {
+		sp_iopen(&c->itxn, &c->s->itxn);
+		sp_ilast(&c->itxn);
+	}
 	if (spunlikely(c->s->s.count == 0))
 		return;
 	sp_pageopen(c, c->s->s.count-1);
 	sp_pagesetlast(c);
 }
 
-static inline int sp_lastkey(spc *c, char *rkey, int size)
+static inline int
+sp_gte(spc *c, char *rkey, int size)
 {
 	/* do gte search the index for the key */
 	int eq = sp_igte(c->s->i, &c->i0, rkey, size);
 	if (! c->s->iskip)
 		eq = eq + sp_igte(sp_ipair(c->s), &c->i1, rkey, size);
+	if (c->s->txn == SPTMS)
+		eq = eq + sp_igte(&c->s->itxn, &c->itxn, rkey, size);
 	/* match page for the key */
 	if (spunlikely(c->s->s.count == 0))
 		return eq;
@@ -199,6 +209,8 @@
 	assert(p != NULL);
 	sp_pageopen(c, idx);
 	eq = eq + sp_pagesetgte(c, rkey, size);
+	if (spunlikely(c->pvi == c->ph->count))
+		sp_pageclose(c);
 	return eq;
 }
 
@@ -210,14 +222,14 @@
 	c->m = SPMCUR;
 	c->o = o;
 	c->s = s;
-	c->dup = 0;
+	c->mask = SPCNONE;
 	c->p = NULL;
 	c->ph = NULL;
 	c->pi = 0;
 	c->pvi = 0;
 	c->pv = NULL;
-	c->vsrc = SPCNONE;
 
+	sp_iinv(&c->s->itxn, &c->itxn);
 	sp_iinv(c->s->i, &c->i0);
 	sp_iinv(sp_ipair(c->s), &c->i1);
 
@@ -227,7 +239,7 @@
 	case SPGT:
 		if (rkey) {
 			/* init first key and skip on gt */
-			if (sp_firstkey(c, rkey, size) && o == SPGT)
+			if (sp_lte(c, rkey, size) && o == SPGT)
 				sp_iterate(c);
 			return;
 		}
@@ -237,7 +249,7 @@
 	case SPLT:
 		if (rkey) {
 			/* init last key and skip on lt */
-			if (sp_lastkey(c, rkey, size) && o == SPLT)
+			if (sp_gte(c, rkey, size) && o == SPLT)
 				sp_iterate(c);
 			return;
 		}
@@ -253,187 +265,184 @@
 }
 
 static inline void sp_reset(spc *c) {
-	c->vsrc = SPCNONE;
+	c->mask   = SPCNONE;
 	c->r.type = SPREFNONE;
-	c->dup = 0;
 }
 
 static inline int sp_next(spc *c)
 {
-	switch (c->vsrc) {
-	case SPCI0:
+	/* apply last iteration skip mask */
+	if (c->mask & SPCITXN)
+		sp_inext(&c->itxn);
+	if (c->mask & SPCI0)
 		sp_inext(&c->i0);
-		if (c->dup & SPCPDUP)
-			sp_pagenext(c);
-		if (c->dup & SPCVDUP)
-			sp_inext(&c->i1);
-		break;
-	case SPCI1:
+	if (c->mask & SPCI1)
 		sp_inext(&c->i1);
-		if (c->dup & SPCPDUP)
-			sp_pagenext(c);
-		if (c->dup & SPCVDUP)
-			sp_inext(&c->i0);
-		break;
-	case SPCP:
+	if (c->mask & SPCP) {
 		assert(c->p != NULL);
 		assert(c->r.v.vh == c->pv);
 		sp_pagenext(c);
-		break;
-	case SPCNONE:
-		break;
 	}
 	sp_reset(c);
-	
+
+	/* do priority multi-index LTE search */
 	int rc;
-	spcsrc src = SPCNONE;
-	spv *v0 = sp_ival(&c->i0);
-	spv *v1 = sp_ival(&c->i1);
-	spv *v  = NULL;
+	int dup = SPCNONE;
+	int src = SPCNONE;
+	spv *v = NULL;
+	struct {
+		spv *v;
+		int mask;
+	} vv[3] = {
+		{ sp_ival(&c->itxn), SPCITXN },
+		{ sp_ival(&c->i0),   SPCI0   },
+		{ sp_ival(&c->i1),   SPCI1   }
+	};
 
-	/* END */
-	if (v0 == NULL && v1 == NULL && c->pv == NULL) {
-		c->vsrc = SPCNONE;
+	/* end of iteration */
+	if (spunlikely(vv[0].v == NULL &&
+	               vv[1].v == NULL &&
+	               vv[2].v == NULL && c->pv == NULL)) {
 		return 0;
 	}
 
-	if (v0 && v1) {
-		rc = c->s->e->cmp(v0->key, v0->size, v1->key, v1->size,
-						  c->s->e->cmparg);
+	int i;
+	for (i = 0; i < 3; i++) {
+		if (vv[i].v == NULL)
+			continue;
+		if (v == NULL) {
+			src = vv[i].mask;
+			v = vv[i].v;
+			continue;
+		}
+		rc = c->s->env->cmp(vv[i].v->key, vv[i].v->size, v->key, v->size,
+		                    c->s->env->cmparg);
 		switch (rc) {
-		case  0: c->dup |= SPCVDUP;
-		case -1: v = v0, src = SPCI0;
+		case  0: dup |= vv[i].mask;
 			break;
-		case  1: v = v1, src = SPCI1;
+		case -1:
+			src = vv[i].mask;
+			v = vv[i].v;
 			break;
 		}
-	} else if (v0) {
-		v = v0, src = SPCI0;
-	} else if (v1) {
-		v = v1, src = SPCI1;
 	}
-
 	/* no page key */
-	if (c->pv == NULL) {
-		c->vsrc = src;
-		c->r.type = SPREFM;
-		c->r.v.v = v; 
-		return 1;
-	}
+	if (c->pv == NULL)
+		goto index;
 	/* no index key */
-	if (v == NULL) {
-		c->vsrc = SPCP;
-		c->r.type = SPREFD;
-		c->r.v.vh = c->pv;
-		return 1;
-	}
+	if (v == NULL)
+		goto page;
 
-	/* compare in-memory key and page one's */
-	rc = c->s->e->cmp(v->key, v->size, c->pv->key, c->pv->size,
-	                  c->s->e->cmparg);
+	/* compare in-memory and on-disk key */
+	rc = c->s->env->cmp(v->key, v->size, c->pv->key, c->pv->size,
+	                    c->s->env->cmparg);
 	switch (rc) {
-	case 0: c->dup |= SPCPDUP;
-	case -1:
-		c->vsrc = src;
-		c->r.type = SPREFM;
-		c->r.v.v = v; 
-		break;
-	case 1:
-		c->vsrc = SPCP;
-		c->r.type = SPREFD;
-		c->r.v.vh = c->pv;
-		break;
+	case  0: dup |= SPCP;
+	case -1: goto index;
+	case  1: goto page;
 	}
+
+	/* unreach */
+	assert(0);
 	return 1;
+index:
+	c->mask   = dup|src;
+	c->r.type = SPREFM;
+	c->r.v.v  = v;
+	return 1;
+page:
+	c->mask   = dup|SPCP;
+	c->r.type = SPREFD;
+	c->r.v.vh = c->pv;
+	return 1;
 }
 
 static inline int sp_prev(spc *c)
 {
-	switch (c->vsrc) {
-	case SPCI0:
+	/* apply last iteration skip mask */
+	if (c->mask & SPCITXN)
+		sp_iprev(&c->itxn);
+	if (c->mask & SPCI0)
 		sp_iprev(&c->i0);
-		if (c->dup & SPCPDUP)
-			sp_pageprev(c);
-		if (c->dup & SPCVDUP)
-			sp_iprev(&c->i1);
-		break;
-	case SPCI1:
+	if (c->mask & SPCI1)
 		sp_iprev(&c->i1);
-		if (c->dup & SPCPDUP)
-			sp_pageprev(c);
-		if (c->dup & SPCVDUP)
-			sp_iprev(&c->i0);
-		break;
-	case SPCP:
+	if (c->mask & SPCP) {
 		assert(c->p != NULL);
 		assert(c->r.v.vh == c->pv);
 		sp_pageprev(c);
-		break;
-	case SPCNONE:
-		break;
 	}
 	sp_reset(c);
-	
+
+	/* do priority multi-index LTE search */
 	int rc;
-	spcsrc src = SPCNONE;
-	spv *v0 = sp_ival(&c->i0);
-	spv *v1 = sp_ival(&c->i1);
-	spv *v  = NULL;
+	int dup = SPCNONE;
+	int src = SPCNONE;
+	spv *v = NULL;
+	struct {
+		spv *v;
+		int mask;
+	} vv[3] = {
+		{ sp_ival(&c->itxn), SPCITXN },
+		{ sp_ival(&c->i0),   SPCI0   },
+		{ sp_ival(&c->i1),   SPCI1   }
+	};
 
-	/* END */
-	if (v0 == NULL && v1 == NULL && c->pv == NULL) {
-		c->vsrc = SPCNONE;
+	/* end of iteration */
+	if (spunlikely(vv[0].v == NULL &&
+	               vv[1].v == NULL &&
+	               vv[2].v == NULL && c->pv == NULL)) {
 		return 0;
 	}
 
-	if (v0 && v1) {
-		rc = c->s->e->cmp(v0->key, v0->size, v1->key, v1->size,
-						  c->s->e->cmparg);
+	int i;
+	for (i = 0; i < 3; i++) {
+		if (vv[i].v == NULL)
+			continue;
+		if (v == NULL) {
+			src = vv[i].mask;
+			v = vv[i].v;
+			continue;
+		}
+		rc = c->s->env->cmp(vv[i].v->key, vv[i].v->size, v->key, v->size,
+		                    c->s->env->cmparg);
 		switch (rc) {
-		case  0: c->dup |= SPCVDUP;
-		case -1: v = v1, src = SPCI1;
+		case 0: dup |= vv[i].mask;
 			break;
-		case  1: v = v0, src = SPCI0;
+		case 1:
+			src = vv[i].mask;
+			v = vv[i].v;
 			break;
 		}
-	} else if (v0) {
-		v = v0, src = SPCI0;
-	} else if (v1) {
-		v = v1, src = SPCI1;
 	}
-
 	/* no page key */
-	if (c->pv == NULL) {
-		c->vsrc = src;
-		c->r.type = SPREFM;
-		c->r.v.v = v; 
-		return 1;
-	}
+	if (c->pv == NULL)
+		goto index;
 	/* no index key */
-	if (v == NULL) {
-		c->vsrc = SPCP;
-		c->r.type = SPREFD;
-		c->r.v.vh = c->pv;
-		return 1;
-	}
+	if (v == NULL)
+		goto page;
 
-	/* compare in-memory key and page one's */
-	rc = c->s->e->cmp(v->key, v->size, c->pv->key, c->pv->size,
-	                  c->s->e->cmparg);
+	/* compare in-memory and on-disk key */
+	rc = c->s->env->cmp(v->key, v->size, c->pv->key, c->pv->size,
+	                    c->s->env->cmparg);
 	switch (rc) {
-	case  0: c->dup |= SPCPDUP;
-	case  1:
-		c->vsrc = src;
-		c->r.type = SPREFM;
-		c->r.v.v = v; 
-		break;
-	case -1:
-		c->vsrc = SPCP;
-		c->r.type = SPREFD;
-		c->r.v.vh = c->pv;
-		break;
+	case  0: dup |= SPCP;
+	case  1: goto index;
+	case -1: goto page;
 	}
+
+	/* unreach */
+	assert(0);
 	return 1;
+index:
+	c->mask   = dup|src;
+	c->r.type = SPREFM;
+	c->r.v.v  = v;
+	return 1;
+page:
+	c->mask   = dup|SPCP;
+	c->r.type = SPREFD;
+	c->r.v.vh = c->pv;
+	return 1;
 }
 
 int sp_iterate(spc *c) {
@@ -475,19 +484,26 @@
 
 int sp_match(sp *s, void *k, size_t ksize, void **v, size_t *vsize)
 {
-	/* I. match both in-memory index'es for the key */
+	/*
+	 * if multi-stmt transaction is active, try to search the key
+	 * in the transaction findex first */
+	int rc;
+	if (s->txn == SPTMS) {
+		rc = sp_matchi(s, &s->itxn, k, ksize, v, vsize);
+		if (rc == -1 || rc == 1)
+			return rc;
+	}
+	/* match both in-memory index'es for the key */
 	sp_lock(&s->locki);
-	int rc = sp_matchi(s, s->i, k, ksize, v, vsize);
+	rc = sp_matchi(s, s->i, k, ksize, v, vsize);
 	if (rc == -1 || rc == 1) {
 		sp_unlock(&s->locki);
 		return rc;
 	}
-	/*
-	 * skip the second index if it is been truncating at the
-	 * somement, all updates are on disk pages.
-	*/
+	/* skip the second index if it is been truncating at the
+	 * somement, all updates are on disk pages */
 	if (! s->iskip) {
-		int rc = sp_matchi(s, sp_ipair(s), k, ksize, v, vsize);
+		rc = sp_matchi(s, sp_ipair(s), k, ksize, v, vsize);
 		if (rc == -1 || rc == 1) {
 			sp_unlock(&s->locki);
 			return rc;
@@ -495,7 +511,7 @@
 	}
 	sp_unlock(&s->locki);
 
-	/* II. match the page */
+	/* match the page */
 	sp_lock(&s->locks);
 	uint32_t page = 0;
 	sppage *p = sp_catfind(&s->s, k, ksize, &page);
@@ -504,7 +520,7 @@
 		return 0;
 	}
 
-	/* III. match the key in the page */
+	/* match the key in the page */
 	spepoch *e = (spepoch*)p->epoch;
 	sp_lock(&e->lock);
 
@@ -518,7 +534,7 @@
 		register spvh *v =
 			(spvh*)(e->db.map + p->offset + sizeof(sppageh) +
 			        ph->bsize * mid);
-		switch (s->e->cmp(v->key, v->size, k, ksize, s->e->cmparg)) {
+		switch (s->env->cmp(v->key, v->size, k, ksize, s->env->cmparg)) {
 		case -1: min = mid + 1;
 			continue;
 		case  1: max = mid - 1;
diff --git a/sophia/db/cursor.h b/sophia/db/cursor.h
--- a/sophia/db/cursor.h
+++ b/sophia/db/cursor.h
@@ -11,30 +11,24 @@
 
 typedef struct spc spc;
 
-enum spcsrc {
-	SPCNONE,
-	SPCI0,
-	SPCI1,
-	SPCP
-};
-
-#define SPCVDUP 1
-#define SPCPDUP 2
-
-typedef enum spcsrc spcsrc;
+#define SPCNONE  0
+#define SPCITXN  1
+#define SPCI0    2
+#define SPCI1    4
+#define SPCP    16
 
 struct spc {
 	spmagic m;
 	sporder o;
 	sp *s;
 	spii i0, i1;
-	int dup;     /* last iteration duplicate flags */
+	spii itxn;
 	sppageh *ph;
 	sppage *p;
 	int pi;      /* page space index */
 	spvh *pv;
 	int pvi;     /* version page index */
-	spcsrc vsrc; /* last iteration source */
+	int mask;    /* last iteration advance mask */
 	spref r;     /* last iteration result */
 };
 
diff --git a/sophia/db/e.c b/sophia/db/e.c
--- a/sophia/db/e.c
+++ b/sophia/db/e.c
@@ -9,39 +9,48 @@
 
 #include <sp.h>
 
-void sp_ve(spe *e, int type, va_list args)
+void sp_vef(spe *e, int type, va_list args)
 {
 	sp_lock(&e->lock);
-	if (e->type != SPENONE) {
+	if (e->type & SPEF) {
 		sp_unlock(&e->lock);
 		return;
 	}
+	const char *fmt;
+	int len;
+	uint32_t epoch;
+
 	e->type = type;
-	switch (type) {
+	switch (type & ~SPEF) {
 	case SPE: {
-		char *fmt = va_arg(args, char*);
-		int len = snprintf(e->e, sizeof(e->e), "error: ");
+		fmt = va_arg(args, const char*);
+		len = snprintf(e->e, sizeof(e->e), "error: ");
 		vsnprintf(e->e + len, sizeof(e->e) - len, fmt, args);
 		break;
 	}
 	case SPEOOM: {
-		char *msg = va_arg(args, char*);
-		snprintf(e->e, sizeof(e->e), "out-of-memory error: %s", msg);
+		fmt = va_arg(args, const char*);
+		len = snprintf(e->e, sizeof(e->e), "out-of-memory error: ");
+		vsnprintf(e->e + len, sizeof(e->e) - len, fmt, args);
 		break;
 	}
 	case SPESYS: {
 		e->errno_ = errno;
-		char *msg = va_arg(args, char*);
-		snprintf(e->e, sizeof(e->e), "system error: %s (errno: %d, %s)",
-		         msg, e->errno_, strerror(e->errno_));
+		fmt  = va_arg(args, const char*);
+		len  = snprintf(e->e, sizeof(e->e), "system error: ");
+		len += vsnprintf(e->e + len, sizeof(e->e) - len, fmt, args);
+		snprintf(e->e + len, sizeof(e->e) - len, " (errno: %d, %s)",
+		         e->errno_, strerror(e->errno_));
 		break;
 	}
 	case SPEIO: {
 		e->errno_ = errno;
-		char *msg = va_arg(args, char*);
-		uint32_t epoch = va_arg(args, uint32_t);
-		snprintf(e->e, sizeof(e->e), "io error: [epoch %"PRIu32"] %s (errno: %d, %s)",
-		         epoch, msg, e->errno_, strerror(e->errno_));
+		epoch = va_arg(args, uint32_t);
+		fmt  = va_arg(args, const char*);
+		len  = snprintf(e->e, sizeof(e->e), "io error: [epoch %"PRIu32"] ", epoch);
+		len += vsnprintf(e->e + len, sizeof(e->e) - len, fmt, args);
+		snprintf(e->e + len, sizeof(e->e) - len, " (errno: %d, %s)",
+		         e->errno_, strerror(e->errno_));
 		break;
 	}
 	}
diff --git a/sophia/db/e.h b/sophia/db/e.h
--- a/sophia/db/e.h
+++ b/sophia/db/e.h
@@ -11,13 +11,12 @@
 
 typedef struct spe spe;
 
-enum {
-	SPENONE,
-	SPE,     /* general error (with format) */
-	SPEOOM,  /* out of memory */
-	SPESYS,  /* system + errno */
-	SPEIO    /* system-io + errno */
-};
+#define SPENONE 0
+#define SPE     1
+#define SPEOOM  2
+#define SPESYS  4
+#define SPEIO   8
+#define SPEF    16
 
 struct spe {
 	spspinlock lock;
@@ -26,24 +25,77 @@
 	char e[256];
 };
 
-static inline void sp_einit(spe *e) {
-	e->lock = 0;
+static inline void
+sp_einit(spe *e) {
 	e->type = SPENONE;
 	e->e[0] = 0;
 	sp_lockinit(&e->lock);
 }
 
-static inline void sp_efree(spe *e) {
+static inline void
+sp_efree(spe *e) {
 	sp_lockfree(&e->lock);
 }
 
-static inline int sp_eis(spe *e) {
+static inline int
+sp_eis(spe *e) {
 	sp_lock(&e->lock);
 	register int is = e->type != SPENONE;
 	sp_unlock(&e->lock);
 	return is;
 }
 
-void sp_ve(spe *e, int type, va_list args);
+static inline void
+sp_esetfatal(spe *e) {
+	sp_lock(&e->lock);
+	assert(e->type != SPENONE);
+	e->type |= SPEF;
+	sp_unlock(&e->lock);
+}
+
+static inline int
+sp_eisfatal(spe *e) {
+	sp_lock(&e->lock);
+	register int is = e->type != SPENONE;
+	sp_unlock(&e->lock);
+	return is;
+}
+
+static inline int
+sp_echeck(spe *e) {
+	sp_lock(&e->lock);
+	register int panic = 0;
+	if (spunlikely(e->type != SPENONE)) {
+		panic = (e->type & SPEF) > 0;
+		if (splikely(! panic)) {
+			e->type = SPENONE;
+			e->e[0] = 0;
+		}
+	}
+	sp_unlock(&e->lock);
+	return panic;
+}
+
+static inline void
+sp_edup(spe *dst, spe *src) {
+	sp_lock(&dst->lock);
+	sp_lock(&src->lock);
+	dst->type = src->type;
+	dst->errno_ = src->errno_;
+	memcpy(dst->e, src->e, sizeof(dst->e));
+	sp_unlock(&src->lock);
+	sp_unlock(&dst->lock);
+}
+
+void sp_vef(spe *e, int type, va_list args);
+
+static inline int
+sp_ef(spe *e, int type, ...) {
+	va_list args;
+	va_start(args, type);
+	sp_vef(e, type, args);
+	va_end(args);
+	return -1;
+}
 
 #endif
diff --git a/sophia/db/file.c b/sophia/db/file.c
--- a/sophia/db/file.c
+++ b/sophia/db/file.c
@@ -158,14 +158,12 @@
 static inline int
 sp_filecomplete(spfile *f)
 {
-	if (f->creat == 0)
-		return 0;
-	/* remove .incomplete part */
 	f->creat = 0;
 	char path[1024];
 	snprintf(path, sizeof(path), "%s", f->file);
-	char *ext = strrchr(path, '.');
-	assert(ext != NULL);
+	char *ext = strstr(path, ".incomplete");
+	if (spunlikely(ext == NULL))
+		return 0;
 	*ext = 0;
 	int rc = rename(f->file, path);
 	if (spunlikely(rc == -1))
@@ -212,14 +210,15 @@
 
 int sp_mapensure(spfile *f, uint64_t size, float grow)
 {
-	if (splikely((f->used + size) < f->size))
+	if (splikely((f->used + size) <= f->size))
 		return 0;
 	int rc = sp_unmap(f);
 	if (spunlikely(rc == -1))
 		return -1;
-	long double nsz = f->size * grow;
+	uint64_t nsz = f->used + f->size * grow;
 	if (spunlikely(nsz < size))
-		nsz = size;
+		nsz = f->used + size;
+	assert(nsz >= (f->used + size));
 	rc = sp_mapresize(f, nsz);
 	if (spunlikely(rc == -1))
 		return -1;
@@ -298,18 +297,34 @@
 	return sp_filerm(f->file);
 }
 
-int sp_logflush(spfile *f)
+int sp_logwrite(spfile *f, void *buf, size_t size)
 {
-	register struct iovec *v = f->iov;
+	size_t n = 0;
+	do {
+		ssize_t r;
+		do {
+			r = write(f->fd, (char*)buf + n, size - n);
+		} while (r == -1 && errno == EINTR);
+		if (r <= 0)
+			return -1;
+		n += r;
+	} while (n != size);
+	f->used += size;
+	return 0;
+}
+
+int sp_logput(spfile *f, spbatch *b)
+{
+	register struct iovec *v = b->iov;
 	register uint64_t size = 0;
-	register int n = f->iovc;
+	register int n = b->iovc;
 	do {
 		int r;
 		do {
 			r = writev(f->fd, v, n);
 		} while (r == -1 && errno == EINTR);
 		if (r < 0) {
-			f->iovc = 0;
+			b->iovc = 0;
 			return -1;
 		}
 		size += r;
@@ -326,13 +341,12 @@
 		}
 	} while (n > 0);
 	f->used += size;
-	f->iovc = 0;
+	b->iovc = 0;
 	return 0;
 }
 
 int sp_logrlb(spfile *f)
 {
-	assert(f->iovc == 0);
 	int rc = ftruncate(f->fd, f->svp);
 	if (spunlikely(rc == -1))
 		return -1;
@@ -345,11 +359,51 @@
 {
 	sp_filesvp(f);
 	speofh eof = { SPEOF };
-	sp_logadd(f, (char*)&eof, sizeof(eof));
-	int rc = sp_logflush(f);
+	int rc = sp_logwrite(f, &eof, sizeof(eof));
 	if (spunlikely(rc == -1)) {
 		sp_logrlb(f);
 		return -1;
 	}
 	return 0;
+}
+
+int sp_lockfile(spfile *f, char *path)
+{
+	f->creat = 0;
+	f->fd = open(path, O_CREAT|O_WRONLY, 0600);
+	if (spunlikely(f->fd == -1))
+		return -1;
+	f->file = sp_strdup(f->a, path);
+	if (spunlikely(f->file == NULL)) {
+		close(f->fd);
+		f->fd = -1;
+		return -1;
+	}
+	struct flock l;
+	memset(&l, 0, sizeof(l));
+	l.l_whence = SEEK_SET;
+	l.l_start = 0;
+	l.l_len = 0;
+	l.l_type = F_WRLCK;
+	int rc = fcntl(f->fd, F_SETLK, &l);
+	if (spunlikely(rc == -1)) {
+		sp_fileclose(f);
+		return 1;
+	}
+	return 0;
+}
+
+int sp_unlockfile(spfile *f)
+{
+	if (spunlikely(f->fd == -1))
+		return 0;
+	struct flock l;
+	memset(&l, 0, sizeof(l));
+	l.l_whence = SEEK_SET;
+	l.l_start = 0;
+	l.l_len = 0;
+	l.l_type = F_UNLCK;
+	fcntl(f->fd, F_SETLK, &l);
+	unlink(f->file);
+	return sp_fileclose(f);
 }
diff --git a/sophia/db/file.h b/sophia/db/file.h
--- a/sophia/db/file.h
+++ b/sophia/db/file.h
@@ -9,8 +9,16 @@
  * BSD License
 */
 
+typedef struct spbatch spbatch;
 typedef struct spfile spfile;
 
+#define SPIOVMAX 1024
+
+struct spbatch {
+	struct iovec iov[SPIOVMAX];
+	int iovc;
+};
+
 struct spfile {
 	spa *a;
 	int creat;
@@ -20,13 +28,18 @@
 	char *file;
 	int fd;
 	char *map;
-	struct iovec iov[8];
-	int iovc;
 };
 
 int sp_fileexists(char*);
 int sp_filerm(char*);
 
+static inline int
+sp_epochrm(char *dir, uint32_t epoch, char *ext) {
+	char path[1024];
+	snprintf(path, sizeof(path), "%s/%"PRIu32".%s", dir, epoch, ext);
+	return sp_filerm(path);
+}
+
 static inline void
 sp_fileinit(spfile *f, spa *a) {
 	memset(f, 0, sizeof(*f));
@@ -78,29 +91,41 @@
 	return off <= f->size;
 }
 
+static inline void
+sp_batchinit(spbatch *b) {
+	b->iovc = 0;
+}
+
+static inline int
+sp_batchhas(spbatch *b) {
+	return b->iovc > 0;
+}
+
+static inline int
+sp_batchensure(spbatch *b, int count) {
+	return (b->iovc + count) < SPIOVMAX;
+}
+
+static inline void
+sp_batchadd(spbatch *b, void *ptr, size_t size) {
+	assert(b->iovc < SPIOVMAX);
+	b->iov[b->iovc].iov_base = ptr;
+	b->iov[b->iovc].iov_len = size;
+	b->iovc++;
+}
+
 int sp_lognew(spfile*, char*, uint32_t);
 int sp_logcontinue(spfile*, char*, uint32_t);
 int sp_logclose(spfile*);
 int sp_logcomplete(spfile*);
 int sp_logcompleteforce(spfile*);
 int sp_logunlink(spfile*);
-int sp_logflush(spfile*);
+int sp_logwrite(spfile*, void*, size_t);
+int sp_logput(spfile*, spbatch*);
 int sp_logrlb(spfile*);
 int sp_logeof(spfile*);
 
-static inline void
-sp_logadd(spfile *f, const void *buf, int size) {
-	assert(f->iovc < 8);
-	f->iov[f->iovc].iov_base = (void*)buf;
-	f->iov[f->iovc].iov_len = size;
-	f->iovc++;
-}
-
-static inline int
-sp_epochrm(char *dir, uint32_t epoch, char *ext) {
-	char path[1024];
-	snprintf(path, sizeof(path), "%s/%"PRIu32".%s", dir, epoch, ext);
-	return sp_filerm(path);
-}
+int sp_lockfile(spfile*, char*);
+int sp_unlockfile(spfile*);
 
 #endif
diff --git a/sophia/db/gc.c b/sophia/db/gc.c
--- a/sophia/db/gc.c
+++ b/sophia/db/gc.c
@@ -19,7 +19,7 @@
 	for (;;)
 	{
 		sp_lock(&s->lockr);
-		spepoch *g = sp_repgc(&s->rep, s->e->gcfactor);
+		spepoch *g = sp_repgc(&s->rep, s->env->gcfactor);
 		sp_unlock(&s->lockr);
 		if (g == NULL)
 			break;
@@ -32,10 +32,10 @@
 			/* map origin page and copy to db file */
 			sppageh *h = (sppageh*)(g->db.map + p->offset);
 			sp_lock(&x->lock);
-			rc = sp_mapensure(&x->db, sizeof(sppageh) + h->size, s->e->dbgrow);
+			rc = sp_mapensure(&x->db, sizeof(sppageh) + h->size, s->env->dbgrow);
 			if (spunlikely(rc == -1)) {
 				sp_unlock(&x->lock);
-				return sp_e(s, SPEIO, "failed to remap db file", x->epoch);
+				return sp_em(s, SPEIO|SPEF, x->epoch, "failed to remap db file");
 			}
 			sp_unlock(&x->lock);
 			memcpy(x->db.map + x->db.used, h, sizeof(sppageh) + h->size);
@@ -58,10 +58,10 @@
 		*/
 		rc = sp_mapunlink(&g->db);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to unlink db file", g->epoch);
+			return sp_em(s, SPEIO|SPEF, g->epoch, "failed to unlink db file");
 		rc = sp_mapclose(&g->db);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to close db file", g->epoch);
+			return sp_em(s, SPEIO|SPEF, g->epoch, "failed to close db file");
 		sp_lock(&s->lockr);
 		sp_repdetach(&s->rep, g);
 		sp_free(&s->a, g);
diff --git a/sophia/db/i.c b/sophia/db/i.c
--- a/sophia/db/i.c
+++ b/sophia/db/i.c
@@ -79,6 +79,18 @@
 	return sp_iinit(i, i->a, i->pagesize, i->cmp, i->cmparg);
 }
 
+int sp_ireset(spi *i)
+{
+	uint32_t p = 0;
+	while (p < i->icount) {
+		sp_free(i->a, i->i[p]);
+		p++;
+	}
+	sp_free(i->a, i->i);
+	i->i = NULL;
+	return sp_iinit(i, i->a, i->pagesize, i->cmp, i->cmparg);
+}
+
 static inline void*
 sp_iminof(spi *i, spipage *p, char *rkey, int size, uint32_t *idx)
 {
@@ -293,15 +305,15 @@
 	/* inside index range */
 	if (l <= 0 && r >= 0)
 		return 0;
-	/* index min < key */
+	/* key > index min */
 	if (l == -1)
 		return -1;
-	/* index max > key */
+	/* key < index max */
 	assert(r == 1);
 	return 1;
 }
 
-inline int sp_ilte(spi *i, spii *ii, char *k, int size)
+int sp_ilte(spi *i, spii *ii, char *k, int size)
 {
 	if (spunlikely(i->count == 0)) {
 		sp_iinv(i, ii);
@@ -318,11 +330,16 @@
 			break;
 		case  1:
 			ii->i = i;
-			ii->p = i->icount - 1;
-			ii->n = i->i[i->icount - 1]->count - 1;
+			ii->p = 0;
+			ii->n = 0;
 			break;
 		case  0:
-			assert(0);
+			if (spunlikely(a >= i->icount))
+				a = i->icount - 1;
+			ii->i = i;
+			ii->p = a;
+			ii->n = 0;
+			break;
 		}
 		return 0;
 	}
@@ -334,7 +351,7 @@
 	return eq;
 }
 
-inline int sp_igte(spi *i, spii *ii, char *k, int size)
+int sp_igte(spi *i, spii *ii, char *k, int size)
 {
 	if (spunlikely(i->count == 0)) {
 		sp_iinv(i, ii);
@@ -355,7 +372,12 @@
 			sp_iinv(i, ii);
 			break;
 		case  0:
-			assert(0);
+			if (spunlikely(a >= i->icount))
+				a = i->icount - 1;
+			ii->i = i;
+			ii->p = a;
+			ii->n = i->i[a]->count - 1;
+			break;
 		}
 		return 0;
 	}
diff --git a/sophia/db/i.h b/sophia/db/i.h
--- a/sophia/db/i.h
+++ b/sophia/db/i.h
@@ -37,6 +37,7 @@
 int sp_iinit(spi*, spa*, int, spcmpf, void*);
 void sp_ifree(spi*);
 int sp_itruncate(spi*);
+int sp_ireset(spi*);
 int sp_isetorget(spi *i, spv*, spii*);
 int sp_idelraw(spi*, char*, int, spv**);
 spv *sp_igetraw(spi*, char*, int);
diff --git a/sophia/db/lock.h b/sophia/db/lock.h
--- a/sophia/db/lock.h
+++ b/sophia/db/lock.h
@@ -9,8 +9,16 @@
  * BSD License
 */
 
+#include <unistd.h>
+
 typedef uint8_t spspinlock;
 
+#if defined(__x86_64__) || defined(__i386) || defined(_X86_)
+# define CPU_PAUSE __asm__ ("pause")
+#else
+# define CPU_PAUSE do { } while(0)
+#endif
+
 static inline void
 sp_lockinit(volatile spspinlock *l) {
 	*l = 0;
@@ -23,7 +31,16 @@
 
 static inline void
 sp_lock(volatile spspinlock *l) {
-	while (__sync_lock_test_and_set(l, 1)) {}
+	if (__sync_lock_test_and_set(l, 1) != 0) {
+		unsigned int spin_count = 0U;
+		for (;;) {
+			CPU_PAUSE;
+			if (*l == 0U && __sync_lock_test_and_set(l, 1) == 0)
+				break;
+			if (++spin_count > 100U)
+				usleep(0);
+		}
+	}
 }
 
 static inline void
diff --git a/sophia/db/macro.h b/sophia/db/macro.h
--- a/sophia/db/macro.h
+++ b/sophia/db/macro.h
@@ -11,7 +11,13 @@
 
 #define sppacked __attribute__((packed))
 #define spunused __attribute__((unused))
-#define sphot __attribute__((hot))
+
+#if __GNUC__ >= 4 && __GNUC_MINOR__ >= 3
+#  define sphot __attribute__((hot))
+#else
+#  define sphot
+#endif
+
 #define splikely(EXPR) __builtin_expect(!! (EXPR), 1)
 #define spunlikely(EXPR) __builtin_expect(!! (EXPR), 0)
 #define spdiv(a, b) ((a) + (b) - 1) / (b)
diff --git a/sophia/db/merge.c b/sophia/db/merge.c
--- a/sophia/db/merge.c
+++ b/sophia/db/merge.c
@@ -53,16 +53,16 @@
 	{
 		/* get the new page properties and a data */
 		spupdate0 u;
-		sp_mergeget0(&i, s->e->page, &u);
+		sp_mergeget0(&i, s->env->page, &u);
 		if (spunlikely(u.count == 0))
 			 break;
 
 		/* ensure enough space for the page in the file */
 		sp_lock(&x->lock);
-		rc = sp_mapensure(&x->db, u.psize, s->e->dbgrow);
+		rc = sp_mapensure(&x->db, u.psize, s->env->dbgrow);
 		if (spunlikely(rc == -1)) {
 			sp_unlock(&x->lock);
-			sp_e(s, SPEIO, "failed to remap db file", x->epoch);
+			sp_em(s, SPEIO|SPEF, x->epoch, "failed to remap db file");
 			goto err;
 		}
 		sp_unlock(&x->lock);
@@ -95,7 +95,7 @@
 			if (spunlikely(min == NULL)) {
 				min = sp_vdup(s, v);
 				if (spunlikely(min == NULL)) {
-					sp_e(s, SPEOOM, "failed to allocate key");
+					sp_em(s, SPEOOM|SPEF, "failed to allocate key");
 					goto err;
 				}
 			}
@@ -123,12 +123,12 @@
 		/* create in-memory page */
 		sppage *page = sp_pagenew(s, x);
 		if (spunlikely(page == NULL)) {
-			sp_e(s, SPEOOM, "failed to allocate page");
+			sp_em(s, SPEOOM|SPEF, "failed to allocate page");
 			goto err;
 		}
 		max = sp_vdup(s, last);
 		if (spunlikely(max == NULL)) {
-			sp_e(s, SPEOOM, "failed to allocate key");
+			sp_em(s, SPEOOM|SPEF, "failed to allocate key");
 			goto err;
 		}
 		assert(min != NULL);
@@ -145,7 +145,7 @@
 		if (spunlikely(rc == -1)) {
 			sp_unlock(&s->locks);
 			sp_pagefree(s, page);
-			sp_e(s, SPEOOM, "failed to allocate page index page");
+			sp_em(s, SPEOOM|SPEF, "failed to allocate page index page");
 			goto err;
 		}
 		sp_unlock(&s->locks);
@@ -254,9 +254,9 @@
 	if (m->A < m->a_count && m->B < m->b_count)
 	{
 		register int cmp =
-			s->e->cmp(m->a->key, m->a->size,
-		              m->b->key,
-		              m->b->size, s->e->cmparg);
+			s->env->cmp(m->a->key, m->a->size,
+		                m->b->key,
+		                m->b->size, s->env->cmparg);
 		switch (cmp) {
 		case  0:
 			/* use updated key B */
@@ -326,13 +326,14 @@
 	int bsize = m->b_bsize;
 	uint32_t pagesize = sizeof(sppageh);
 	uint32_t count = 0;
+
 	/*
 	 * merge in-memory keys with the origin page keys,
 	 * skip any deletes and calculate result
 	 * page size.
 	*/
 	sp_refsetreset(&s->refs);
-	while (count < s->e->page && sp_mergenext(s, m)) {
+	while (count < s->env->page && sp_mergenext(s, m)) {
 		if (sp_refisdel(&m->last))
 			continue;
 		sp_refsetadd(&s->refs, &m->last);
@@ -349,11 +350,10 @@
 
 	/* ensure enough space for the page in the file */
 	sp_lock(&m->x->lock);
-	rc = sp_mapensure(&m->x->db, pagesize, s->e->dbgrow);
+	rc = sp_mapensure(&m->x->db, pagesize, s->env->dbgrow);
 	if (spunlikely(rc == -1)) {
 		sp_unlock(&m->x->lock);
-		return sp_e(s, SPEIO, "failed to remap db file",
-		            m->x->epoch);
+		return sp_em(s, SPEIO|SPEF, m->x->epoch, "failed to remap db file");
 	}
 	sp_unlock(&m->x->lock);
 
@@ -429,20 +429,20 @@
 	/* create in-memory page */
 	sppage *p = sp_pagenew(s, m->x);
 	if (spunlikely(p == NULL))
-		return sp_e(s, SPEOOM, "failed to allocate page");
+		return sp_em(s, SPEOOM|SPEF, "failed to allocate page");
 	p->id     = psn;
 	p->offset = m->x->db.used;
 	p->size   = pagesize;
 	p->min    = sp_vdupref(s, min, m->x->epoch);
 	if (spunlikely(p->min == NULL)) {
 		sp_free(&s->a, p);
-		return sp_e(s, SPEOOM, "failed to allocate key");
+		return sp_em(s, SPEOOM|SPEF, "failed to allocate key");
 	}
 	p->max    = sp_vdupref(s, max, m->x->epoch);
 	if (spunlikely(p->max == NULL)) {
 		sp_free(&s->a, p->min);
 		sp_free(&s->a, p);
-		return sp_e(s, SPEOOM, "failed to allocate key");
+		return sp_em(s, SPEOOM|SPEF, "failed to allocate key");
 	}
 
 	/* add page to split list */
@@ -498,7 +498,7 @@
 		int rc = sp_catset(&s->s, p, &o);
 		if (spunlikely(rc == -1)) {
 			sp_unlock(&s->locks);
-			return sp_e(s, SPEOOM, "failed to allocate page index page");
+			return sp_em(s, SPEOOM|SPEF, "failed to allocate page index page");
 		}
 		assert(o == NULL);
 		sp_pageattach(p);
@@ -550,8 +550,9 @@
 	spepoch *x = sp_replive(&s->rep);
 	/* rotate current live epoch */
 	sp_repset(&s->rep, x, SPXFER);
-	int rc = sp_rotate(s);
+	int rc = sp_rotate(s, &s->em);
 	if (spunlikely(rc == -1)) {
+		sp_esetfatal(&s->em);
 		sp_lock(&s->lockr);
 		sp_lock(&s->locki);
 		return -1;
@@ -565,15 +566,16 @@
 	/* complete old live epoch log */ 
 	rc = sp_logeof(&x->log);
 	if (spunlikely(rc == -1))
-		return sp_e(s, SPEIO, "failed to write eof marker", x->epoch);
+		return sp_em(s, SPEIO|SPEF, x->epoch, "failed to write eof marker");
 	rc = sp_logcomplete(&x->log);
 	if (spunlikely(rc == -1))
-		return sp_e(s, SPEIO, "failed to complete log file", x->epoch);
+		return sp_em(s, SPEIO|SPEF, x->epoch, "failed to complete log file");
 
 	/* create db file */
-	rc = sp_mapepochnew(&x->db, s->e->dbnewsize, s->e->dir, x->epoch, "db");
+	rc = sp_mapepochnew(&x->db, s->env->dbnewsize, s->env->dir,
+	                    x->epoch, "db");
 	if (spunlikely(rc == -1))
-		return sp_e(s, SPEIO, "failed to create db file", x->epoch);
+		return sp_em(s, SPEIO|SPEF, x->epoch, "failed to create db file");
 
 	/* merge index */
 	if (splikely(s->s.count > 0))
@@ -591,7 +593,7 @@
 		return -1;
 
 	/* gc */
-	if (s->e->gc) {
+	if (s->env->gc) {
 		rc = sp_gc(s, x);
 		if (spunlikely(rc == -1))
 			return -1;
@@ -604,7 +606,7 @@
 		rc = sp_mapcomplete(&x->db);
 		if (spunlikely(rc == -1)) {
 			sp_unlock(&x->lock);
-			return sp_e(s, SPEIO, "failed to complete db file", x->epoch);
+			return sp_em(s, SPEIO|SPEF, x->epoch, "failed to complete db file");
 		}
 		sp_unlock(&x->lock);
 		/* set epoch as db */
@@ -614,19 +616,19 @@
 		/* remove log file */
 		rc = sp_logunlink(&x->log);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to unlink log file", x->epoch);
+			return sp_em(s, SPEIO|SPEF, x->epoch, "failed to unlink log file");
 		rc = sp_logclose(&x->log);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to close log file", x->epoch);
+			return sp_em(s, SPEIO|SPEF, x->epoch, "failed to close log file");
 	} else {
 		/* there are possible situation when all keys has
 		 * been deleted. */
 		rc = sp_mapunlink(&x->db);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to unlink db file", x->epoch);
+			return sp_em(s, SPEIO|SPEF, x->epoch, "failed to unlink db file");
 		rc = sp_mapclose(&x->db);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to close db file", x->epoch);
+			return sp_em(s, SPEIO|SPEF, x->epoch, "failed to close db file");
 	}
 
 	/* remove all xfer epochs that took part in the merge
@@ -640,10 +642,10 @@
 			break;
 		rc = sp_logunlink(&e->log);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to unlink log file", e->epoch);
+			return sp_em(s, SPEIO|SPEF, e->epoch, "failed to unlink log file");
 		rc = sp_logclose(&e->log);
 		if (spunlikely(rc == -1))
-			return sp_e(s, SPEIO, "failed to close log file", e->epoch);
+			return sp_em(s, SPEIO|SPEF, e->epoch, "failed to close log file");
 		sp_lock(&s->lockr);
 		sp_repdetach(&s->rep, e);
 		sp_free(&s->a, e);
@@ -655,7 +657,7 @@
 	rc = sp_itruncate(index);
 	if (spunlikely(rc == -1)) {
 		sp_iskipset(s, 0);
-		return sp_e(s, SPE, "failed create index");
+		return sp_em(s, SPE|SPEF, "failed create index");
 	}
 	sp_iskipset(s, 0);
 	return 0;
diff --git a/sophia/db/recover.c b/sophia/db/recover.c
--- a/sophia/db/recover.c
+++ b/sophia/db/recover.c
@@ -15,16 +15,89 @@
 #include <dirent.h>
 #include <unistd.h>
 
-static inline int sp_dircreate(sp *s) {
-	int rc = mkdir(s->e->dir, 0700);
+static inline int sp_dircreate(sp *s)
+{
+	int rc = mkdir(s->env->dir, 0700);
 	if (spunlikely(rc == -1)) {
 		sp_e(s, SPE, "failed to create directory %s (errno: %d, %s)",
-		     s->e->dir, errno, strerror(errno));
+		     s->env->dir, errno, strerror(errno));
 		return -1;
 	}
 	return 0;
 }
 
+typedef struct {
+	splist link;
+	char dir[];
+} spdirlock;
+
+static spspinlock dirlock = 0;
+static int dirlockn = 0;
+static splist dirlocks;
+
+static int sp_dirlock(sp *s)
+{
+	/* implement multi-process database exclusive access by creating
+	 * lock file. */
+	char path[1024];
+	snprintf(path, sizeof(path), "%s/lock", s->env->dir);
+	int rc = sp_lockfile(&s->lockdb, path);
+	if (spunlikely(rc == -1))
+		sp_e(s, SPE, "failed to create lock file (errno: %d, %s)",
+		     errno, strerror(errno));
+	if (rc == 1)
+		return sp_e(s, SPE, "database is locked");
+	/* within a single process, add database path to the list and
+	 * check the presence on a open. */
+	sp_lock(&dirlock);
+	if (dirlockn == 0) {
+		sp_listinit(&dirlocks);
+	} else {
+		splist *i;
+		sp_listforeach(&dirlocks, i) {
+			spdirlock *l = spcast(i, spdirlock, link);
+			if (! strcmp(s->env->dir, l->dir)) {
+				sp_unlock(&dirlock);
+				return sp_e(s, SPE, "database is locked");
+			}
+		}
+	}
+	int lendir = strlen(s->env->dir) + 1;
+	int len = sizeof(spdirlock) + lendir;
+	spdirlock *l = malloc(len);
+	if (spunlikely(l == NULL)) {
+		sp_unlock(&dirlock);
+		return sp_e(s, SPEOOM, "failed to allocate memory");
+	}
+	sp_listinit(&l->link);
+	memcpy(l->dir, s->env->dir, lendir);
+	sp_listappend(&dirlocks, &l->link);
+	dirlockn++;
+	sp_unlock(&dirlock);
+	return 0;
+}
+
+static int sp_dirunlock(sp *s)
+{
+	sp_lock(&dirlock);
+	if (spunlikely(dirlockn == 0)) {
+		sp_unlock(&dirlock);
+		return 0;
+	}
+	splist *i, *n;
+	sp_listforeach_safe(&dirlocks, i, n) {
+		spdirlock *l = spcast(i, spdirlock, link);
+		if (! strcmp(s->env->dir, l->dir)) {
+			sp_listunlink(&l->link);
+			free(l);
+			break;
+		}
+	}
+	dirlockn--;
+	sp_unlock(&dirlock);
+	return sp_unlockfile(&s->lockdb);
+}
+
 static inline ssize_t
 sp_epochof(char *s) {
 	size_t v = 0;
@@ -40,10 +113,10 @@
 static int sp_diropen(sp *s)
 {
 	/* read repository and determine states */
-	DIR *d = opendir(s->e->dir);
+	DIR *d = opendir(s->env->dir);
 	if (spunlikely(d == NULL)) {
 		sp_e(s, SPE, "failed to open directory %s (errno: %d, %s)",
-		     s->e->dir, errno, strerror(errno));
+		     s->env->dir, errno, strerror(errno));
 		return -1;
 	}
 	struct dirent *de;
@@ -88,9 +161,9 @@
 
 static int sp_recoverdb(sp *s, spepoch *x, sptrack *t)
 {
-	int rc = sp_mapepoch(&x->db, s->e->dir, x->epoch, "db");
+	int rc = sp_mapepoch(&x->db, s->env->dir, x->epoch, "db");
 	if (spunlikely(rc == -1))
-		return sp_e(s, SPEIO, "failed to open db file", x->epoch);
+		return sp_e(s, SPEIO, x->epoch, "failed to open db file");
 
 	sppageh *h = (sppageh*)(x->db.map);
 
@@ -209,9 +282,9 @@
 	/* open and map log file */
 	char *ext = (incomplete ? "log.incomplete" : "log");
 	int rc;
-	rc = sp_mapepoch(&x->log, s->e->dir, x->epoch, ext);
+	rc = sp_mapepoch(&x->log, s->env->dir, x->epoch, ext);
 	if (spunlikely(rc == -1))
-		return sp_e(s, SPEIO, "failed to open log file", x->epoch);
+		return sp_e(s, SPEIO, x->epoch, "failed to open log file");
 
 	/* validate log header */
 	if (spunlikely(! sp_mapinbound(&x->log, sizeof(splogh)) ))
@@ -315,7 +388,7 @@
 	 * during shutdown */
 	rc = sp_mapunmap(&x->log);
 	if (spunlikely(rc == -1))
-		return sp_e(s, SPEIO, "failed to unmap log file", x->epoch);
+		return sp_e(s, SPEIO, x->epoch, "failed to unmap log file");
 
 	/*
 	 * if there is eof marker missing, try to add one
@@ -328,22 +401,22 @@
 		if (! eof) {
 			rc = sp_logclose(&x->log);
 			if (spunlikely(rc == -1))
-				return sp_e(s, SPEIO, "failed to close log file", x->epoch);
-			rc = sp_logcontinue(&x->log, s->e->dir, x->epoch);
+				return sp_e(s, SPEIO, x->epoch, "failed to close log file");
+			rc = sp_logcontinue(&x->log, s->env->dir, x->epoch);
 			if (spunlikely(rc == -1)) {
 				sp_logclose(&x->log);
-				return sp_e(s, SPEIO, "failed to reopen log file", x->epoch);
+				return sp_e(s, SPEIO, x->epoch, "failed to reopen log file");
 			}
 			rc = sp_logeof(&x->log);
 			if (spunlikely(rc == -1)) {
 				sp_logclose(&x->log);
-				return sp_e(s, SPEIO, "failed to add eof marker", x->epoch);
+				return sp_e(s, SPEIO, x->epoch, "failed to add eof marker");
 			}
 		}
 		rc = sp_logcompleteforce(&x->log);
 		if (spunlikely(rc == -1)) {
 			sp_logclose(&x->log);
-			return sp_e(s, SPEIO, "failed to complete log file", x->epoch);
+			return sp_e(s, SPEIO, x->epoch, "failed to complete log file");
 		}
 	}
 	return 0;
@@ -368,13 +441,13 @@
 			if (spunlikely(rc == -1))
 				goto err;
 			if (e->recover == (SPRDB|SPRLOG)) {
-				rc = sp_epochrm(s->e->dir, e->epoch, "log");
+				rc = sp_epochrm(s->env->dir, e->epoch, "log");
 				if (spunlikely(rc == -1))
 					goto err;
 			}
 			break;
 		case SPRLOG|SPRDBI:
-			rc = sp_epochrm(s->e->dir, e->epoch, "db.incomplete");
+			rc = sp_epochrm(s->env->dir, e->epoch, "db.incomplete");
 			if (spunlikely(rc == -1))
 				goto err;
 		case SPRLOG:
@@ -411,17 +484,27 @@
 	return -1;
 }
 
+int sp_recoverunlock(sp *s) {
+	return sp_dirunlock(s);
+}
+
 int sp_recover(sp *s)
 {
-	int exists = sp_fileexists(s->e->dir);
+	int exists = sp_fileexists(s->env->dir);
 	int rc;
 	if (!exists) {
-		if (! (s->e->flags & SPO_CREAT))
+		if (! (s->env->flags & SPO_CREAT))
 			return sp_e(s, SPE, "directory doesn't exists and no SPO_CREAT specified");
-		if (s->e->flags & SPO_RDONLY)
+		if (s->env->flags & SPO_RDONLY)
 			return sp_e(s, SPE, "directory doesn't exists");
 		rc = sp_dircreate(s);
+		if (spunlikely(rc == -1))
+			return -1;
+		rc = sp_dirlock(s);
 	} else {
+		rc = sp_dirlock(s);
+		if (spunlikely(rc == -1))
+			return -1;
 		rc = sp_diropen(s);
 		if (spunlikely(rc == -1))
 			return -1;
diff --git a/sophia/db/recover.h b/sophia/db/recover.h
--- a/sophia/db/recover.h
+++ b/sophia/db/recover.h
@@ -9,6 +9,7 @@
  * BSD License
 */
 
+int sp_recoverunlock(sp*);
 int sp_recover(sp*);
 
 #endif
diff --git a/sophia/db/sophia.h b/sophia/db/sophia.h
--- a/sophia/db/sophia.h
+++ b/sophia/db/sophia.h
@@ -9,12 +9,22 @@
  * BSD License
 */
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #include <stdlib.h>
 #include <stdint.h>
 
-typedef void *(*spallocf)(void*, size_t, void*);
-typedef int (*spcmpf)(char*, size_t, char*, size_t, void*);
+#if __GNUC__ >= 4
+#  define SP_API __attribute__((visibility("default")))
+#else
+#  define SP_API
+#endif
 
+typedef void *(*spallocf)(void *ptr, size_t size, void *arg);
+typedef int (*spcmpf)(char *a, size_t asz, char *b, size_t bsz, void *arg);
+
 typedef enum {
 	/* env related */
 	SPDIR,     /* uint32_t, char* */
@@ -32,12 +42,12 @@
 	SPVERSION  /* uint32_t*, uint32_t* */
 } spopt;
 
-enum {
+typedef enum {
 	SPO_RDONLY = 1,
 	SPO_RDWR   = 2,
 	SPO_CREAT  = 4,
 	SPO_SYNC   = 8
-};
+} spflags;
 
 typedef enum {
 	SPGT,
@@ -57,20 +67,27 @@
 	uint32_t indexpages;
 } spstat;
 
-void *sp_env(void);
-void *sp_open(void*);
-int sp_ctl(void*, spopt, ...);
-int sp_destroy(void*);
-int sp_set(void*, const void*, size_t, const void*, size_t);
-int sp_delete(void*, const void*, size_t);
-int sp_get(void*, const void*, size_t, void**, size_t*);
-void *sp_cursor(void*, sporder, const void*, size_t);
-int sp_fetch(void*);
-const char *sp_key(void*);
-size_t sp_keysize(void*);
-const char *sp_value(void*);
-size_t sp_valuesize(void*);
-char *sp_error(void*);
-void sp_stat(void*, spstat*);
+SP_API void *sp_env(void);
+SP_API void *sp_open(void *env);
+SP_API int sp_ctl(void*, spopt, ...);
+SP_API int sp_destroy(void *ptr);
+SP_API int sp_begin(void *db);
+SP_API int sp_commit(void *db);
+SP_API int sp_rollback(void *db);
+SP_API int sp_set(void *db, const void *k, size_t ksize, const void *v, size_t vsize);
+SP_API int sp_delete(void *db, const void *k, size_t ksize);
+SP_API int sp_get(void *db, const void *k, size_t ksize, void **v, size_t *vsize);
+SP_API void *sp_cursor(void *db, sporder, const void *k, size_t ksize);
+SP_API int sp_fetch(void *cur);
+SP_API const char *sp_key(void *cur);
+SP_API size_t sp_keysize(void *cur);
+SP_API const char *sp_value(void *cur);
+SP_API size_t sp_valuesize(void *cur);
+SP_API char *sp_error(void *ptr);
+SP_API void sp_stat(void *ptr, spstat*);
+
+#ifdef __cplusplus
+}
+#endif
 
 #endif
diff --git a/sophia/db/sp.c b/sophia/db/sp.c
--- a/sophia/db/sp.c
+++ b/sophia/db/sp.c
@@ -13,7 +13,9 @@
 cmpstd(char *a, size_t asz, char *b, size_t bsz, void *arg spunused) {
 	register size_t sz = (asz < bsz ? asz : bsz);
 	register int rc = memcmp(a, b, sz);
-	return (rc == 0 ? rc : (rc > 0 ? 1 : -1));
+	if (rc == 0)
+		return (asz == bsz) ? 0 : (asz > bsz ? 1 : -1);
+	return rc > 0 ? 1 : -1;
 }
 
 static inline void sp_envinit(spenv *e) {
@@ -127,7 +129,7 @@
 {
 	switch (opt) {
 	case SPMERGEFORCE:
-		if (s->e->merge)
+		if (s->env->merge)
 			return sp_e(s, SPE, "force merge doesn't work with merger thread active");
 		return sp_merge(s);
 	default:
@@ -161,30 +163,29 @@
 	return rc;
 }
 
-int sp_rotate(sp *s)
+int sp_rotate(sp *s, spe *err)
 {
 	int rc;
 	sp_repepochincrement(&s->rep);
 	/* allocate new epoch */
 	spepoch *e = sp_repalloc(&s->rep, sp_repepoch(&s->rep));
 	if (spunlikely(s == NULL))
-		return sp_e(s, SPEOOM, "failed to allocate repository");
+		return sp_ef(err, SPEOOM, "failed to allocate repository");
 	/* create log file */
-	rc = sp_lognew(&e->log, s->e->dir, sp_repepoch(&s->rep));
+	rc = sp_lognew(&e->log, s->env->dir, sp_repepoch(&s->rep));
 	if (spunlikely(rc == -1)) {
 		sp_free(&s->a, e);
-		return sp_e(s, SPEIO, "failed to create log file", e->epoch);
+		return sp_ef(err, SPEIO, e->epoch, "failed to create log file");
 	}
 	splogh h;
 	h.magic = SPMAGIC;
 	h.version[0] = SP_VERSION_MAJOR;
 	h.version[1] = SP_VERSION_MINOR;
-	sp_logadd(&e->log, (char*)&h, sizeof(h));
-	rc = sp_logflush(&e->log);
+	rc = sp_logwrite(&e->log, &h, sizeof(h));
 	if (spunlikely(rc == -1)) {
 		sp_logclose(&e->log);
 		sp_free(&s->a, e);
-		return sp_e(s, SPEIO, "failed to write log file", e->epoch);
+		return sp_ef(err, SPEIO, e->epoch, "failed to write log file");
 	}
 	/* attach epoch and mark it is as live */
 	sp_repattach(&s->rep, e);
@@ -209,28 +210,28 @@
 			if (e->nupdate == 0) {
 				rc = sp_logunlink(&e->log);
 				if (spunlikely(rc == -1))
-					rcret = sp_e(s, SPEIO, "failed to unlink log file", e->epoch);
+					rcret = sp_e(s, SPEIO, e->epoch, "failed to unlink log file");
 				rc = sp_logclose(&e->log);
 				if (spunlikely(rc == -1))
-					rcret = sp_e(s, SPEIO, "failed to close log file", e->epoch);
+					rcret = sp_e(s, SPEIO, e->epoch, "failed to close log file");
 				break;
 			} else {
 				rc = sp_logeof(&e->log);
 				if (spunlikely(rc == -1))
-					rcret = sp_e(s, SPEIO, "failed to write eof marker", e->epoch);
+					rcret = sp_e(s, SPEIO, e->epoch, "failed to write eof marker");
 			}
 		case SPXFER:
 			rc = sp_logcomplete(&e->log);
 			if (spunlikely(rc == -1))
-				rcret = sp_e(s, SPEIO, "failed to complete log file", e->epoch);
+				rcret = sp_e(s, SPEIO, e->epoch, "failed to complete log file");
 			rc = sp_logclose(&e->log);
 			if (spunlikely(rc == -1))
-				rcret = sp_e(s, SPEIO, "failed to close log file", e->epoch);
+				rcret = sp_e(s, SPEIO, e->epoch, "failed to close log file");
 			break;
 		case SPDB:
 			rc = sp_mapclose(&e->db);
 			if (spunlikely(rc == -1))
-				rcret = sp_e(s, SPEIO, "failed to close db file", e->epoch);
+				rcret = sp_e(s, SPEIO, e->epoch, "failed to close db file");
 			break;
 		}
 		sp_free(&s->a, e);
@@ -243,7 +244,7 @@
 	int rcret = 0;
 	int rc = 0;
 	s->stop = 1;
-	if (s->e->merge) {
+	if (s->env->merge) {
 		rc = sp_taskstop(&s->merger);
 		if (spunlikely(rc == -1))
 			rcret = sp_e(s, SPESYS, "failed to stop merger thread");
@@ -252,13 +253,19 @@
 	rc = sp_closerep(s);
 	if (spunlikely(rc == -1))
 		rcret = -1;
+	rc = sp_recoverunlock(s);
+	if (spunlikely(rc == -1))
+		rcret = -1;
 	sp_ifree(&s->i0);
 	sp_ifree(&s->i1);
+	sp_ifree(&s->itxn); /* equal to rollback */
 	sp_catfree(&s->s);
-	s->e->inuse = 0;
+	s->env->inuse = 0;
 	sp_lockfree(&s->lockr);
 	sp_lockfree(&s->locks);
 	sp_lockfree(&s->locki);
+	sp_efree(&s->e);
+	sp_efree(&s->em);
 	return rcret;
 }
 
@@ -268,7 +275,7 @@
 	sp *s = self->arg;
 	do {
 		sp_lock(&s->locki);
-		int merge = s->i->count > s->e->mergewm;
+		int merge = s->i->count > s->env->mergewm;
 		sp_unlock(&s->locki);
 		if (! merge)
 			continue;
@@ -293,34 +300,45 @@
 	sp_allocinit(&a, env->alloc, env->allocarg);
 	sp *s = sp_malloc(&a, sizeof(sp));
 	if (spunlikely(s == NULL)) {
-		sp_e(s, SPEOOM, "failed to allocate db handle");
+		sp_ee(env, SPEOOM, "failed to allocate db handle");
 		return NULL;
 	}
 	memset(s, 0, sizeof(sp));
+	sp_einit(&s->e);
+	sp_einit(&s->em);
 	s->m = SPMDB;
-	s->e = env;
-	s->e->inuse = 1;
+	s->env = env;
+	s->env->inuse = 1;
 	memcpy(&s->a, &a, sizeof(s->a));
 	/* init locks */
+	sp_fileinit(&s->lockdb, &s->a);
 	sp_lockinit(&s->lockr);
 	sp_lockinit(&s->locks);
 	sp_lockinit(&s->locki);
 	s->lockc = 0;
 	/* init key index */
-	rc = sp_iinit(&s->i0, &s->a, 1024, s->e->cmp, s->e->cmparg);
+	rc = sp_iinit(&s->i0, &s->a, 1024, s->env->cmp, s->env->cmparg);
 	if (spunlikely(rc == -1)) {
 		sp_e(s, SPEOOM, "failed to allocate key index");
 		goto e0;
 	}
-	rc = sp_iinit(&s->i1, &s->a, 1024, s->e->cmp, s->e->cmparg);
+	rc = sp_iinit(&s->i1, &s->a, 1024, s->env->cmp, s->env->cmparg);
 	if (spunlikely(rc == -1)) {
 		sp_e(s, SPEOOM, "failed to allocate key index");
 		goto e1;
 	}
 	s->i = &s->i0;
+	/* init transaction index */
+	rc = sp_iinit(&s->itxn, &s->a, 1024, s->env->cmp, s->env->cmparg);
+	if (spunlikely(rc == -1)) {
+		sp_e(s, SPEOOM, "failed to allocate transaction index");
+		goto e2;
+	}
+	/* set current transaction state as single-stmt */
+	s->txn = SPTSS;
 	/* init page index */
 	s->psn = 0;
-	rc = sp_catinit(&s->s, &s->a, 512, s->e->cmp, s->e->cmparg);
+	rc = sp_catinit(&s->s, &s->a, 512, s->env->cmp, s->env->cmparg);
 	if (spunlikely(rc == -1)) {
 		sp_e(s, SPEOOM, "failed to allocate page index");
 		goto e2;
@@ -330,18 +348,18 @@
 	if (spunlikely(rc == -1))
 		goto e3;
 	/* do not create new live epoch in read-only mode */
-	if (! (s->e->flags & SPO_RDONLY)) {
-		rc = sp_rotate(s);
+	if (! (s->env->flags & SPO_RDONLY)) {
+		rc = sp_rotate(s, &s->e);
 		if (spunlikely(rc == -1))
 			goto e3;
 	}
 	s->stop = 0;
-	rc = sp_refsetinit(&s->refs, &s->a, s->e->page);
+	rc = sp_refsetinit(&s->refs, &s->a, s->env->page);
 	if (spunlikely(rc == -1)) {
 		sp_e(s, SPEOOM, "failed to allocate key buffer");
 		goto e3;
 	}
-	if (s->e->merge) {
+	if (s->env->merge) {
 		rc = sp_taskstart(&s->merger, merger, s);
 		if (spunlikely(rc == -1)) {
 			sp_e(s, SPESYS, "failed to start merger thread");
@@ -350,15 +368,25 @@
 		sp_taskwakeup(&s->merger);
 	}
 	return s;
-e4: sp_refsetfree(&s->refs, &s->a);
-e3: sp_closerep(s);
+e4:
+	sp_refsetfree(&s->refs, &s->a);
+e3:
+	sp_closerep(s);
+	sp_recoverunlock(s);
 	sp_catfree(&s->s);
-e2: sp_ifree(&s->i1);
-e1:	sp_ifree(&s->i0);
-e0: s->e->inuse = 0;
+e2:
+	sp_ifree(&s->itxn);
+	sp_ifree(&s->i1);
+e1:
+	sp_ifree(&s->i0);
+e0:
+	s->env->inuse = 0;
 	sp_lockfree(&s->lockr);
 	sp_lockfree(&s->locks);
 	sp_lockfree(&s->locki);
+	sp_edup(&env->e, &s->e);
+	sp_efree(&s->e);
+	sp_efree(&s->em);
 	sp_free(&a, s);
 	return NULL;
 }
@@ -404,27 +432,158 @@
 char *sp_error(void *o)
 {
 	spmagic *magic = (spmagic*)o;
-	spe *e = NULL;
+	spenv *env;
 	switch (*magic) {
-	case SPMDB: {
-		sp *s = o;
-		e = &s->e->e;
-		break;
-	}
-	case SPMENV: {
-		spenv *env = o;
-		e = &env->e;
-		break;
-	}
+	case SPMENV:
+		env = o;
+		if (! sp_eis(&env->e))
+			return NULL;
+		return env->e.e;
+	case SPMDB: break;
 	default:
 		assert(0);
 		return NULL;
 	}
-	if (! sp_eis(e))
-		return NULL;
-	return e->e;
+	sp *s = o;
+	if (sp_eis(&s->em))
+		return s->em.e;
+	if (sp_eis(&s->e))
+		return s->e.e;
+	return NULL;
 }
 
+int sp_begin(void *o)
+{
+	sp *s = o;
+	assert(s->m == SPMDB);
+	if (spunlikely(sp_evalidate(s)))
+		return -1;
+	if (spunlikely(s->txn == SPTMS))
+		return -1;
+	if (spunlikely(s->lockc))
+		return sp_e(s, SPE, "begin with open cursor");
+	s->txn = SPTMS;
+	return 0;
+}
+
+int sp_commit(void *o)
+{
+	sp *s = o;
+	assert(s->m == SPMDB);
+	if (spunlikely(sp_evalidate(s)))
+		return -1;
+	if (spunlikely(s->txn == SPTSS))
+		return sp_e(s, SPE, "no active transaction to commit");
+	if (spunlikely(s->lockc))
+		return sp_e(s, SPE, "commit with open cursor");
+	if (spunlikely(s->itxn.count == 0)) {
+		s->txn = SPTSS;
+		return 0;
+	}
+
+	/* prepare to write the transaction
+	 * to the log */
+	int n = s->itxn.count;
+
+	sp_lock(&s->lockr);
+	sp_lock(&s->locki);
+
+	spepoch *live = sp_replive(&s->rep);
+	sp_filesvp(&live->log);
+
+	char hbuf[sizeof(spvh) * 512];
+	unsigned int hpos = 0;
+
+	spii it;
+	sp_iopen(&it, &s->itxn);
+	int rc;
+	do {
+		spv *v = sp_ival(&it);
+
+		if (spunlikely(! sp_batchensure(&s->lb, 3))) {
+			rc = sp_logput(&live->log, &s->lb);
+			if (spunlikely(rc == -1)) {
+				sp_e(s, SPEIO|SPEF, live->epoch, "failed to write log file");
+				goto abort;
+			}
+			hpos = 0;
+		}
+
+		v->epoch = live->epoch;
+		assert(hpos < sizeof(hbuf));
+		spvh *hp = (spvh*)(hbuf + hpos);
+		hp->crc     = 0;
+		hp->size    = v->size;
+		hp->voffset = 0;
+		hp->vsize   = sp_vvsize(v);
+		hp->flags   = v->flags;
+		hp->crc     = sp_crc32c(v->crc, &hp->size, sizeof(spvh) - sizeof(uint32_t));
+		sp_batchadd(&s->lb, hp, sizeof(spvh));
+		sp_batchadd(&s->lb, v->key, v->size);
+		sp_batchadd(&s->lb, sp_vv(v), hp->vsize);
+		hpos += sizeof(spvh);
+
+		spv *old = NULL;
+		rc = sp_iset(s->i, v, &old);
+		if (spunlikely(rc == -1)) {
+			sp_e(s, SPEOOM|SPEF, "failed to allocate key index page");
+			goto abort;
+		}
+		if (old)
+			sp_free(&s->a, old);
+
+	} while (sp_inext(&it));
+
+	if (sp_batchhas(&s->lb)) {
+		rc = sp_logput(&live->log, &s->lb);
+		if (spunlikely(rc == -1)) {
+			sp_e(s, SPEIO|SPEF, live->epoch, "failed to write log file");
+			goto abort;
+		}
+	}
+
+	/* clean up transaction index (pages only) */
+	sp_ireset(&s->itxn);
+
+	sp_unlock(&s->locki);
+	sp_unlock(&s->lockr);
+
+	/* set transaction as single-stmt */
+	s->txn = SPTSS;
+
+	/* wake up merger if necessary */
+	live->nupdate += n;
+	if (live->nupdate >= s->env->mergewm) {
+		if (splikely(s->env->merge))
+			sp_taskwakeup(&s->merger);
+	}
+	return 0;
+
+abort:
+	sp_rollback(o);
+	sp_logrlb(&live->log);
+	sp_unlock(&s->locki);
+	sp_unlock(&s->lockr);
+	return -1;
+}
+
+int sp_rollback(void *o)
+{
+	sp *s = o;
+	assert(s->m == SPMDB);
+	if (spunlikely(sp_evalidate(s)))
+		return -1;
+	if (spunlikely(s->txn == SPTSS))
+		return sp_e(s, SPE, "no active transaction to rollback");
+	if (spunlikely(s->lockc))
+		return sp_e(s, SPE, "rollback with open cursor");
+	int rc = sp_itruncate(&s->itxn);
+	if (spunlikely(rc == -1))
+		return sp_e(s, SPEOOM, "failed to allocate key index page");
+	s->txn = SPTSS;
+	return 0;
+}
+
 static inline int
 sp_do(sp *s, int op, void *k, size_t ksize, void *v, size_t vsize)
 {
@@ -450,35 +609,57 @@
 	crc   = sp_crc32c(crc, v, vsize);
 	h.crc = sp_crc32c(crc, &h.size, sizeof(spvh) - sizeof(uint32_t));
 
+	n->flags = op;
+	n->crc = crc;
+
+	/* in case of multi-stmt transaction, simply add version to the
+	 * transaction index only. */
+	int rc;
+	if (s->txn == SPTMS) {
+		spv *old = NULL;
+		rc = sp_iset(&s->itxn, n, &old);
+		if (spunlikely(rc == -1)) {
+			sp_free(&s->a, n);
+			return sp_e(s, SPEOOM, "failed to allocate transacton key index page");
+		}
+		if (old)
+			sp_free(&s->a, old);
+		return 0;
+	}
+
 	sp_lock(&s->lockr);
 	sp_lock(&s->locki);
 
 	/* write to current live epoch log */
 	spepoch *live = sp_replive(&s->rep);
 	sp_filesvp(&live->log);
-	sp_logadd(&live->log, &h, sizeof(spvh));
-	sp_logadd(&live->log, k, ksize);
-	sp_logadd(&live->log, v, vsize);
-	int rc = sp_logflush(&live->log);
+	sp_batchadd(&s->lb, &h, sizeof(spvh));
+	sp_batchadd(&s->lb, k, ksize);
+	sp_batchadd(&s->lb, v, vsize);
+	rc = sp_logput(&live->log, &s->lb);
 	if (spunlikely(rc == -1)) {
 		sp_free(&s->a, n);
-		sp_logrlb(&live->log);
+		rc = sp_logrlb(&live->log);
+		if (spunlikely(rc == -1))
+			sp_esetfatal(&s->e);
 		sp_unlock(&s->locki);
 		sp_unlock(&s->lockr);
-		return sp_e(s, SPEIO, "failed to write log file", live->epoch);
+		return sp_e(s, SPEIO, live->epoch, "failed to write log file");
 	}
 
 	/* add new version to the index */
-	spv *old = NULL;
 	n->epoch = live->epoch;
-	n->flags = op;
-	n->crc   = crc;
+	spv *old = NULL;
 	rc = sp_iset(s->i, n, &old);
 	if (spunlikely(rc == -1)) {
 		sp_free(&s->a, n);
+		rc = sp_logrlb(&live->log);
+		if (spunlikely(rc == -1))
+			sp_esetfatal(&s->e);
 		sp_unlock(&s->locki);
 		sp_unlock(&s->lockr);
-		return sp_e(s, SPEOOM, "failed to allocate key index page");
+		return (spunlikely(rc == -1)) ? -1 :
+		        sp_e(s, SPEOOM, "failed to allocate key index page");
 	}
 
 	sp_unlock(&s->locki);
@@ -489,8 +670,8 @@
 
 	/* wake up merger on merge watermark reached */
 	live->nupdate++;
-	if ((live->nupdate % s->e->mergewm) == 0) {
-		if (splikely(s->e->merge))
+	if ((live->nupdate % s->env->mergewm) == 0) {
+		if (splikely(s->env->merge))
 			sp_taskwakeup(&s->merger);
 	}
 	return 0;
@@ -500,14 +681,16 @@
 {
 	sp *s = o;
 	assert(s->m == SPMDB);
-	if (spunlikely(sp_eis(&s->e->e)))
+	if (spunlikely(sp_evalidate(s)))
 		return -1;
-	if (spunlikely(s->e->flags & SPO_RDONLY))
+	if (spunlikely(s->env->flags & SPO_RDONLY))
 		return sp_e(s, SPE, "db handle is read-only");
 	if (spunlikely(ksize > UINT16_MAX))
 		return sp_e(s, SPE, "key size limit reached");
 	if (spunlikely(vsize > UINT32_MAX))
 		return sp_e(s, SPE, "value size limit reached");
+	if (spunlikely(s->lockc))
+		return sp_e(s, SPE, "modify with open cursor");
 	return sp_do(s, SPSET, (char*)k, ksize, (char*)v, vsize);
 }
 
@@ -515,31 +698,25 @@
 {
 	sp *s = o;
 	assert(s->m == SPMDB);
-	if (spunlikely(sp_eis(&s->e->e)))
+	if (spunlikely(sp_evalidate(s)))
 		return -1;
-	if (spunlikely(s->e->flags & SPO_RDONLY))
+	if (spunlikely(s->env->flags & SPO_RDONLY))
 		return sp_e(s, SPE, "db handle is read-only");
 	if (spunlikely(ksize > UINT16_MAX))
 		return sp_e(s, SPE, "key size limit reached");
+	if (spunlikely(s->lockc))
+		return sp_e(s, SPE, "modify with open cursor");
 	return sp_do(s, SPDEL, (char*)k, ksize, NULL, 0);
 }
 
-static inline int
-sp_checkro(sp *s, size_t ksize)
-{
-	if (spunlikely(sp_eis(&s->e->e)))
-		return -1;
-	if (spunlikely(ksize > UINT16_MAX))
-		return sp_e(s, SPE, "key size limit reached");
-	return 0;
-}
-
 int sp_get(void *o, const void *k, size_t ksize, void **v, size_t *vsize)
 {
 	sp *s = o;
 	assert(s->m == SPMDB);
-	if (spunlikely(sp_checkro(s, ksize) == -1))
+	if (spunlikely(sp_evalidate(s)))
 		return -1;
+	if (spunlikely(ksize > UINT16_MAX))
+		return sp_e(s, SPE, "key size limit reached");
 	return sp_match(s, (char*)k, ksize, v, vsize);
 }
 
@@ -547,8 +724,12 @@
 {
 	sp *s = o;
 	assert(s->m == SPMDB);
-	if (spunlikely(sp_checkro(s, ksize) == -1))
+	if (spunlikely(sp_evalidate(s)))
 		return NULL;
+	if (spunlikely(ksize > UINT16_MAX)) {
+		sp_e(s, SPE, "key size limit reached");
+		return NULL;
+	}
 	spc *c = sp_malloc(&s->a, sizeof(spc));
 	if (spunlikely(c == NULL)) {
 		sp_e(s, SPEOOM, "failed to allocate cursor handle");
@@ -562,7 +743,7 @@
 int sp_fetch(void *o) {
 	spc *c = o;
 	assert(c->m == SPMCUR);
-	if (spunlikely(sp_eis(&c->s->e->e)))
+	if (spunlikely(sp_evalidate(c->s)))
 		return -1;
 	return sp_iterate(c);
 }
@@ -606,7 +787,6 @@
 	sp_lock(&s->lockr);
 	sp_lock(&s->locki);
 	sp_lock(&s->locks);
-
 	stat->epoch = s->rep.epoch;
 	stat->psn = s->psn;
 	stat->repn = s->rep.n;
@@ -615,7 +795,6 @@
 	stat->catn = s->s.count;
 	stat->indexn = s->i->count;
 	stat->indexpages = s->i->icount;
-
 	sp_unlock(&s->locks);
 	sp_unlock(&s->locki);
 	sp_unlock(&s->lockr);
diff --git a/sophia/makefile b/sophia/makefile
--- a/sophia/makefile
+++ b/sophia/makefile
@@ -1,7 +1,6 @@
-
 all:
-	@(cd db; make --no-print-directory)
-	@(cd test; make --no-print-directory)
+	@(cd db; $(MAKE))
+	@(cd test; $(MAKE))
 clean:
-	@(cd db; make clean --no-print-directory)
-	@(cd test; make clean --no-print-directory)
+	@(cd db; $(MAKE) clean)
+	@(cd test; $(MAKE) clean)
diff --git a/sophia/test/common.c b/sophia/test/common.c
--- a/sophia/test/common.c
+++ b/sophia/test/common.c
@@ -331,6 +331,24 @@
 }
 
 static void
+cursor_set(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == -1 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
 fetch_gte_empty(void) {
 	void *env = sp_env();
 	t( env != NULL );
@@ -807,62 +825,447 @@
 	t( rmrf(dbrep) == 0 );
 }
 
-#if 0
 static void
-rotate_empty(void) {
+fetch_kgte_random(void) {
 	void *env = sp_env();
 	t( env != NULL );
 	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
 	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
 	t( sp_ctl(env, SPMERGE, 0) == 0 );
-	t( sp_ctl(env, SPTTL, 2) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
 	void *db = sp_open(env);
-	t( exists(dbrep, 1, "log.incomplete") == 1 );
+	t( db != NULL );
+
+	uint32_t k = 0;
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 0;
+	while (k < 1024) {
+		void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k + n );
+			t( *(uint32_t*)sp_value(cur) == k + n );
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (1024 - k) );
+		k++;
+	}
+
 	t( sp_destroy(db) == 0 );
-	t( exists(dbrep, 1, "log.incomplete") == 0 );
-	t( exists(dbrep, 1, "log") == 0 );
 	t( sp_destroy(env) == 0 );
 	t( rmrf(dbrep) == 0 );
 }
 
 static void
-rotate(void) {
+fetch_kgte_random_merge(void) {
 	void *env = sp_env();
 	t( env != NULL );
 	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
 	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
 	t( sp_ctl(env, SPMERGE, 0) == 0 );
-	t( sp_ctl(env, SPTTL, 2) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
 	void *db = sp_open(env);
 	t( db != NULL );
-	uint32_t k = 1, v = 1;
-	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
-	t( exists(dbrep, 1, "log.incomplete") == 1 );
-	t( exists(dbrep, 1, "db.incomplete") == 0 );
-	t( exists(dbrep, 1, "db") == 0 );
-	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
-	t( exists(dbrep, 1, "log.incomplete") == 1 );
-	t( exists(dbrep, 2, "log.incomplete") == 1 );
-	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
-	t( exists(dbrep, 3, "log.incomplete") == 0 );
-	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
-	t( exists(dbrep, 1, "log.incomplete") == 1 );
-	t( exists(dbrep, 2, "log.incomplete") == 1 );
-	t( exists(dbrep, 3, "log.incomplete") == 1 );
+
+	uint32_t k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 0;
+	while (k < 1024) {
+		void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k + n );
+			t( *(uint32_t*)sp_value(cur) == k + n );
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (1024 - k) );
+		k++;
+	}
+
 	t( sp_destroy(db) == 0 );
-	t( exists(dbrep, 1, "db.incomplete") == 0 );
-	t( exists(dbrep, 1, "db") == 0 );
-	t( exists(dbrep, 1, "log.incomplete") == 0 );
-	t( exists(dbrep, 2, "log.incomplete") == 0 );
-	t( exists(dbrep, 3, "log.incomplete") == 0 );
-	t( exists(dbrep, 1, "log") == 1 );
-	t( exists(dbrep, 2, "log") == 1 );
-	t( exists(dbrep, 3, "log") == 0 );
 	t( sp_destroy(env) == 0 );
 	t( rmrf(dbrep) == 0 );
 }
-#endif
 
+static void
+fetch_kgte_random_merge_rev(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	uint32_t k = 512;
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 0;
+	while (k < 1024) {
+		void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k + n );
+			t( *(uint32_t*)sp_value(cur) == k + n );
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (1024 - k) );
+		k++;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+fetch_klte_random(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	int32_t k = 0;
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 1023;
+	while (k >= 0) {
+		void *cur = sp_cursor(db, SPLTE, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k - n );
+			t( *(uint32_t*)sp_value(cur) == k - n );
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (k + 1) );
+		k--;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+fetch_klte_random_merge(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	int32_t k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 1023;
+	while (k >= 0) {
+		void *cur = sp_cursor(db, SPLTE, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k - n );
+			t( *(uint32_t*)sp_value(cur) == k - n );
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (k + 1) );
+		k--;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+fetch_klte_random_merge_rev(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	int32_t k = 512;
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 1023;
+	while (k >= 0) {
+		void *cur = sp_cursor(db, SPLTE, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k - n );
+			t( *(uint32_t*)sp_value(cur) == k - n );
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (k + 1) );
+		k--;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+
+static void
+fetch_kgt_random_merge(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	uint32_t k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 0;
+	while (k < 1024) {
+		void *cur = sp_cursor(db, SPGT, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k + (n + 1));
+			t( *(uint32_t*)sp_key(cur) == k + (n + 1));
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (1023 - k) );
+		k++;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+fetch_kgt_random_merge_rev(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	uint32_t k = 512;
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 0;
+	while (k < 1024) {
+		void *cur = sp_cursor(db, SPGT, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k + (n + 1));
+			t( *(uint32_t*)sp_key(cur) == k + (n + 1));
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == (1023 - k) );
+		k++;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+fetch_klt_random_merge(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	int32_t k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 1023;
+	while (k >= 0) {
+		void *cur = sp_cursor(db, SPLT, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k - (n + 1));
+			t( *(uint32_t*)sp_value(cur) == k - (n + 1));
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == k );
+		k--;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+fetch_klt_random_merge_rev(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPPAGE, 64) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPMERGEWM, 256) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	int32_t k = 512;
+	while (k < 1024) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+
+	k = 0;
+	while (k < 512) {
+		t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		k++;
+	}
+
+	k = 1023;
+	while (k >= 0) {
+		void *cur = sp_cursor(db, SPLT, &k, sizeof(k));
+		t( cur != NULL );
+		uint32_t n = 0;
+		while (sp_fetch(cur)) {
+			t( *(uint32_t*)sp_key(cur) == k - (n + 1));
+			t( *(uint32_t*)sp_value(cur) == k - (n + 1));
+			n++;
+		}
+		t( sp_destroy(cur) == 0 );
+		t( n == k );
+		k--;
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
 int
 main(int argc, char *argv[])
 {
@@ -886,6 +1289,7 @@
 	test(delete);
 	test(delete_set_get);
 	test(cursor);
+	test(cursor_set);
 	test(fetch_gte_empty);
 	test(fetch_gt_empty);
 	test(fetch_lte_empty);
@@ -903,9 +1307,16 @@
 	test(fetch_klte);
 	test(fetch_klt);
 	test(fetch_after_end);
-	/*
-	test(rotate_empty);
-	test(rotate);
-	*/
+
+	test(fetch_kgte_random);
+	test(fetch_kgte_random_merge);
+	test(fetch_kgte_random_merge_rev);
+	test(fetch_klte_random);
+	test(fetch_klte_random_merge);
+	test(fetch_klte_random_merge_rev);
+	test(fetch_kgt_random_merge);
+	test(fetch_kgt_random_merge_rev);
+	test(fetch_klt_random_merge);
+	test(fetch_klt_random_merge_rev);
 	return 0;
 }
diff --git a/sophia/test/concurrent.c b/sophia/test/concurrent.c
new file mode 100644
--- /dev/null
+++ b/sophia/test/concurrent.c
@@ -0,0 +1,173 @@
+
+/*
+ * sophia database
+ * sphia.org
+ *
+ * Copyright (c) Dmitry Simonenko
+ * BSD License
+*/
+
+#include <sophia.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include "test.h"
+
+static char *dbrep1 = "./rep";
+static char *dbrep2 = "./rep2";
+
+static inline int
+cmp(char *a, size_t asz, char *b, size_t bsz, void *arg) {
+	register uint32_t av = *(uint32_t*)a;
+	register uint32_t bv = *(uint32_t*)b;
+	if (av == bv)
+		return 0;
+	return (av > bv) ? 1 : -1;
+}
+
+static void
+single_process(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep1) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	void *env2 = sp_env();
+	t( env2 != NULL );
+	t( sp_ctl(env2, SPDIR, SPO_CREAT|SPO_RDWR, dbrep1) == 0 );
+	void *db2 = sp_open(env2);
+	t( db2 == NULL );
+	t( sp_destroy(env2) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep1);
+}
+
+static void
+single_process_2(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep1) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	void *env2 = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env2, SPDIR, SPO_CREAT|SPO_RDWR, dbrep2) == 0 );
+	t( sp_ctl(env2, SPCMP, cmp, NULL) == 0 );
+	void *db2 = sp_open(env2);
+	t( db2 != NULL );
+	uint32_t k = 1;
+	t( sp_set(db2, &k, sizeof(k), &k, sizeof(k)) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	t( sp_get(db2, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( sp_destroy(db2) == 0 );
+	t( sp_destroy(env2) == 0 );
+	t( rmrf(dbrep1) == 0 );
+	t( rmrf(dbrep2) == 0 );
+}
+
+static void
+multi_process(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep1) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	int pid = fork();
+	t( pid != -1 );
+	if (pid == 0) {
+		/* new process */
+		void *env2 = sp_env();
+		t( env2 != NULL );
+		t( sp_ctl(env2, SPDIR, SPO_CREAT|SPO_RDWR, dbrep1) == 0 );
+		void *db2 = sp_open(env2);
+		t( db2 == NULL );
+		t( sp_destroy(env2) == 0 );
+		/* valgrind: parent db and env are unfreed here, and that
+		 *           is correct otherwise destroy would
+		 *           corrupt the database.
+		*/
+		exit(0);
+	} else {
+		int status = 0;
+		t( waitpid(pid, &status, 0) == pid );
+		t( status == 0 );
+	}
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep1);
+}
+
+static void
+multi_process_2(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep1) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	void *env2 = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env2, SPDIR, SPO_CREAT|SPO_RDWR, dbrep2) == 0 );
+	t( sp_ctl(env2, SPCMP, cmp, NULL) == 0 );
+	uint32_t k = 1;
+	size_t vsize = 0;
+	void *vp = NULL;
+	int pid = fork();
+	t( pid != -1 );
+	if (pid == 0) {
+		void *db2 = sp_open(env2);
+		t( db2 != NULL );
+		t( sp_set(db2, &k, sizeof(k), &k, sizeof(k)) == 0 );
+		t( sp_get(db2, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(k) );
+		t( *(uint32_t*)vp == k );
+		free(vp);
+		t( sp_destroy(db2) == 0 );
+		t( sp_destroy(env2) == 0 );
+		t( rmrf(dbrep2) == 0 );
+		/* valgrind: parent db and env are unfreed here, and that
+		 *           is correct otherwise destroy would
+		 *           corrupt the database.
+		*/
+		exit(0);
+	}
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0 );
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep1) == 0 );
+	int status = 0;
+	t( waitpid(pid, &status, 0) == pid );
+	t( status == 0 );
+}
+
+int
+main(int argc, char *argv[])
+{
+	rmrf(dbrep1);
+	rmrf(dbrep2);
+
+	test(single_process);
+	test(single_process_2);
+	test(multi_process);
+	test(multi_process_2);
+	return 0;
+}
diff --git a/sophia/test/crash.c b/sophia/test/crash.c
--- a/sophia/test/crash.c
+++ b/sophia/test/crash.c
@@ -83,15 +83,17 @@
 	};
 	speofh eof = { SPEOF };
 	spfile f;
+	spbatch b;
 	sp_fileinit(&f, &a);
+	sp_batchinit(&b);
 	t( mkdir(dbrep, 0755) == 0 );
 	t( sp_lognew(&f, dbrep, 1) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &eof, sizeof(eof));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &eof, sizeof(eof));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logcomplete(&f)  == 0 );
 	t( sp_logclose(&f)  == 0 );
 	t( exists(dbrep, 1, "log") == 1 );
@@ -124,15 +126,17 @@
 	};
 	speofh eof = { SPEOF };
 	spfile f;
+	spbatch b;
 	sp_fileinit(&f, &a);
+	sp_batchinit(&b);
 	t( mkdir(dbrep, 0755) == 0 );
 	t( sp_lognew(&f, dbrep, 1) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &eof, sizeof(eof));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &eof, sizeof(eof));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logclose(&f)  == 0 );
 	t( exists(dbrep, 1, "log.incomplete") == 1 );
 	void *env = sp_env();
@@ -175,14 +179,16 @@
 	};
 	speofh eof = { SPEOF };
 	spfile f;
+	spbatch b;
+	sp_batchinit(&b);
 	sp_fileinit(&f, &a);
 	t( sp_lognew(&f, dbrep, 2) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &eof, sizeof(eof));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &eof, sizeof(eof));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logcomplete(&f)  == 0 );
 	t( sp_logclose(&f)  == 0 );
 	t( exists(dbrep, 2, "log") == 1 );
@@ -214,14 +220,16 @@
 	crc    = sp_crc32c(crc, &k, sizeof(k));
 	vh.crc = sp_crc32c(crc, &vh.size, sizeof(spvh) - sizeof(uint32_t));
 	spfile f;
+	spbatch b;
+	sp_batchinit(&b);
 	sp_fileinit(&f, &a);
 	t( mkdir(dbrep, 0755) == 0 );
 	t( sp_lognew(&f, dbrep, 1) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logclose(&f)  == 0 );
 	t( exists(dbrep, 1, "log.incomplete") == 1 );
 	void *env = sp_env();
@@ -262,14 +270,17 @@
 	crc    = sp_crc32c(crc, &k, sizeof(k));
 	vh.crc = sp_crc32c(crc, &vh.size, sizeof(spvh) - sizeof(uint32_t));
 	spfile f;
+	spbatch b;
+	sp_batchinit(&b);
 	sp_fileinit(&f, &a);
+	sp_batchinit(&b);
 	t( mkdir(dbrep, 0755) == 0 );
 	t( sp_lognew(&f, dbrep, 1) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logcomplete(&f)  == 0 );
 	t( sp_logclose(&f)  == 0 );
 	t( exists(dbrep, 1, "log") == 1 );
@@ -382,15 +393,17 @@
 	crc    = sp_crc32c(crc, &k, sizeof(k));
 	vh.crc = sp_crc32c(crc, &vh.size, sizeof(spvh) - sizeof(uint32_t));
 	spfile f;
+	spbatch b;
+	sp_batchinit(&b);
 	sp_fileinit(&f, &a);
 	t( mkdir(dbrep, 0755) == 0 );
 	t( sp_lognew(&f, dbrep, 1) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &eof, sizeof(eof));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &eof, sizeof(eof));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logclose(&f)  == 0 );
 	char path[1024];
 	snprintf(path, sizeof(path), "%s/%"PRIu32".db.incomplete",
@@ -431,15 +444,17 @@
 	crc    = sp_crc32c(crc, &k, sizeof(k));
 	vh.crc = sp_crc32c(crc, &vh.size, sizeof(spvh) - sizeof(uint32_t));
 	spfile f;
+	spbatch b;
+	sp_batchinit(&b);
 	sp_fileinit(&f, &a);
 	t( mkdir(dbrep, 0755) == 0 );
 	t( sp_lognew(&f, dbrep, 1) == 0 );
-	sp_logadd(&f, &h, sizeof(h));
-	sp_logadd(&f, &vh, sizeof(vh));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &k, sizeof(k));
-	sp_logadd(&f, &eof, sizeof(eof));
-	t( sp_logflush(&f) == 0 );
+	sp_batchadd(&b, &h, sizeof(h));
+	sp_batchadd(&b, &vh, sizeof(vh));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &k, sizeof(k));
+	sp_batchadd(&b, &eof, sizeof(eof));
+	t( sp_logput(&f, &b) == 0 );
 	t( sp_logcomplete(&f) == 0 );
 	t( sp_logclose(&f)  == 0 );
 	t( exists(dbrep, 1, "log") == 1 );
diff --git a/sophia/test/issues.c b/sophia/test/issues.c
new file mode 100644
--- /dev/null
+++ b/sophia/test/issues.c
@@ -0,0 +1,129 @@
+
+/*
+ * sophia database
+ * sphia.org
+ *
+ * Copyright (c) Dmitry Simonenko
+ * BSD License
+*/
+
+#include <string.h>
+#include <sophia.h>
+#include "test.h"
+
+static char *dbrep = "./rep";
+
+static void
+gh_5(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+
+	char k1[] = "key-1";
+	char v1[] = "val-1";
+	t( sp_set(db, k1, sizeof(k1), v1, sizeof(v1)) == 0);
+
+	char k2[] = "key-10";
+	char v2[] = "val-10";
+	t( sp_set(db, k2, sizeof(k2), v2, sizeof(v2)) == 0);
+
+	size_t ressize = 0;
+	void *res = NULL;
+	t( sp_get(db, k1, sizeof(k1), &res, &ressize) == 1 );
+
+	t( ressize == sizeof(v1) );
+	t( strcmp(v1, res) == 0 );
+
+	free(res);
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+gh_29(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	int i;
+	for (i = 1; i <= 10; i++)  {
+		char key[4];
+		int len = snprintf(key, sizeof(key), "%d", i);
+		t( sp_set(db, key, len, key, len) == 0 );
+	}
+	for (i = 1; i <= 10; i++) {
+		char key[4];
+		int len = snprintf(key, sizeof(key), "%d", i);
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, key, len, &vp, &vsize) == 1 );
+		t( vsize == len );
+		t( memcmp(key, vp, len) == 0 );
+		free(vp);
+	}
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+gh_37(void)
+{
+	const int size = 999;
+	const int watermark = 3996;
+	const int pagesize = 1022;
+
+	char data[size];
+	memset(data, 0, sizeof(data));
+
+	void *env = sp_env();
+	int rc = sp_ctl(env, SPDIR, SPO_CREAT | SPO_RDWR, dbrep);
+	t(rc == 0);
+	if (rc == -1)
+
+	rc = sp_ctl(env, SPMERGE, 0);
+	t(rc == 0);
+
+	rc = sp_ctl(env, SPMERGEWM, watermark);
+	if (rc == -1)
+	t(rc == 0);
+
+	sp_ctl(env, SPPAGE, pagesize);
+	t(rc == 0);
+
+	void *db = sp_open(env);
+	t(db != NULL);
+
+	int byteCount = 0;
+	int i;
+	for (i = 0; i < 100000; ++i) {
+		rc = sp_set(db, &i, sizeof(int), data, size);
+		t(rc == 0);
+	}
+
+	byteCount += size;
+	if (i != 0 && i % watermark == 0) {
+		rc = sp_ctl(db, SPMERGEFORCE);
+		t(rc == 0);
+	}
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+int
+main(int argc, char *argv[])
+{
+	rmrf(dbrep);
+
+	test(gh_5);
+	test(gh_29);
+	test(gh_37);
+	return 0;
+}
diff --git a/sophia/test/limit.c b/sophia/test/limit.c
--- a/sophia/test/limit.c
+++ b/sophia/test/limit.c
@@ -31,7 +31,7 @@
 	t( db != NULL );
 	char buf[1];
 	t( sp_set(db, buf, UINT16_MAX + 1, buf, sizeof(buf)) == -1 );
-	t( sp_error(env) != NULL );
+	t( sp_error(db) != NULL );
 	t( sp_destroy(db) == 0 );
 	t( sp_destroy(env) == 0 );
 	rmrf(dbrep);
@@ -47,12 +47,32 @@
 	t( db != NULL );
 	char buf[1];
 	t( sp_set(db, buf, sizeof(buf), buf, UINT32_MAX + 1ULL) == -1 );
-	t( sp_error(env) != NULL );
+	t( sp_error(db) != NULL );
 	t( sp_destroy(db) == 0 );
 	t( sp_destroy(env) == 0 );
 	rmrf(dbrep);
 }
 
+static void
+error_nonfatal(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_set(db, &k, UINT16_MAX + 1 , &v, sizeof(v)) == -1);
+	t( sp_error(db) != NULL );
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_error(db) == NULL );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+
 int
 main(int argc, char *argv[])
 {
@@ -61,5 +81,7 @@
 	test(limit_key);
 	if (sizeof(size_t) > 4)
 		test(limit_value);
+
+	test(error_nonfatal);
 	return 0;
 }
diff --git a/sophia/test/recover.c b/sophia/test/recover.c
--- a/sophia/test/recover.c
+++ b/sophia/test/recover.c
@@ -1506,45 +1506,1656 @@
 	t( rmrf(dbrep) == 0 );
 }
 
-int
-main(int argc, char *argv[])
-{
-	rmrf(dbrep);
-
-	test(recover_log_set_get);
-	test(recover_log_replace_get);
-	test(recover_log_set_get_replace_get);
-	test(recover_log_delete_get);
-	test(recover_log_delete_set_get);
-	test(recover_log_fetch_gte);
-	test(recover_log_fetch_lte);
-	test(recover_log_fetch_kgte);
-	test(recover_log_fetch_kgt);
-	test(recover_log_fetch_klte);
-	test(recover_log_fetch_klt);
-	test(recover_log_n_get);
-	test(recover_log_n_replace);
-
-	test(recover_page_set_get);
-	test(recover_page_replace_get);
-	test(recover_page_set_get_replace_get);
-	test(recover_page_delete_get);
-	test(recover_page_delete_set_get);
-	test(recover_page_fetch_gte);
-	test(recover_page_fetch_lte);
-	test(recover_page_fetch_kgte);
-	test(recover_page_fetch_kgt);
-	test(recover_page_fetch_klte);
-	test(recover_page_fetch_klt);
-	test(recover_page_n_get);
-	test(recover_page_n_replace);
-
-	test(recover_page_log_fetch_gte);
-	test(recover_page_log_fetch_lte);
-	test(recover_page_log_fetch_kgte);
-	test(recover_page_log_fetch_kgt);
-	test(recover_page_log_fetch_klte);
-	test(recover_page_log_fetch_klt);
-	test(recover_page_log_n_replace);
+static void
+txn_recover_log_set_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == v );
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == v );
+	free(vp);
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == v );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_replace_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	v = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 3 );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_set_get_replace_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 1 );
+	free(vp);
+	v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	vsize = 0;
+	vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 2 );
+	free(vp);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	vsize = 0;
+	vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 2 );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_delete_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_delete(db, &k, sizeof(k)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_delete_set_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_delete(db, &k, sizeof(k)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	vsize = 0;
+	vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 2 );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_fetch_gte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_fetch_lte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPLTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_fetch_kgte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_fetch_kgt(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPGT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_fetch_klte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPLTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_fetch_klt(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPLT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_keysize(cur) == 0 );
+	t( sp_key(cur) == NULL );
+	t( sp_valuesize(cur) == 0 );
+	t( sp_value(cur) == NULL );
+	t( sp_fetch(cur) == 0 );
+	t( sp_keysize(cur) == 0 );
+	t( sp_key(cur) == NULL );
+	t( sp_valuesize(cur) == 0 );
+	t( sp_value(cur) == NULL );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_n_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 0, v;
+	while (k < 12) {
+		v = k;
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "log") == 1 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 0;
+	while (k < 12) {
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(v) );
+		t( *(uint32_t*)vp == k );
+		free(vp);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_log_n_replace(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( exists(dbrep, 1, "log.incomplete") == 1 );
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 0, v = 1;
+	while (k < 12) {
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	k = 0;
+	v = 2;
+	while (k < 12) {
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "log") == 1 );
+	db = sp_open(env);
+	t( exists(dbrep, 1, "log") == 1 );
+	t( exists(dbrep, 2, "log.incomplete") == 1 );
+	t( db != NULL );
+	k = 0;
+	while (k < 12) {
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(v) );
+		t( *(uint32_t*)vp == 2 );
+		free(vp);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "log") == 1 );
+	t( exists(dbrep, 2, "log") == 0 );
+	t( exists(dbrep, 2, "log.incomplete") == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_set_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	t( exists(dbrep, 2, "log.incomplete") == 1 );
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	t( exists(dbrep, 2, "log") == 0 );
+	t( exists(dbrep, 2, "log.incomplete") == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == v );
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == v );
+	free(vp);
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == v );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_replace_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	v = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 2, "log.incomplete") == 1 );
+	t( exists(dbrep, 2, "log") == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 3 );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_set_get_replace_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	v = 8;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 8 );
+	free(vp);
+	v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 2;
+	v = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	vsize = 0;
+	vp = NULL;
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 2 );
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 3 );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	vsize = 0;
+	vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 2 );
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 3 );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_delete_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_delete(db, &k, sizeof(k)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 1, "db.incomplete") == 0 );
+	t( exists(dbrep, 1, "db") == 0 );
+	t( exists(dbrep, 2, "log.incomplete") == 1 );
+	t( exists(dbrep, 2, "log") == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_delete_set_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_delete(db, &k, sizeof(k)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 1, "db.incomplete") == 0 );
+	t( exists(dbrep, 1, "db") == 0 );
+	t( exists(dbrep, 2, "log.incomplete") == 1 );
+	t( exists(dbrep, 2, "log") == 0 );
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 1, "db.incomplete") == 0 );
+	t( exists(dbrep, 1, "db") == 0 );
+	t( exists(dbrep, 2, "log.incomplete") == 0 );
+	t( exists(dbrep, 2, "log") == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_delete(db, &k, sizeof(k)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 1, "db.incomplete") == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	t( exists(dbrep, 2, "log.incomplete") == 1 );
+	t( exists(dbrep, 2, "log") == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	vsize = 0;
+	vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(v) );
+	t( *(uint32_t*)vp == 1 );
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_fetch_gte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_fetch_lte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPLTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_fetch_kgte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_fetch_kgt(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPGT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_fetch_klte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPLTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_fetch_klt(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPLT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_n_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 0, v;
+	while (k < 12) {
+		v = k;
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	t( exists(dbrep, 1, "log.incomplete") == 0 );
+	t( exists(dbrep, 1, "log") == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 0;
+	while (k < 12) {
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(v) );
+		t( *(uint32_t*)vp == k );
+		free(vp);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_n_replace(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 0, v = 1;
+	while (k < 12) {
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	k = 0;
+	v = 2;
+	while (k < 12) {
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 2, "db") == 1 );
+	t( exists(dbrep, 1, "log") == 0 );
+	t( exists(dbrep, 2, "log") == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 0;
+	while (k < 12) {
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(v) );
+		t( *(uint32_t*)vp == 2 );
+		free(vp);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_fetch_gte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 4;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 5;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 6;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 4 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 5 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 6 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_fetch_lte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 4;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 5;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 6;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPLTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 6 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 5 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 4 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_fetch_kgte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 4;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 5;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 6;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 4 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 5 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 6 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_fetch_kgt(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 4;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 5;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 6;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 2;
+	void *cur = sp_cursor(db, SPGT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 4 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 5 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 6 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_fetch_klte(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 4;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 5;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 6;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 4;
+	void *cur = sp_cursor(db, SPLTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 4 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_fetch_klt(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 4;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 5;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 6;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 4;
+	void *cur = sp_cursor(db, SPLT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+txn_recover_page_log_n_replace(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	t( sp_ctl(env, SPGC, 0) == 0 );
+	t( sp_ctl(env, SPMERGE, 0) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 0, v = 1;
+	while (k < 12) {
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		if (k > 0 && k % 3 == 0) {
+			t( sp_commit(db) == 0 );
+			t( sp_ctl(db, SPMERGEFORCE) == 0 );
+			t( sp_begin(db) == 0 );
+		}
+		k++;
+	}
+	t( sp_commit(db) == 0 );
+	k = 0;
+	v = 2;
+	while (k < 12) {
+		t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	t( exists(dbrep, 1, "db") == 1 );
+	t( exists(dbrep, 2, "db") == 1 );
+	t( exists(dbrep, 3, "db") == 1 );
+	t( exists(dbrep, 4, "log") == 1 );
+	db = sp_open(env);
+	t( db != NULL );
+	k = 0;
+	while (k < 12) {
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(v) );
+		t( *(uint32_t*)vp == 2 );
+		free(vp);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	t( sp_ctl(db, SPMERGEFORCE) == 0 );
+	t( exists(dbrep, 5, "db") == 1 );
+	t( exists(dbrep, 5, "log") == 0 );
+	t( exists(dbrep, 5, "log.incomplete") == 0 );
+	t( exists(dbrep, 6, "log.incomplete") == 1 );
+	k = 0;
+	while (k < 12) {
+		size_t vsize = 0;
+		void *vp = NULL;
+		t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+		t( vsize == sizeof(v) );
+		t( *(uint32_t*)vp == 2 );
+		free(vp);
+		k++;
+	}
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+int
+main(int argc, char *argv[])
+{
+	rmrf(dbrep);
+
+	test(recover_log_set_get);
+	test(recover_log_replace_get);
+	test(recover_log_set_get_replace_get);
+	test(recover_log_delete_get);
+	test(recover_log_delete_set_get);
+	test(recover_log_fetch_gte);
+	test(recover_log_fetch_lte);
+	test(recover_log_fetch_kgte);
+	test(recover_log_fetch_kgt);
+	test(recover_log_fetch_klte);
+	test(recover_log_fetch_klt);
+	test(recover_log_n_get);
+	test(recover_log_n_replace);
+
+	test(recover_page_set_get);
+	test(recover_page_replace_get);
+	test(recover_page_set_get_replace_get);
+	test(recover_page_delete_get);
+	test(recover_page_delete_set_get);
+	test(recover_page_fetch_gte);
+	test(recover_page_fetch_lte);
+	test(recover_page_fetch_kgte);
+	test(recover_page_fetch_kgt);
+	test(recover_page_fetch_klte);
+	test(recover_page_fetch_klt);
+	test(recover_page_n_get);
+	test(recover_page_n_replace);
+
+	test(recover_page_log_fetch_gte);
+	test(recover_page_log_fetch_lte);
+	test(recover_page_log_fetch_kgte);
+	test(recover_page_log_fetch_kgt);
+	test(recover_page_log_fetch_klte);
+	test(recover_page_log_fetch_klt);
+	test(recover_page_log_n_replace);
+
+	test(txn_recover_log_set_get);
+	test(txn_recover_log_replace_get);
+	test(txn_recover_log_set_get_replace_get);
+	test(txn_recover_log_delete_get);
+	test(txn_recover_log_delete_set_get);
+	test(txn_recover_log_fetch_gte);
+	test(txn_recover_log_fetch_lte);
+	test(txn_recover_log_fetch_kgte);
+	test(txn_recover_log_fetch_kgt);
+	test(txn_recover_log_fetch_klte);
+	test(txn_recover_log_fetch_klt);
+	test(txn_recover_log_n_get);
+	test(txn_recover_log_n_replace);
+
+	test(txn_recover_page_set_get);
+	test(txn_recover_page_replace_get);
+	test(txn_recover_page_set_get_replace_get);
+	test(txn_recover_page_delete_get);
+	test(txn_recover_page_delete_set_get);
+	test(txn_recover_page_fetch_gte);
+	test(txn_recover_page_fetch_lte);
+	test(txn_recover_page_fetch_kgte);
+	test(txn_recover_page_fetch_kgt);
+	test(txn_recover_page_fetch_klte);
+	test(txn_recover_page_fetch_klt);
+	test(txn_recover_page_n_get);
+	test(txn_recover_page_n_replace);
+
+	test(txn_recover_page_log_fetch_gte);
+	test(txn_recover_page_log_fetch_lte);
+	test(txn_recover_page_log_fetch_kgte);
+	test(txn_recover_page_log_fetch_kgt);
+	test(txn_recover_page_log_fetch_klte);
+	test(txn_recover_page_log_fetch_klt);
+	test(txn_recover_page_log_n_replace);
 	return 0;
 }
diff --git a/sophia/test/transaction.c b/sophia/test/transaction.c
new file mode 100644
--- /dev/null
+++ b/sophia/test/transaction.c
@@ -0,0 +1,1015 @@
+
+/*
+ * sophia database
+ * sphia.org
+ *
+ * Copyright (c) Dmitry Simonenko
+ * BSD License
+*/
+
+#include <sophia.h>
+#include "test.h"
+
+static char *dbrep = "./rep";
+
+static inline int
+cmp(char *a, size_t asz, char *b, size_t bsz, void *arg) {
+	register uint32_t av = *(uint32_t*)a;
+	register uint32_t bv = *(uint32_t*)b;
+	if (av == bv)
+		return 0;
+	return (av > bv) ? 1 : -1;
+}
+
+static void
+begin(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_begin(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	t( sp_begin(db) == -1 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_commit(db) == -1 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+rollback(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_rollback(db) == -1 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_commit_empty(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_rollback_empty(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_commit_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_commit(db) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_get_commit_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	size_t vsize = 0;
+	void *vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	t( sp_commit(db) == 0 );
+	vsize = 0;
+	vp = NULL;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k );
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_rollback(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_rollback(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_rollback_get(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_rollback(db) == 0 );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_rollback_consistent(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_begin(db) == 0 );
+	k = 1;
+	uint32_t v = k + 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 2;
+	v = k + 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+	k = 3;
+	v = k + 1;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0);
+
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k + 1);
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k + 1);
+	free(vp);
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k + 1);
+	free(vp);
+	t( sp_rollback(db) == 0 );
+
+	vsize = 0;
+	vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k);
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k);
+	free(vp);
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k);
+	free(vp);
+
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_commit_2x(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_begin(db) == 0 );
+	k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+rollback_on_close(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+rollback_recover(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_rollback(db) == 0);
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+commit_recover(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &k, sizeof(k)) == 0);
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	db = sp_open(env);
+	t( db != NULL );
+	size_t vsize = 0;
+	void *vp = NULL;
+	k = 1;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k);
+	free(vp);
+	k = 2;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k);
+	free(vp);
+	k = 3;
+	t( sp_get(db, &k, sizeof(k), &vp, &vsize) == 1 );
+	t( vsize == sizeof(k) );
+	t( *(uint32_t*)vp == k);
+	free(vp);
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+cursor_begin(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_begin(db) == -1 );
+	t( sp_commit(db) == -1 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+cursor_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_commit(db) == -1 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+cursor_closed_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+cursor_rollback(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_rollback(db) == -1 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	rmrf(dbrep);
+}
+
+static void
+begin_fetch_gte_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_gt_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	void *cur = sp_cursor(db, SPGT, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_lte_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	void *cur = sp_cursor(db, SPLTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_lt_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	void *cur = sp_cursor(db, SPLT, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_kgte_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+
+	k = 2;
+	void *cur = sp_cursor(db, SPGTE, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_kgt_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	void *cur = sp_cursor(db, SPGT, &k, sizeof(k));
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_klte_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	void *cur = sp_cursor(db, SPLTE, &k, sizeof(k) );
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_klt_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	void *cur = sp_cursor(db, SPLT, &k, sizeof(k) );
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_after_end_commit(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_keysize(cur) == 0 );
+	t( sp_key(cur) == NULL );
+	t( sp_valuesize(cur) == 0 );
+	t( sp_value(cur) == NULL );
+	t( sp_destroy(cur) == 0 );
+	t( sp_commit(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_fetch_gte_rollback(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_rollback(db) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_commit_fetch(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_commit(db) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 1 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 2 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 1 );
+	t( *(uint32_t*)sp_key(cur) == 3 );
+	t( sp_keysize(cur) == sizeof(k) );
+	t( *(uint32_t*)sp_value(cur) == 2 );
+	t( sp_valuesize(cur) == sizeof(v) );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+static void
+begin_rollback_fetch(void) {
+	void *env = sp_env();
+	t( env != NULL );
+	t( sp_ctl(env, SPDIR, SPO_CREAT|SPO_RDWR, dbrep) == 0 );
+	t( sp_ctl(env, SPCMP, cmp, NULL) == 0 );
+	void *db = sp_open(env);
+	t( db != NULL );
+	t( sp_begin(db) == 0 );
+	uint32_t k = 1, v = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 2;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	k = 3;
+	t( sp_set(db, &k, sizeof(k), &v, sizeof(v)) == 0 );
+	t( sp_rollback(db) == 0 );
+	void *cur = sp_cursor(db, SPGTE, NULL, 0);
+	t( cur != NULL );
+	t( sp_fetch(cur) == 0 );
+	t( sp_fetch(cur) == 0 );
+	t( sp_destroy(cur) == 0 );
+	t( sp_destroy(db) == 0 );
+	t( sp_destroy(env) == 0 );
+	t( rmrf(dbrep) == 0 );
+}
+
+int
+main(int argc, char *argv[])
+{
+	rmrf(dbrep);
+
+	test(begin);
+	test(begin_begin);
+	test(begin);
+	test(commit);
+	test(rollback);
+	test(begin_commit_empty);
+	test(begin_rollback_empty);
+	test(begin_commit);
+	test(begin_commit_get);
+	test(begin_get_commit_get);
+	test(begin_rollback);
+	test(begin_rollback_get);
+	test(begin_rollback_consistent);
+	test(begin_commit_2x);
+	test(rollback_on_close);
+	test(rollback_recover);
+	test(commit_recover);
+	test(cursor_begin);
+	test(cursor_commit);
+	test(cursor_closed_commit);
+	test(cursor_rollback);
+	test(begin_fetch_gte_commit);
+	test(begin_fetch_gt_commit);
+	test(begin_fetch_lte_commit);
+	test(begin_fetch_lt_commit);
+	test(begin_fetch_kgte_commit);
+	test(begin_fetch_kgt_commit);
+	test(begin_fetch_klte_commit);
+	test(begin_fetch_klt_commit);
+	test(begin_fetch_after_end_commit);
+	test(begin_fetch_gte_rollback);
+	test(begin_commit_fetch);
+	test(begin_rollback_fetch);
+	return 0;
+}
